作者: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. Win7命令终端基础配色指南

    微软对控制台字体的元数据有严格的限制,https://support.microsoft.com/zh-cn/help/247815/necessary-criteria-for-fonts-to-b ...

  2. SQL慢查询安装过程

    SQL慢查询 基本操作 打开防火墙 firewall-cmd --zone=public --add-port=3306/tcp --permanent firewall-cmd --reload 安 ...

  3. 系统升级win7 sp1后,ado,MSJRO.tlh error 问题

    MSJRO.tlh() : error C2501: '_RecordsetPtr' : missing storage-class or type specifiers MSJRO.tlh() : ...

  4. sql语句查询月份的数据

    在实际项目中,经常需要按月查询数据,在这里把我用到的sql整理一下,以便日后查看. 例如,查询当月的数据 ),addtime,)),) 查询结果: 查询上月的数据,需要用另一个sql函数,datead ...

  5. Alpha冲刺报告(6/12)(麻瓜制造者)

    今日已完成 邓弘立: 看github上的开源库 确定了几个对UI改进有帮助的第三方库 符天愉: 部署了用户修改信息,修改头像的接口,并且完成两个接口的api文档,复习了PHP的无限分类来实现商品的发布 ...

  6. Alpha冲刺报告(11/12)(麻瓜制造者)

    今日已完成 邓弘立: 整合了主页的功能 符天愉: 大致上完成了留言部分的添加,删除,查询功能 江郑: 测试了剩余四个查询,一个添加接口,也搞定了接口说明. 刘双玉: 测试了剩余四个查询,一个添加接口, ...

  7. C#中抽象类(abstract)和接口(interface)的实现

    抽象类 抽象方法是没有代码实现的方法,使用abstract关键字修饰: 抽象类是包含0到多个抽象方法的类,其不能实例化.含有抽象方法的类必须是抽象类,抽象类中也可以包含非抽象方法: 重写抽象类的方法用 ...

  8. Markdown基本语法规范

    1. 标题 #的个数即表示Hn, 一下依次从h1~h6.  也可在句尾添加同样个数的#(也可忽略) # This is H1 ## This is H2 ### This is H3 #### Thi ...

  9. 函数式编程的终极形式:面向映射流的编程pipeline

    1.单体(数据)映射:基本操作:数据的单次映射: 2.管道流:数据的流程化处理 基础是monand类型,形式是声明式编程: Pipeline模型: 它以一种“链式模型”来串接不同的程序或者不同的组件, ...

  10. $Gauss$消元

    $Gauss$消元 今天金牌爷来问我一个高消的题目,我才想起来忘了学高消... 高斯消元用于解线性方程组,也就是形如: $\left\{\begin{matrix}a_{11}x_1+a_{12}x_ ...