作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可。

设计模式系列结束,迎来了LZ第一篇关于JAVA虚拟机的文章,这一系列文章不再像之前的设计模式一样,有着严格的约束力,本系列文章相对会比较随性,本次LZ就跟各位分享一个关于FileInputStream的小秘密。

在探究这个秘密之前,各位如果没有openjdk的源码,可以去LZ的资源先下载下来,链接是:JVM源码 和 JDK源码

由于资源有最大60MB的限制,所以LZ分成了两部分,一个是JVM的源码,一个是JDK中的源码,而本地方法的源码都在JDK的那个压缩包当中,全部源码下载在openjdk的官网上也有,各位也可以去那里找一下,如果嫌麻烦的话,就去LZ的资源里下载即可。

现在源码我们已经有了,可以来看下我们研究的小秘密了。大家都知道我们在读取文件时离不开FileInputStream这个类,那么不知道各位有没有好奇过,我们的FileInputStream是如何建立的呢?

我们一起先来看看FileInputStream的源码,我们平时都是通过new FileInputStream(name or File)的方式得到的文件输入流,所以我们来看FileInputStream的构造方法。

public
class FileInputStream extends InputStream
{
/* File Descriptor - handle to the open file */
private FileDescriptor fd; private FileChannel channel = null; public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
//这个方法是我们创建文件输入流时的方式
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
fd = new FileDescriptor();
open(name);
}

我们忽略安全管理器的检查,可以看到,在创建一个文件输入流时,主要做了两件事,一个是new一个FileDescriptor(文件描述符),一个便是调用了open方法

不过在此之前,其实还调用了一个方法,在FileInputStream源码的下方,有这样一个静态块。

static {
initIDs();
}

它将在第一次加载FileInputStream类的时候,调用一个静态的initIDs的本地方法,这里我们不跟踪这个方法的源码,它并不是我们的重点,它的作用是设置类中(也就是FileInputStream)的属性的地址偏移量,便于在必要时操作内存给它赋值,而FileInputStream的initIDs方法只设置了fd这一个属性的地址偏移量。

接下来,我们首先看下FileDescriptor这个类是什么样子的,它的源码如下。

package java.io;

public final class FileDescriptor {

    private int fd;

    private long handle;

    /**
* Constructs an (invalid) FileDescriptor
* object.
*/
public /**/ FileDescriptor() {
fd = -1;
handle = -1;
} private /* */ FileDescriptor(int fd) {
this.fd = fd;
handle = -1;
} static {
initIDs();
} /**
* A handle to the standard input stream. Usually, this file
* descriptor is not used directly, but rather via the input stream
* known as <code>System.in</code>.
*
* @see java.lang.System#in
*/
public static final FileDescriptor in = standardStream(0); /**
* A handle to the standard output stream. Usually, this file
* descriptor is not used directly, but rather via the output stream
* known as <code>System.out</code>.
* @see java.lang.System#out
*/
public static final FileDescriptor out = standardStream(1); /**
* A handle to the standard error stream. Usually, this file
* descriptor is not used directly, but rather via the output stream
* known as <code>System.err</code>.
*
* @see java.lang.System#err
*/
public static final FileDescriptor err = standardStream(2); /**
* Tests if this file descriptor object is valid.
*
* @return <code>true</code> if the file descriptor object represents a
* valid, open file, socket, or other active I/O connection;
* <code>false</code> otherwise.
*/
public boolean valid() {
return ((handle != -1) || (fd != -1));
} public native void sync() throws SyncFailedException; /* This routine initializes JNI field offsets for the class */
private static native void initIDs(); private static native long set(int d); private static FileDescriptor standardStream(int fd) {
FileDescriptor desc = new FileDescriptor();
desc.handle = set(fd);
return desc;
} }

可以看到,这里面也有initIDs的静态块,它与FileInputStream中的静态块的作用类似,只不过这里设置了两个属性(fd和handle)的地址偏移量。

如果抛开这两个静态块不说,其实到现在只是做了很简单的一件事,就是new了一个FileDescriptor对象,而最关键的地方其实都在FileInputStream的构造方法中一个名叫open(name)的这个本地方法当中,这个我们接下来再去看。

