1. 程式人生 > >java虛擬機器記憶體管理機制(一):JVM記憶體管理總結【分享】

java虛擬機器記憶體管理機制(一):JVM記憶體管理總結【分享】

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


如圖所示,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堆中的各代分佈。  
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時一直沒有備份成功過,總會有一些類的錯誤

簡單的概念:

堆(Heap)和非堆(Non-heap)記憶體
按照官方的說法:“Java 虛擬機器具有一個堆,堆是執行時資料區域,所有類例項和陣列的記憶體均從此處分配。堆是在 Java 虛擬機器啟動時建立的。”“在JVM中堆之外的記憶體稱為非堆記憶體(Non-heap memory)”。可以看出JVM主要管理兩種型別的記憶體:堆和非堆。簡單來說堆就是Java程式碼可及的記憶體,是留給開發人員使用的;非堆就是JVM留給 自己用的,所以方法區、JVM內部處理或優化所需的記憶體(如JIT編譯後的程式碼快取)、每個類結構(如執行時常數池、欄位和方法資料)以及方法和構造方法 的程式碼都在非堆記憶體中。 堆記憶體分配

JVM初始分配的記憶體由-Xms指定,預設是實體記憶體的1/64;JVM最大分配的記憶體由 -Xmx指定,預設是實體記憶體的1/4。預設空餘堆記憶體小於40%時,JVM就會增大堆直到-Xmx的最大限制;空餘堆記憶體大於70%時,JVM會減少堆 直到-Xms的最小限制。因此伺服器一般設定-Xms、-Xmx相等以避免在每次GC 後調整堆的大小。 非堆記憶體分配
JVM使用-XX:PermSize設定非堆記憶體初始值,預設是實體記憶體的1/64;由XX:MaxPermSize設定最大非堆記憶體的大小,預設是實體記憶體的1/4。 JVM記憶體限制(最大值)
首先JVM記憶體限制於實際的最大實體記憶體(廢話!呵呵),假設實體記憶體無限 大的話,JVM記憶體的最大值跟作業系統有很大的關係。簡單的說就32位處理器雖然可控記憶體空間有4GB,但是具體的作業系統會給一個限制,這個限制一般是 2GB-3GB(一般來說Windows系統下為1.5G-2G,Linux系統下為2G-3G),而64bit以上的處理器就不會有限制了

同系列文章: