作者: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 >= 0) { 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 -1; } 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 -1; } throwFileNotFoundException(env, path); return -1; } 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方法又是如何去读取文件的。
本章就到此结束了,感谢各位的收看,下次再见。
版权声明:本文版权归作者(左潇龙)所有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关推荐
赠送jar包:zstd-jni-1.4.3-1.jar; 赠送原API文档:zstd-jni-1.4.3-1-javadoc.jar; 赠送源代码:zstd-jni-1.4.3-1-sources.jar; 赠送Maven依赖信息文件:zstd-jni-1.4.3-1.pom; 包含翻译后的API文档:zstd-jni-...
libtensorflow_jni-cpu-windows-x86_64-1.6.0 libtensorflow_jni-cpu-windows-x86_64-1.6.0 libtensorflow_jni-cpu-windows-x86_64-1.6.0 libtensorflow_jni-cpu-windows-x86_64-1.6.0 libtensorflow_jni-cpu-...
赠送jar包:zstd-jni-1.4.3-1.jar; 赠送原API文档:zstd-jni-1.4.3-1-javadoc.jar; 赠送源代码:zstd-jni-1.4.3-1-sources.jar; 赠送Maven依赖信息文件:zstd-jni-1.4.3-1.pom; 包含翻译后的API文档:zstd-jni-...
标题中的"zstd-jni-1.4.0-1.zip"表明这是一个关于Zstandard (Zstd)的Java Native Interface (JNI)库的压缩包,版本号为1.4.0,可能包含了一些修复和改进。这个库允许Java应用程序利用Zstd的高效压缩和解压缩算法,而...
赠送jar包:zstd-jni-1.3.8-1.jar; 赠送原API文档:zstd-jni-1.3.8-1-javadoc.jar; 赠送源代码:zstd-jni-1.3.8-1-sources.jar; 赠送Maven依赖信息文件:zstd-jni-1.3.8-1.pom; 包含翻译后的API文档:zstd-jni-...
赠送jar包:zstd-jni-1.4.4-3.jar; 赠送原API文档:zstd-jni-1.4.4-3-javadoc.jar; 赠送源代码:zstd-jni-1.4.4-3-sources.jar; 赠送Maven依赖信息文件:zstd-jni-1.4.4-3.pom; 包含翻译后的API文档:zstd-jni-...
赠送jar包:zstd-jni-1.3.8-1.jar; 赠送原API文档:zstd-jni-1.3.8-1-javadoc.jar; 赠送源代码:zstd-jni-1.3.8-1-sources.jar; 赠送Maven依赖信息文件:zstd-jni-1.3.8-1.pom; 包含翻译后的API文档:zstd-jni-...
赠送jar包:zstd-jni-1.3.2-2.jar; 赠送原API文档:zstd-jni-1.3.2-2-javadoc.jar; 赠送源代码:zstd-jni-1.3.2-2-sources.jar; 赠送Maven依赖信息文件:zstd-jni-1.3.2-2.pom; 包含翻译后的API文档:zstd-jni-...
《深入解析JNIDemo-master 3.17版本》 在Java开发中,与C/C++进行交互是一项常见的需求,而JNI(Java Native Interface)就是为此设计的一种接口。本篇将围绕"Jnidemo-master.3.17.final.ok(20200813.final.ok)"这...
《JNI与C编程实践——深度解析Jnidemo-master项目》 JNI(Java Native Interface)是Java平台中用于连接Java代码和本地(非Java)代码的关键技术。它允许开发者在Java程序中调用C/C++代码,反之亦然,极大地拓展了...
赠送jar包:zstd-jni-1.4.4-3.jar; 赠送原API文档:zstd-jni-1.4.4-3-javadoc.jar; 赠送源代码:zstd-jni-1.4.4-3-sources.jar; 赠送Maven依赖信息文件:zstd-jni-1.4.4-3.pom; 包含翻译后的API文档:zstd-jni-...
1. **创建本地方法声明**:在Java类中,你需要声明本地方法。这些方法没有具体实现,而是使用`native`关键字标记,例如: ```java public class MyNativeClass { native void callCppFunction(); static { ...
赠送jar包:zstd-jni-1.3.2-2.jar; 赠送原API文档:zstd-jni-1.3.2-2-javadoc.jar; 赠送源代码:zstd-jni-1.3.2-2-sources.jar; 赠送Maven依赖信息文件:zstd-jni-1.3.2-2.pom; 包含翻译后的API文档:zstd-jni-...
本项目"jni-jna-web.zip"是一个基于Spring Boot 2.3.0的Web工程,深入实践了JNI和JNA的应用。 首先,让我们来了解一下JNI。JNI是一种原生接口,允许Java代码直接调用本地(非Java)代码。它通过定义了一套API,使得...
官方离线安装包,亲测可用。使用rpm -ivh [rpm完整包名] 进行安装
Android JNI(Java Native Interface)是Android平台上的一个关键特性,它允许Java代码和其他语言写...对于深入的Android开发,理解和掌握JNI是必不可少的技能之一,它可以让你更好地利用本地代码来提升应用性能和功能。
最近项目中需要使用JNI,所以研究了一下,其中遇到过不少问题,总结一下,让遇到同样问题的人可以得到解决。 在C/C++中调用Java的方法一般分为五个步骤:初始化虚拟机、获取类、获取类的方法、创建类对象、调用方法...
Android JNI基础-利用JNI实现JAVA调用C++代码
1. **准备C#库**:首先,确保你的C# DLL已经准备好并且可以在.NET环境中正常运行。 2. **转换DLL**:使用jni4net提供的工具将C# DLL转换为JNI兼容的库。 3. **配置jni4net**:在Java和.NET环境中配置jni4net,包括...
Java JNI例子-创建DLL、项目导入DLL、IDEA配置JNI、JNI调用DLL(该DLL同时依赖第三方DLL)