Java文件映射[Mmap]揭祕

Java文件映射[mmap]揭祕

 前言

相信現在做Java的人沒有人不用NIO來進行IO相關的操作了吧。這個新的IO類庫[雖然現在已經不新了]爲我們帶來了基於塊的IO處理方式,通過預定義的Buffer,我們可以更高效地完成IO操作。在NIO中,我比較關注的是一個成爲mmap的文件映射功能,其特點是可以把文件的一部分或全部映射到內存中,之後我們就可以通過MappedBuffer對內存進行操作,而操作的結果會由操作系統負責flush到文件中。由於應用程序只是操作內存,所以處理速度比普通的文件操作快很多,在某些應用場景下mmap可以發揮相當大的作用。本文就來揭祕java的mmap背後的工作原理和實現方法,以及使用java的mmap要注意的一些問題。 

1       功能簡析

作爲NIO的一個重要的功能,Mmap方法爲我們提供了將文件的部分或全部映射到內存地址空間的能力,同當這塊內存區域被寫入數據之後[dirty],操作系統會用一定的算法把這些數據寫入到文件中[這一過程java並沒有提供API,後面會提到]。這樣我們實際上就獲得了間接操縱內存的能力,而且內存與文件之間的同步是由操作系統完成的,不用我們額外操心。也就是說,只要我們把內存數據塊規劃好[也就是實現一下C語言的SharedMemory功能],剩下的事情交給操作系統煩惱就好了。我們既獲得了高效的讀寫操作能力,又解決了數據的持久化問題,多麼理想的功能啊!但必須說明的是mmap畢竟不是數據庫,不能很方便地提供事務功能、類似sql語句那樣的查找功能,也不具備備份、回滾、遷移的能力,這些都要自己實現。不過這樣顯然不如放在數據庫裏放心,所以我們的經驗是特別重要的數據還是存數據庫,不太重要的、但是又訪問量很大、讀寫操作多且需要持久化功能的數據是最適合使用mmap功能的。使用Java的mmapAPI代碼框架如下所示:

(1)RandomAccessFile raf = new RandomAccessFile (File, "rw");

(2)FileChannel channel = raf.getChannel();

(3)MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_WRITE,startAddr,SIZE);

(4)buf.put((byte)255);

(5)buf.write(byte[] data)

其中最重要的就是那個buff,它是文件在內存中映射的標的物,通過對buff的read/write我們就可以間接實現對於文件的讀寫操作,當然寫操作是操作系統幫忙完成的。

雖然mmap功能是如此的強大,但凡事都有侷限,java的mmap瓶頸在哪裏?使用mmap會遇到哪些問題和限制?要回到這些問題,還是需要先從mmap的實現入手。 

2   實現原理

研究實現原理的最好方式就是閱讀源碼,由於SUN(或許不應該這樣叫了?)開放了JDK源碼,爲我們的研究敞開了大門,這裏我採用的是linux版的JDK1.6_u13的源碼。

2.1     目標和方法

在查看Java源碼之前,我首先google了一下mmap,結果發現mmap在linux下是一個系統調用:

void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off );

man了一下發現其功能描述和JavaAPI上說的差不多,難道JDK底層就是用這個東東實現的?馬上動手寫個程序然後STrace一下看看是不是使用了這個系統調用。這個測試程序應用的就是上面提到的那個程序框架,map了1G的文件,然後每次一個字節地往裏面寫數據,由於很簡單這裏就不貼出來了。結果如下:

 爲簡便起見中間的內容就忽略掉了,不過我們可以很清楚地看到mmap的操作就是打開[使用open系統調用]文件,然後mmap之,之後的操作都是對內存地址的直接操作,而操作系統負責把剩下的事情搞定了。於是可以大膽預言,java的實現是用JNI包裝了的mmap()系統調用。其功能也應該和下圖所示的內容保持一致。

《APUE》中關於Mmap()系統調用的示意圖

在經過上面的分析之後,我們已經有了初步的目標,那就是找到JavaMmap的C源碼,看其使用了哪些系統調用。這樣我們就可以更好地瞭解和控制JavaMmap的行爲。

 

2.2     詢源之旅

還是以上面這個代碼框架爲例,注意這裏除了map文件的動作之外就只有寫操作,因爲mmap的讀方法是讀內存的,我們已經很清楚,所以這裏我們只關心寫操作。通過閱讀源碼,我得到的結論如下:

 

(1)打開文件和建立FileChannel這兩步應該只有一個open()系統調用。

(2)mmap方法沒什麼懸念地用到了mmap系統調用。但值得注意的是JDK只提供了建立文件/內存映射的方法,而沒有給出解除映射關係的API。在FileChannelImpl.java中我們可以看到,解除映射的方法[在Unmapper中定義]是在創建MappedByteBuffer時嵌入到這個類裏面的,在buffer被GC回收之前會調用Unmapper的unmap方法來解除文件到內存的映射關係。也就是說我們要想解除映射只能先把buffer置爲null,然後祈禱GC趕緊起作用,實在等不及還可以用System.gc()催促一下GC趕快乾活,不過後果是會引發FullGC。

(3)對於map到內存中的部分的寫操作就是對內存地址的寫操作,只不過jdk用的是jni。 

3 詭異的問題  

因爲在一般運維監控的時候,我們都會很自然地選擇Top或者PS看一下進程當前實用的物理內存是多少,以防進程內存佔用過高導致系統崩潰。雖然TOP/PS的結果不是十分精確,但是大部分時候還是夠用的。然而在使用了java的mmap之後我們發現,top和ps命令居然失效了。在我們的程序中map了一個3G大小的文件[這個文件自此之後一直沒有變大],可是過幾天之後[當然程序裏面還有一些業務邏輯]卻發現TOP命令的RSS字段居然變成了19G,更誇張的是過幾天之後RSS的值仍然在不斷增長,這已經遠遠超過了內存的實際大小,但此時系統的IO並不高,效率沒有降低,也根本沒用到swap。這就是說TOP/PS的結果是有問題的,此時的RSS已經不能正確標示當前進程所佔用的物理內存了,而導致這個問題發生的原因又是什麼呢?

爲此我查看了一下/proc/PID/smaps文件,因爲這裏面描述了進程地址空間的使用情況,我得到的結果是:

同一個文件被map了幾次,smap文件中就有多少條記錄項。於是我們可以大膽猜想,TOP/PS命令是否就是把smaps文件的中RSS做了一個簡單的加法輸出出來?後來經我們驗證果然是這樣的!也就是說文件被統一進程map的次數越多,smaps裏面的對應項也就越多,所以TOP/PS的RSS字段值也就越大。

既然TOP/PS的值已經不可靠了,那麼應該怎樣獲取使用了mmap的進程當前所佔用的物理內存呢?google了一下排名最靠前的是一個叫做exmap的工具,不過那個工具不僅自己要重新編譯,還需要重新編譯內核[因爲可能操作系統禁用了Module載入],最不能接受的是還是圖形界面的,還有可能造成性能上的不穩定,這些限制使其在開發機上部署和使用變得不現實。後來用嘗試了一些系統調用和shell命令,效果都不太理想。

4       後記

我們略帶遺憾地結束了Java的Mmap之旅,最終也沒能找到一個簡單而準確的方法來查看當前進程的佔用了多少物理內存[前提是不引入影響系統性能的組件和不引入帶界面的東西],如果哪位有更好的辦法[無論是應用那個命令或者寫個小程序都可以]請通過email聯繫我[email protected],您的方法如果證明確實有效我會送您一個45cm的QQ公仔聊表謝意,同時也可以爲這篇文章畫上一個完滿的句號,期待中……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章