1. 程式人生 > >JVM垃圾回收分代機制及效能調優

JVM垃圾回收分代機制及效能調優

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 Tunning Practice(1) - 記憶體分代 - Harry - 染出一道彩虹

下面是一些需要關注的常用的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在初始狀態

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

2)在Eden存放新物件

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

3)Eden空間不足分配新物件,進行第一次minor gc

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

4) Eden區再次被寫滿,進行第二次minor gc

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

5)Eden再次被寫滿,進行第3次minor gc

第3次gc,發生了物件從from space提升到old區的遷移,然後也發生了from space到to space的copy

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

以下是Survivor space空間不足但物件的minor gc次數未到達MaxTenuringThreshold時的gc情況:

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC過程 - Harry - 染出一道彩虹

3.GC實戰  

在進行GC Tuning時有兩個很強大的利器:

jstat:用於檢視某java程序的gc情況;

jmap:檢視java程序堆疊分配和使用情況,以及dump出當前堆疊內容(可以用Eclipse MAT進行進一步分析)

以上兩個利器都是jdk自帶,且無需java程序新增任何額外的debug資訊輸出引數的,直接就可以對任意java程序進行跟蹤了。

       我認為GC調優的整體目標是要減緩GC的總體時間增加和降低每次gc引起的應用停頓(大概就是每次執行gc消耗的時間)。但往往我們只能在兩者間獲取一個平衡,在吞吐量和應用暫停時間之間取得一個平衡。

JVM Tunning Practice(2) - GC Tunning - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC Tunning - Harry - 染出一道彩虹

JVM Tunning Practice(2) - GC Tunning - Harry - 染出一道彩虹

調優前配置:-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