我们先看下FileDescriptor这个类,这个类有几个属性,一个是int类型的fd,目前没发现它有什么作用,唯一与它相关的构造方法还是私有的,而且在类中也没有调用,不过它与本次的分析并无关系,可先忽略。一个是long类型的handle(句柄),而handle这个属性就是最重要的属性了,它是一个文件的句柄,我们读取文件全靠它了,剩下的就是三个静态的标准流的FileDescriptor对象。

接下来我们就来看看open(name)这个方法到底做了什么,猜一下其实也大致知道,它一定是打开了一个文件,然后把得到的文件句柄赋给了handle属性,而赋值的时候,就要依赖于刚才initIDs所初始化的地址偏移量

下面我们就要看下open这个方法的源码了,不过本地方法以及JVM使用的编程语言是C/C++,所以研究JVM源码时,会给只懂JAVA的猿友们造成一定阻碍。不过不懂C++的猿友也不要失望,LZ会详细标注上每一句话的含义,只要是熟悉JAVA的猿友,基本上是能看懂的。

以下便是open这个方法的源码,FileInputStream.c中的一段代码。

JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open(JNIEnv *env, jobject this, jstring path) {
fileOpen(env, this, path, fis_fd, O_RDONLY);
}

这个方法没什么难度,它只是单纯的调用了一个叫fileOpen的方法,而这个方法是与具体的操作系统相关的,这也是为什么这里没有直接写实现的原因,我们随便找一个操作系统的实现来做例子,我们看一下windows当中的fileOpen的方法实现,以下是io_util_md.c文件的一段代码。

/*
env是一个指向JAVA本地方法环境的指针,它的作用大部分用来获取环境参数,比如当前线程。
this相信大家都不陌生,这就是指的当前FileInputStream的实例,只不过在C/C++环境中,它是jobject类型
path就是文件路径了,也是我们传进来的name参数
fid是FileInputStream类中fd属性的地址偏移量
flags是打开文件的方式,一般就是只读方式。
*/
void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
jlong h = winFileHandleOpen(env, path, flags);//这一句话就得到了一个文件的句柄
if (h >= ) {
SET_FD(this, h, fid);//这一句话就是将这个句柄赋给了FileDescriptor类的handle属性
}
}

LZ已经加了详细的注释,那么关键点还有两个,一个是winFileHandleOpen方法里做了什么,一个是SET_FD这个宏定义做了什么。虽然LZ已经解释了它们各自都做了什么,但是不看源码是不是始终不爽呢?

接下来我们先来看下winFileHandleOpen方法,这个方法就在fileOpen的上面。

/*
path是文件路径,flags代表的是只读
*/
jlong
winFileHandleOpen(JNIEnv *env, jstring path, int flags)
{
const DWORD access =
(flags & O_WRONLY) ? GENERIC_WRITE :
(flags & O_RDWR) ? (GENERIC_READ | GENERIC_WRITE) :
GENERIC_READ;//访问权限
const DWORD sharing =
FILE_SHARE_READ | FILE_SHARE_WRITE;//是否共享访问
const DWORD disposition =
/* Note: O_TRUNC overrides O_CREAT */
(flags & O_TRUNC) ? CREATE_ALWAYS :
(flags & O_CREAT) ? OPEN_ALWAYS :
OPEN_EXISTING;
const DWORD maybeWriteThrough =
(flags & (O_SYNC | O_DSYNC)) ?
FILE_FLAG_WRITE_THROUGH :
FILE_ATTRIBUTE_NORMAL;
const DWORD maybeDeleteOnClose =
(flags & O_TEMPORARY) ?
FILE_FLAG_DELETE_ON_CLOSE :
FILE_ATTRIBUTE_NORMAL;
const DWORD flagsAndAttributes = maybeWriteThrough | maybeDeleteOnClose;//
HANDLE h = NULL;//定义一个句柄 if (onNT) {//如果是NT系统
WCHAR *pathbuf = pathToNTPath(env, path, JNI_TRUE);//转成NT系统下的路径
if (pathbuf == NULL) {//等于空返回-1,-1就是空句柄
/* Exception already pending */
return -;
}
h = CreateFileW(
pathbuf, /* Wide char path name */
access, /* Read and/or write permission */
sharing, /* File sharing flags */
NULL, /* Security attributes */
disposition, /* creation disposition */
flagsAndAttributes, /* flags and attributes */
NULL);//CreateFileW是一个WIN API,可以打开一个文件
free(pathbuf);//释放内存
} else {//不是NT,那么就是XP WIN7 等各位熟悉的系统,WITH_PLATFORM_STRING和END_PLATFORM_STRING都是宏定义
//这个就没必要带各位再去分析宏定义了,主要作用是将jstring转换成与平台相关的char *类型变量。
WITH_PLATFORM_STRING(env, path, _ps) {
h = CreateFile(_ps, access, sharing, NULL, disposition,
flagsAndAttributes, NULL);//最终这个方法也是得到一个文件句柄
} END_PLATFORM_STRING(env, _ps);
}
if (h == INVALID_HANDLE_VALUE) {//如果句柄为无效句柄,则抛出FileNotFoundException异常,相信各位都不陌生
int error = GetLastError();
if (error == ERROR_TOO_MANY_OPEN_FILES) {
JNU_ThrowByName(env, JNU_JAVAIOPKG "IOException",
"Too many open files");
return -;
}
throwFileNotFoundException(env, path);
return -;
}
return (jlong) h;//返回句柄
}

