作者: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方法又是如何去讀取文件的。
本章就到此結束了,感謝各位的收看,下次再見。