天天看點

Java記憶體回收 - 落日之心的日志

Java記憶體回收 - 落日之心的日志

近期看了看Java記憶體洩露的一些案例,跟原來的幾個哥們讨論了一下,深入研究發現JVM裡面還是有不少以前不知道的細節,這裡稍微剖析一下。先看一看JVM的内部結構——

Java記憶體回收 - 落日之心的日志

如圖所示,JVM主要包括兩個子系統和兩個元件。兩個子系統分别是Class loader子系統和Execution engine(執行引擎) 子系統;兩個元件分别是Runtime data area (運作時資料區域)元件和Native interface(本地接口)元件。

Class loader子系統的作用:根據給定的全限定名類名(如 java.lang.Object)來裝載class檔案的内容到 Runtime data area中的method area(方法區域)。Java程式員可以extends java.lang.ClassLoader類來寫自己的Class loader。

Execution engine子系統的作用:執行classes中的指令。任何JVM specification實作(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好壞主要就取決于他們各自實作的Execution  engine的好壞。

Native interface元件:與native libraries互動,是其它程式設計語言互動的接口。當調用native方法的時候,就進入了一個全新的并且不再受虛拟機限制的世界,是以也很容易出現JVM無法控制的native heap OutOfMemory。

Runtime Data Area元件:這就是我們常說的JVM的記憶體了。它主要分為五個部分——

1、Heap (堆):一個Java虛拟執行個體中隻存在一個堆空間

2、Method Area(方法區域):被裝載的class的資訊存儲在Method area的記憶體中。當虛拟機裝載某個類型時,它使用類裝載器定位相應的class檔案,然後讀入這個class檔案内容并把它傳輸到虛拟機中。

3、Java Stack(java的棧):虛拟機隻會直接對Java stack執行兩種操作:以幀為機關的壓棧或出棧

4、Program Counter(程式計數器):每一個線程都有它自己的PC寄存器,也是該線程啟動時建立的。PC寄存器的内容總是指向下一條将被執行指令的餓位址,這裡的位址可以是一個本地指針,也可以是在方法區中相對應于該方法起始指令的偏移量。

5、Native method stack(本地方法棧):儲存native方法進入區域的位址

以上五部分隻有Heap 和Method Area是被所有線程的共享使用的;而Java stack, Program counter 和Native method stack是以線程為粒度的,每個線程獨自擁有自己的部分。

了解JVM的系統結構,再來看看JVM記憶體回收問題了——

Sun的JVM Generational Collecting(垃圾回收)原理是這樣的:把對象分為年青代(Young)、年老代(Tenured)、持久代(Perm),對不同生命周期的對象使用不同的算法。(基于對對象生命周期分析)

Java記憶體回收 - 落日之心的日志

如上圖所示,為Java堆中的各代分布。

1. Young(年輕代)

年輕代分三個區。一個Eden區,兩個Survivor區。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象将被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象将被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複制過來的并且此時還存活的對象,将被複制年老區(Tenured。需要注意,Survivor的兩個區是對稱的,沒先後關系,是以同一個區中可能同時存在從Eden複制過來 對象,和從前一個Survivor複制過來的對象,而複制到年老區的隻有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。

2. Tenured(年老代)

年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象。

3. Perm(持久代)

用于存放靜态檔案,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動态生成或者調用一些class,例如Hibernate等,在這種時候需要設定一個比較大的持久代空間來存放這些運作過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設定。

舉個例子:當在程式中生成對象時,正常對象會在年輕代中配置設定空間,如果是過大的對象也可能會直接在年老代生成(據觀測在運作某程式時候每次會生成一個十兆的空間用收發消息,這部分記憶體就會直接在年老代配置設定)。年輕代在空間被配置設定完的時候就會發起記憶體回收,大部分記憶體會被回收,一部分幸存的記憶體會被拷貝至Survivor的from區,經過多次回收以後如果from區記憶體也配置設定完畢,就會也發生記憶體回收然後将剩餘的對象拷貝至to區。等到to區也滿的時候,就會再次發生記憶體回收然後把幸存的對象拷貝至年老區。

通常我們說的JVM記憶體回收總是在指堆記憶體回收,确實隻有堆中的内容是動态申請配置設定的,是以以上對象的年輕代和年老代都是指的JVM的Heap空間,而持久代則是之前提到的Method Area,不屬于Heap。

了解完這些之後,以下的轉載一熱衷于鑽研技術的哥們Richen Wang關于記憶體管理的一些建議——

1、手動将生成的無用對象,中間對象置為null,加快記憶體回收。

2、對象池技術 如果生成的對象是可重用的對象,隻是其中的屬性不同時,可以考慮采用對象池來較少對象的生成。如果有空閑的對象就從對象池中取出使用,沒有再生成新的對象,大大提高了對象的複用率。

3、JVM調優 通過配置JVM的參數來提高垃圾回收的速度,如果在沒有出現記憶體洩露且上面兩種辦法都不能保證記憶體的回收時,可以考慮采用JVM調優的方式來解決,不過一定要經過實體機的長期測試,因為不同的參數可能引起不同的效果。如-Xnoclassgc參數等。

推薦的兩款記憶體檢測工具

1、jconsole  JDK自帶的記憶體監測工具,路徑jdk bin目錄下jconsole.exe,輕按兩下可運作。連接配接方式有兩種,第一種是本地方式如調試時運作的程序可以直接連,第二種是遠端方式,可以連接配接以服務形式啟動的程序。遠端連接配接方式是:在目标程序的jvm啟動參數中添加-Dcom.sun.management.jmxremote.port=1090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false  1090是監聽的端口号具體使用時要進行修改,然後使用IP加端口号連接配接即可。通過該工具可以監測到當時記憶體的大小,CPU的使用量以及類的加載,還提供了手動gc的功能。優點是效率高,速度快,在不影響進行運作的情況下監測産品的運作。缺點是無法看到類或者對象之類的具體資訊。使用方式很簡單點選幾下就可以知道功能如何了,确實有不明白之處可以上網查詢文檔。

2、JProfiler 收費的工具,但是到處都有破解辦法。安裝好以後按照配置調試的方式配置好一個本地的session即可運作。可以監測當時的記憶體、CPU、線程等,能具體的列出記憶體的占用情況,還可以就某個類進行分析。優點很多,缺點太影響速度,而且有的類可能無法被織入方法,例如我使用jprofiler時一直沒有備份成功過,總會有一些類的錯誤。

作者:少帥

您的支援是對部落客最大的鼓勵,感謝您的認真閱讀。