LZ已经在上面方法加了注释,相信熟悉JAVA的同学哪怕不懂C/C++,也不难看懂上面这个函数,而唯一LZ没有解释全的就是CreateFile方法那几个参数,这个如果各位有兴趣可以去搜索一下,百度上有很多这个方法的参数的具体解释,但是不管怎么说,我们都是以只读方式打开了一个文件。也就是在上面调用fileOpen方法时传入的O_RDONLY这个参数代表的含义。

搞清楚了文件句柄获取的过程,下面我们来看一下,这个句柄是如何赋给了FileDescriptor类的handle属性,我们看一下SET_FD这个宏定义都做了什么。

#define SET_FD(this, fd, fid) \
if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \//这一句,是判断FileInputStream这个对象的fd属性是不是空
//如果不是空的话,调用了一个SetLongField的方法,看它的参数,(*env)->GetObjectField(env, (this), (fid))这个传入
//的是FileInputStream这个对象的fd属性,IO_handle_fdID是handle属性的地址偏移量,fd则是文件句柄的值
//我们不需要进去看,就能看出来这个函数就是把fd赋给了FileInputStream这个对象的fd属性的handle属性。
(*env)->SetLongField(env, (*env)->GetObjectField(env, (this), (fid)), IO_handle_fdID, (fd))

这下我们已经明白了,综合上面的分析过程,我们可以总结出当我们new一个FileInputStream的时候,都做了哪些步骤。

下面LZ将这些步骤写出来:

          1、如果FileInputStream类尚未加载,则执行initIDs方法,否则这一步直接跳过。

          2、如果FileDescriptor类尚未加载,则执行initIDs方法,否则这一步也直接跳过。

          3、new一个FileDescriptor对象赋给FileInputStream的fd属性。

          4、打开一个文件句柄。

          5、将文件句柄赋给FileDescriptor对象的handle属性。

到此我们已经将FileInputStream的创建过程全部搞清楚了,不过一直分析下来好像都一直在看C/C++代码了,下面LZ给各位写了一个小程序,是使用的FileDescriptor这个类的静态变量,也就是那几个标准流。

import java.io.FileDescriptor;
import java.io.FileWriter;
import java.io.IOException; public class Client { public static void main(String[] args) throws IOException {
FileDescriptor descriptor = FileDescriptor.out;
FileWriter fileWriter = new FileWriter(descriptor);
fileWriter.write("hello world");
fileWriter.flush();
fileWriter.close();
}
}

输出结果为hello world,相信不出大家意料,不过这是不是挺有意思的呢?好了,本次简单的分析了一下FileInputStream对象的创建,下次我们再来看看在获得了handle(文件句柄)之后,read方法又是如何去读取文件的。

本章就到此结束了,感谢各位的收看,下次再见。

