java的Mmap二三事

 先說些題外話,Java的內存管理一直是讓人又愛又恨玩意。學生時代寫Java程序主要考慮的只是實現算法、完成功能,偶爾碰到OutofMemory也是遞歸搞成死循環。什麼垃圾回收、內存管理統統交給JVM去煩惱。上班之後再用Java發現完全不是那麼回事,大訪問量的情況下如何減少fullgc和停頓時間,內存管理無法由程序員控制和干預、甚至無法獲知究竟哪個部分用了多少內存有時着實讓人有些抓狂。各位可能無法想象當我們撞大運般的用jdk6_u12代替之前的某個版本解決了困擾多時的內存在高峯期無法正常回收的問題(後面會提到)時,那種如釋重負卻又心有不甘的感覺。因爲存在對於內存管理絲毫插不上手的無力感,從那時起,我就一直在尋找一種把數據丟到內存裏但又能夠對其有所控制的方法,mmap算是不錯的選擇。

    其實在日常工作中,很多時候最麻煩的問題是要把數據存在哪裏和如何對這些數據進行存取和管理。保存數據也無外乎數據庫、文件和內存這幾種方式,我這裏涉及到的情況是用戶登陸時把很多數據(比如好友列表、在線信息等)從各個地方拉過來丟到內存裏,當用戶logout之後再把這些東西清理出去,其實就是一個臨時的cache。目前的做法是弄幾個HashMap,再起幾個清理線程每隔一段時間遍歷一遍把map裏過期數據清理出去。看起來貌似沒啥問題,事實上之前跑了很長時間也沒出任何問題。但倒黴的是我接手不久之後,隨着用戶數量的增長,出現高峯期java進程佔用內存數量越來越高、直到把所有內存都吃乾淨然後掛掉的現象。但是我用jprofiler等工具經過仔細分析驗證之後發現即便是達到目前設計容量的上限,內存也還會有很大富餘,那問題就出在JVM沒有很好地進行內存回收。雖然升級JDK解決了問題,但是引發了我其他的一些思考。(1)對於內存管理引發的問題,如果jvm沒幫我們搞定怎麼辦?雖然sun的更新還算快,我們也許不會每次都如此幸運(2)java對象佔用的內存中遠比我們通過看到的東西計算出來的要大很多,例如一個含有800萬個key/value對的HashMap,其key和value都是int(注意不是Integer),在內存中大概就要340M。所以有些結構化數據,例如每個用戶的年齡、性別、生日等信息(按ID進行索引)似乎用共享內存的方法開闢出一塊緩衝區來管理更合適。(3)同樣是這些信息,雖然只是臨時的cache,但是我希望Server重啓之後這些信息不要丟失,因爲重新load需要不少時間和代價。
    可能有不少人有這種需求,所以sun在jdk1.4裏提供了共享內存和讀盤文件進行映射的mmap方法,有了它上述問題幾乎都可以得到解決,如果有更深層次的需要,比如數據格式定義和抽象、備份、擴容等需求的話也可以自己寫代碼進行擴展。寫mmap的代碼應該不難,這裏就不去討論了。這裏我想關注一下別的問題,那就是我們知道unix/linux中的mmap是一種進程間通信的方式之一,所以多個進程map到同一個文件的話在內存中應該只有一份鏡像數據。java的的底層實現也應該是這樣,但是網上和一些書中只有在多線程情況下的討論,沒親自驗證過畢竟不太放心,所以我就想辦法驗證一下。這裏值得一提的是,linux的ps、top等工具是沒辦法進行驗證的,他們看到的都是程序的虛擬內存空間的大小[VIRT列],如果用同樣的程序map同一個文件,那麼看到的VIRT值是一樣的。同理用more /proc/進程ID/maps也是一樣:
 
可以看到兩個進程map同一個文件之後所映射到的虛擬地址空間是一樣的,但是用同樣的程序換個同樣大小的文件還是會映射到這個地址區間去,可見用這個方法是不可靠的。那麼還是得用最原始的方法--寫個程序驗證一下。驗證的原理是mmap既然是進程間通信的方式之一,那麼如果一個進程先往裏寫而另一個進程讀的話,那麼讀的的進程就應該能夠讀到寫進程寫入的信息,這樣就能印證這個文件的映像在內存中只有一份copy了。我的程序如下:
程序的輸出如下:
看樣子,javammap沒讓我失望,確實是一個文件會map到內存相同的地址空間去,即該文件在內存中的映射只有一份。
PS:用/proc/進程ID/maps查看進程內存信息的時候還有點以外收穫,當我用"java -cp . 主類名"這種什麼都不加的方式啓動一個應用程序的時候用TOP查看VIRT總是顯示1G左右的大小,用/proc/進程ID/maps就可以比較清楚地看到可能是java虛擬機啓動時候分配給應用程序的共享內存,可能是怕我們不夠用吧^_^.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章