JVM Specification中的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, 換句話說:Sun 的JDK 和IBM的JDK好壞主要取決於他們各自實現的Execution engine的好壞。每個運行中的線程都有一個 Execution engine的實例。
Native interface 組件 :與native libraries 交互,是其它編程語言交互的接口。
Runtime data area 組件:這個組件就是 JVM中的內存。
Runtime data area 的整體架構圖
Runtime data area 主要包括五個部分:Heap (堆), Method Area(方法區域), Java Stack(java 的棧), Program Counter(程序計數器), Native method stack(本地方法棧)。Heap 和Method Area 是被所有線程的共享使用的;而Java stack, Program counter 和 Native method stack 是以線程爲粒度的,每個線程獨自擁有。
Heap
Java程序在運行時創建的所有類實例或數組都放在同一個堆中。而一個Java虛擬實例中只存在一個堆空間,因此所有線程都將共享這個堆。每一個 java程序獨佔一個JVM實例,因而每個 java程序都有它自己的堆空間,它們不會彼此干擾。但是同一java程序的多個線程都共享着同一個堆空間,就得考慮多線程訪問對象(堆數據)的同步問 題。(這裏可能出現的異常 java.lang.OutOfMemoryError: Java heap space)
Method area
在Java 虛擬機中,被裝載的 class的信息存儲在 Method area的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的 class文件,然後讀入這個class文件內容並把它傳輸到虛擬機中。緊接着虛擬機提取其中的類型信息,並將這些信息存儲到方法區。該類型中的類(靜 態)變量同樣也存儲在方法區中。與Heap 一樣,method area 是多線程共享的,因此要考慮多線程訪問的同步問題。比如,假設同時兩個線程都企圖訪問一個名爲 Lava的類,而這個類還沒有內裝載入虛擬機,那麼,這時應該只有一個線程去裝載它,而另一個線程則只能等待。 (這裏可能出現的異常 java.lang.OutOfMemoryError: PermGen full)
Java stack
Java stack 以幀爲單位保存線程的運行狀態。虛擬機只會直接對 Java stack執行兩種操作:以幀爲單位的壓棧或出棧。每當線程調用一個方法的時候,就對當前狀態作爲一個幀保存到 java stack 中(壓棧);當一個方法調用返回時,從java stack 彈出一個幀(出棧)。棧的大小是有一定的限制,這個可能出現StackOverFlow 問題,例如遞歸的層數太深。
Program counter
每個運行中的Java程序,每一個線程都有它自己的PC寄存器,也是該線程啓動時創建的。PC寄存器的內容總是指向下一條將被執行指令的地址,這裏的地址可以是一個本地指針,也可以是在方法區中相對應於該方法起始指令的偏移量。
Native method stack
對於一個運行中的Java程序而言,它還能會用到一些跟本地方法相關的數據區。當某個線程調用一個本地方法時,它就進入了一個全新的並且不再受虛擬機限 制的世界。本地方法可以通過本地方法接口來訪問虛擬機的運行時數據區,不止如此,它還可以做任何它想做的事情。比如,可以調用寄存器,或在操作系統中分配 內存等。總之,本地方法具有和JVM 相同的能力和權限。 (這裏出現 JVM無法控制的內存溢出問題 native heap OutOfMemory ) 。
Sun JVM 中對 JVM Specification 的實現(內存部分)
JVM Specification只是抽象的說明了 JVM 實例按照子系統、內存區、數據類型以及指令這幾個術語來描述的, 但是規範並非是要強制規定 Java 虛擬機實現內部的體系結構,更多的是爲了嚴格地定義這些實現的外部特徵。 Sun JVM 實現中:Runtime data area(JVM 內存) 五個部分中的 Java Stack , Program Counter, Native method stack 三部分和規範中的描述基本一致;但對 Heap 和 Method Area 進行了自己獨特的實現。這個實現和 Sun JVM 的Garbage collector(垃圾回收)機制有關。
垃圾分代回收算法(Generational Collecting)
基於對對象生命週期分析後得出的垃圾回收算法。把對象分爲年青代、年老代、持久代,對不同生命週期的對象使用不同的算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此算法的。
如上圖所示,爲Java 堆中的各代分佈。
1. Young(年輕代)JVM specification 中的 Heap的一部分。年輕代分三個區。一個Eden區,兩個 Survivor區。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到 Survivor區(兩個中的一個),當這個 Survivor區滿時,此區的存活對象將被複制到另外一個 Survivor區,當這個 Survivor去也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的對象,將被複制到年老區(Tenured)。需要注 意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來的對象,和從前一個 Survivor複製過來的對象,而複製到年老區的只有從第一個 Survivor 去過來的對象。而且,Survivor 區總有一個是空的。
2. Tenured(年老代)JVM specification中的 Heap的一部分。年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象。
3. Perm(持久代) JVM specification 中的 Method area 用於存放靜態文件,如 Java 類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些 class,例如 Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設置。
1.內存分代
JVM的內存分代管理結構:
下面是一些需要關注的常用的JVM內存配置參數,我們來看看它們是如何影響上圖中的比例的。
1)Heap Size
-Xmx ---最大Heap Size,即上圖的Total size(包括Eden+form+to,Tenured,不包含Perm,見上圖),限制了年輕代和年老代的可分配最大值;
-Xms ---初始化分配的Heap Size
生產環境中ms一般設置成跟mx相等,因爲若ms不等於mx那麼在某些場景下JVM可能需要對Heap Size進行頻繁的擴展和收縮,增加處理時間;
2)New/Young Generation Size
-Xmn ---最大年輕代大小,即上圖中的Eden+S0+S1+Virtual
-XX:NewSize ---初始化年輕代大小,即上圖中的Eden+S0+S1,在只設置了-Xmn不設置-XX:NewSize的情況下,NewSize等於mn。
生產環境中一般只需設置-Xmn或者設置mn和NewSize相等,理由和HeapSize的設置一樣,避免容量震盪消耗資源;
3)Old Generation Size (Tenured)
-XX:NewRatio --- Old Size/New Size,通過年老代和年輕代的比例和Heap Size就可以算出年老代的大小。一般默認爲8,若Heap Size爲1024m,則 NewSize=HeapSize/(NewRatio+1)=114m,OldSize=HeapSize-NewSize=910m;
注意:-Xmn的優先級比-XX:NewRatio高,若-Xmn已指定,則OldSize=HeapSize-NewSize,無需再按比例計算。生產環境中一般只需指定-Xmn就足夠了。
4)Eden和S0、S1
-XX:SurvivorRatio --- Eden/S0,即 Eden區和S0的比例,默認爲8,若NewSize爲114m,則S0=NewSize/(SurvivorRatio+2)=11.4m;
S0==S1,S0、S1的職能是一模一樣的,又叫做From space和To space,在每一次minor gc后角色會交換。
注意:-XX類型的選項在不同的JDK版本或實現中定義可能有所區別,在近日的實踐中發現,
在Linux jdk_1_5_0_10_x86版本中,SurvivorRatio=(YoungSize/S0),而Linux jdk_1_5_0_20_x64版本中,SurvivorRatio=(Eden/S0)
所以,我們在實際的工程實踐中還是應該用jmap -heap輸出的jvm內存結構信息爲準,不要想當然。
5)Permanent Generation Size
-XX:MaxPermSize ---最大持久代大小,默認爲64m;
-XX:PermSize ---初始化持久代大小,默認爲16m;
生產環境中一般設置MaxPermSize和PermSize相等,理由和HeapSize的設置一樣,避免容量震盪消耗資源;
當應用引用的類比較多或者應用了一些動態類生產技術時應該加大該區的值,一般256m對服務器程序都很足夠了。
下面是一些JVM對Native Memory內存的使用:
6)Thread Stack Size
-Xss ---線程堆棧大小,一般用於存放方法入口參數和返回值,以及原子類型的本地變量(即方法內部變量);
一般可設置爲128k.
7)Direct Memory Size
-XX:MaxDirectMemorySize ---direct byte buffer用到的本地內存,在本blog的一篇《xsocket內存泄漏》文章中介紹過該參數的作用。默認跟mx相等,所以生產環境中一般不設置mx大於物理內存的一半。
2.GC過程
在講述GC過程前我先解釋一下JVM的兩個控制參數:
-XX:TargetSurvivorRatio --- Survivor Space最大使用率,若存放對象的總大小超過該值,將引起對象向Old區遷移;
-XX:MaxTenuringThreshold --- Young區對象的最大任期閥值,即可經歷minor gc的次數,超過該次數的對象直接遷移到Old區;
實際的TenuringThreshold由JVM通過Survivor Space的佔用率和TargetSurvivorRatio動態計算,詳情請查看參考資料。
《HP-MemoryManagement.pdf》中有對JVM GC過程的形象描述,我借用其中的一些圖例來說明一下。
1)Heap在初始狀態
2)在Eden存放新對象
3)Eden空間不足分配新對象,進行第一次minor gc
4) Eden區再次被寫滿,進行第二次minor gc
5)Eden再次被寫滿,進行第3次minor gc
第3次gc,發生了對象從from space提升到old區的遷移,然後也發生了from space到to space的copy
以下是Survivor space空間不足但對象的minor gc次數未到達MaxTenuringThreshold時的gc情況:
3.GC實戰
在進行GC Tuning時有兩個很強大的利器:
jstat:用於查看某java進程的gc情況;
jmap:查看java進程堆棧分配和使用情況,以及dump出當前堆棧內容(可以用Eclipse MAT進行進一步分析)
以上兩個利器都是jdk自帶,且無需java進程添加任何額外的debug信息輸出參數的,直接就可以對任意java進程進行跟蹤了。
我認爲GC調優的整體目標是要減緩GC的總體時間增加和降低每次gc引起的應用停頓(大概就是每次執行gc消耗的時間)。但往往我們只能在兩者間獲取一個平衡,在吞吐量和應用暫停時間之間取得一個平衡。
調優前配置:-Xmx1024m -Xms1024m
調優前gc情況:jstat -gcutil <pid> 3000
minor gc: 3~6次/3秒
full gc: 1次/30秒
調優後配置:-Xmx2g -Xms2g -Xmn1g -Xss128k -XX:NewSize=1g -XX:PermSize=128m
-XX:MaxPermSize=256m -XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=60
-XX:MaxTenuringThreshold=20 -XX:+UseParNewGC -XX:ParallelGCThreads=8 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0
調優後gc情況:
minor gc: 1次/15秒
full gc: 1次/數小時到數十小時
UseParNewGC表示對新生代採用並行gc;
ParallelGCThreads表示並行的線程數爲8,一般是cpu的核個數,當核個數大於8時可能不是很適用;
UseConcMarkSweepGC表示對full gc採用CMS gc;
另外還有幾個跟GC有關的有用參數,這裏沒有用到:
-XX:+DisableExplicitGC 表示禁止顯式gc,System.gc()
-XX:+UseCMSCompactAtFullCollection 適用於CMS gc,表示在進行gc的同時清理內存碎片,但會加長gc的總時間
-XX:CMSInitiatingOccupancyFraction=80 適用於CMS gc,表示在年老代達到80%使用率時馬上進行回收
另外下面是在JVM Crash時獲heap信息的一些配置參數:
-XX:ErrorFile=./xxx.log JVM Crash時記錄heap信息
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./yyy.log JVM OOM時記錄heap信息
拿到heap文件後可以用Eclipse MAT進行分析,找出引起內存泄漏的class。
參考資料:
1)HP-MemoryManagement.pdf
2)http://www.51testing.com/?uid-77492-action-viewspace-itemid-203728
3)http://sunqi.javaeye.com/blog/486048
4)http://fallenlord.blogbus.com/logs/57543373.html