JNI探秘-----你不知道的FileInputStream的秘密的更多相关文章

  1. JNI探秘-----FileInputStream的read方法详解

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 上一章我们已经分析过File ...

  2. 网络营销行业十大看了就想吐的“滥词”

    网络营销行业在国内的互联网界已"猖獗"数年之久,它是一个让企业爱让用户恨的行业.有互联网的地方,就有网络营销的机会,有了机会就有了相关产业的存在,只不过是业大业小的问题.但是随着互 ...

  3. [肯定不知道]PeopleSoft中PSADMIN你不知道的秘密

    PeopleSoft psadmin工具是用于管理PS App server,process scheduler 和 web server节点的.可以使用一些设置菜单选项来管理或配置上面提到的任何组件 ...

  4. webstorm你不知道的秘密

    相信你们用webstorm肯定都会用上下面介绍的Emmet插件这个可以自带的哦 Emmet语法 子代:> 兄弟:+ 父代:^ 重复:* 成组:() ID:# class:. 属性:[] 编号:$ ...

  5. JavaScript arguments你不知道的秘密

    (function test(x){ x=10; console.log(arguments[0], x); //undefined, 10 })(); (function test(x){ x=10 ...

  6. go语言 defer 你不知道的秘密!

    go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱哦.我们先来看几个例子. 例一: defer 是先进后出 这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先 ...

  7. Mac Idea你不知道的秘密

    导读 工欲善其事必先利其器,日常工作中,知道这些Idea技巧,可以极大提高日常开发效率. 技巧篇 以下内容不分先后顺序 显示类中的方法 搜索 搜索方法,按两下shift 文字搜索,control+sh ...

  8. 探秘Java类加载

    Java是一门面向对象的编程语言. 面向对象以抽象为基础,有封装.继承.多态三大特性. 宇宙万物,经过抽象,均可归入相应的种类.不同种类之间,有着相对井然的分别. Java中的类,便是基于现实世界中的 ...

  9. .NET跨平台之旅:探秘 dotnet run 如何运行 .NET Core 应用程序

    自从用 dotnet run 成功运行第一个 "Hello world" .NET Core 应用程序后,一直有个好奇心:dotnet run 究竟是如何运行一个 .NET Cor ...

随机推荐

  1. 《MySQL技术内幕:InnoDB存储引擎(第2版)》书摘

    MySQL技术内幕:InnoDB存储引擎(第2版) 姜承尧 第1章 MySQL体系结构和存储引擎 >> 在上述例子中使用了mysqld_safe命令来启动数据库,当然启动MySQL实例的方 ...

  2. LUA 运算笔记

    循环 比如要实现这样的一个For for(int i=10;i>1;i—) { print(i) } lua的for循环 转换成LUA for i=10,1,-1 do print(i) end ...

  3. mySQL 约束 (Constraints):一、非空约束 NOT NULL 约束

    非空约束 NOT NULL 约束: 强制列不能为 NULL 值,约束强制字段始终包含值.这意味着,如果不向字段添加值,就无法插入新记录或者更新记录. 1.在 "Persons" 表 ...

  4. 实现CTreeCtrl父子节点的联动选择

    本文实现了下面的功能: 当选中父节点时, 其所有子节点全部选中. 当取消选中父节点时, 其所以子节点全部取消选中. 点击子节点时, 根据子节点与其兄弟节点的选中状态, 自动设置父节点的选中状态. 通过 ...

  5. 数据库迁移之从oracle 到 MySQL最简单的方法

    数据库迁移之从oracle 到 MySQL最简单的方法 因工作需要将oracle数据库换到MySQL数据库,数据量比较大,百万级别的数据,表也比较多,有没有一种既快捷又安全的方法呢?答案是肯定的,下面 ...

  6. supervisor 使用系列之一

    supervisor 使用系列之一 前几年自己用PHP写过一个服务守护的脚本,初步实现了被守护脚本的状态监控.优雅杀死.以及自动重启的功能.面试的时候也有问到,为什么不使用supervisor这个工具 ...

  7. October 27th, 2017 Week 43rd Friday

    The only thing predictable about life is its unpredictability. 人生唯一可以预知的,就是它的变化莫测. Is it really unpr ...

  8. 解决The valid characters are defined in RFC 7230 and RFC 3986错误问题

    分析原因: 导致上述问题是因为tomcat自tomcat 8.0.35版本之后对URL参数做了比较规范的限制,必须遵循RFC 7230 and RFC 3986规范,对于非保留字字符(json格式的请 ...

  9. MYSQL一次千万级连表查询优化(一)

    摘自网上学习之用 https://blog.csdn.net/Tim_phper/article/details/78344444 概述: 交代一下背景,这算是一次项目经验吧,属于公司一个已上线平台的 ...

  10. jsp运算符

    一.执行运算: 支持四则运算.关系(>;<....),逻辑运算(&&.||) 注意: +:只能进行加法运算,字符串形式的数字可以进行加法运算,这里的+号不能进行字符串的拼接 ...