如題,本文的宗旨既是透過對象的生命周期,來梳理JVM內存結構及GC相關知識,並輔以AOP及雙親委派機制原理,學習不僅僅是海綿式的吸收學習,還需要自己去分析why,加深對技術的理解和認知,祝大家早日走上自己的“成金之路”。
Java對象的創建
本部分,從攻城獅編寫.java文件入手,詳解了編譯、載入、AOP原理。
讀過《程序員的自我修養》的朋友,對程序的編譯及執行會有一個很清晰的認識:編譯其實就是將人類能理解的代碼文件轉譯為機器/CPU能執行的文件(包括數據段、代碼段),而執行的過程,則是根據文件頭部字節的標識(簡稱魔數),映射為對應的文件結構體,找到程序入口,當獲取到CPU執行權限時,將方法壓棧,執行對應的指令碼,完成相應的邏輯操作。
而對應.java文件,則先需要使用javac進行編譯,編譯后的.class文件,此文件將java程序能讀懂的數據段和代碼段,之后用java執行文件,既是載入.class文件,找到程序入口,並根據要執行的方法,不停的壓棧、出棧,進行邏輯處理。
class文件載入過程
加載
在加載階段,虛擬機需要完成以下三件事情:
- 通過一個類的全限定名來獲取其定義的二進制字節流。
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
- 在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口。
即相當於在內存中將代碼段和數據段關聯起來,組織好Class對象的內存空間,作為對象成員和方法的引入入口,並將.class及方法載入Perm內存區。相對於類加載的其他階段而言,加載階段(准確地說,是加載階段獲取類的二進制字節流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
連接
分為四個階段:
- 第一階段是驗證,確保被加載的類的正確性。
這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:
文件格式驗證驗證字節流是否符合Class文件格式的規范;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理范圍之內、常量池中的常量是否有不被支持的類型。
元數據驗證對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規范的要求;例如:這個類是否有父類,除了java.lang.Object之外。
字節碼驗證通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
符號引用驗證確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反復驗證,那么可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
- 第二階段是准備:為類的靜態變量分配內存,並將其初始化為默認值
准備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:
1、這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。
2、這里所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。如public static int value = 3;那么變量value在准備階段過后的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value賦值為3的putstatic指令是在程序編譯后,存放於類構造器
()方法之中的,所以把value賦值為3的動作將在 初始化階段才會執行。這里還需要注意如下幾點:
i.對基本數據類型來說,對於類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統會為其賦予默認的零值,而對於 局部變量來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
ii.對於同時被static和final修飾的常量,必須在聲明的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值, 系統不會為final修飾的常量賦予默認零值。
iii.對於 引用數據類型 reference來說,如數組引用、對象引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予默認的零值,即null。
iv.如果在 數組初始化時沒有對數組中的各元素賦值,那么其中的元素將根據對應的數據類型而被賦予默認的零值。
3、如果類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那么在准備階段變量value就會被初始化為ConstValue屬性所指定的值。如public static final int value = 3;編譯時Javac將會為value生成ConstantValue屬性,在准備階段虛擬機就會根據ConstantValue的設置將value賦值為3。
- 第三階段是解析:把類中的符號引用轉換為直接引用
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
這個階段的主要目的將編譯后的虛擬地址(類似動態庫,庫數據段都是0x00開始,載入內存后需要與實際分配的地址關聯起來)與實際運行的地址關聯起來。
- 第四階段是初始化,為類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化,包括給聲明類變量指定初始值,和為類static變量指定靜態代碼塊地址。
1、假如這個類還沒有被加載和連接,則程序先加載並連接該類
2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3、假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化時機:只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:
– 創建類的實例,也就是new的方式
– 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
– 調用類的靜態方法
– 反射(如Class.forName(“com.xxx.Test”))
– 初始化某個類的子類,則其父類也會被初始化
– Java虛擬機啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來運行某個主類。
拓展:雙親委派機制與AOP面向切面編程原理
JVM最底層的載入是BootstrapClassLoader,為C語言編寫,從Java中引用不到,其上是ExtClassLoader,然后是AppClassLoader,普通類中getContextClassLoader()得到的是AppClassLoader,getContextClassLoader().getParent()得到的是ExtClassLoader,而再往上getParent()為null,即引用不到BootstrapLoader。
啟動類加載器:BootstrapClassLoader,負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被BootstrapClassLoader加載)。啟動類加載器是無法被Java程序直接引用的。
擴展類加載器:ExtensionClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載DK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴展類加載器。
應用程序類加載器:ApplicationClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。因為JVM自帶的ClassLoader只是懂得從本地文件系統加載標准的java class文件,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:
1)在執行非置信代碼之前,自動驗證數字簽名。
2)動態地創建符合用戶特定需要的定制化構建類。
3)從特定的場所取得java class,例如數據庫中和網絡中。
雙親委派機制
簡單來說既是先拿父類加載器加載class,父類加載失敗后再使用本類加載器加載。當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。當ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),會使用ExtClassLoader來嘗試加載;若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。
其它相關點
1.全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。
2.父類委托,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時,才使用本類加載器從自己的類路徑中加載該類。
3.緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效。
--
AOP面向切面編程
AOP 專門用於處理系統中分布於各個模塊(不同方法)中的交叉關注點的問題,在 Java EE 應用中,常常通過AOP來處理一些具有橫切性質的系統級服務,如事務管理、安全檢查、緩存、對象池管理等,在不改變已有代碼的情況下,靜態/動態的插入代碼。
將AOP放到這里的主要原因是因為AOP改變的class文件,達到嵌入方法的目的,靜態模式使用AspectJ進行由.java到.class文件編譯。而動態模式時使用CGLIB載入使用javac編譯的.class文件后,使用動態代理的方式,將要執行的方法嵌入到原有class方法中,完成在內存中對class對象的構造,這也就是所謂動態代理技術的內在原理。同時靜態方式在載入前已經修好完.class文件,而動態方式在.class載入時需要做額外的處理,導致性能受到一定影響,但其優勢是無須使用額外的編譯器。總體的技術的切入點在於在修改機器執行碼,達到增加執行方法的目的。參考我。
對象的內存分配和回收
對象創建及方法調用
數據分類:基本類型與引用類型
基本類型包括:byte,short,int,long,char,float,double,boolean,returnAddress
引用類型包括:類類型,接口類型和數組。
數據存儲:使用堆存儲對象信息
方法調用:使用棧來解決方法嵌套調用,而棧內部由一個個棧幀構成,調用一個方法時,在當前棧上壓入一個棧幀,此棧幀包含局部變量表,操作棧等子項,每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。表面上代碼在運行時,是通過程序計數器不斷執行下一條指令;而實際指令運算等操作是通過控制操作棧的操作數入棧和出棧,將操作數在局部變量表和操作棧之間轉移。參見我。
這里對內存的分配做個深入的擴展,解釋下基礎類型的自動裝箱 boxing
以"Object obj = new Object();"為例,一個空Object對象占用8byte空間,而obj引用占用4byte,此條語句執行后共占用12byte;而Java中對象大小是8的整數倍,則Boolean b = new Boolean(true)至少需要20byte(16byte+4byte),而如果直接使用基本數據類型boolean b = true則僅僅需要1byte,在棧幀中存儲;為優化此問題,JVM提出了基本類型的自動裝載技術,來自動化進行基本類型與基本類型對象間的轉換,來降低內存的使用量。
內存集中管理(模型及GC機制)
- JVM內存模型圖如下
內存結構主要有三大塊:堆內存、方法區和棧。
1.堆內存是JVM中最大的一塊由Young Generation(年輕代、新生代)和Old Generation(年老代)組成,而Young Generation內存又被分成三部分,Eden空間、From Survivor空間、To Survivor空間,默認情況下年輕代按照8:1:1的比例來分配。
2.方法區存儲類信息、常量、靜態變量等數據,是線程共享的區域,為與Java堆區分,方法區還有一個別名Non-Heap(非堆)。
3.棧又分為java虛擬機棧(方法執行的內存區,每個方法執行時會在虛擬機棧中創建棧幀)和本地方法棧(虛擬機的Native方法執行的內存區)主要用於方法的執行。
4.內存設置參數:
-Xms設置堆的最小空間大小。
-Xmx設置堆的最大空間大小。
-XX:NewSize設置新生代最小空間大小。
-XX:MaxNewSize設置新生代最大空間大小。
-XX:PermSize設置永久代最小空間大小。
-XX:MaxPermSize設置永久代最大空間大小。
-Xss設置每個線程的堆棧大小。
-XX:SurvivorRatio=x #Eden區與Survivor區的大小比值,默認為8(Eden:From-Survivor=8:1)
-XX:NewRatio=x #年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)默認值為2,即年輕代:年老代=1:2(這里與數學的比值有差異)
- Minor GC
對象分配先從Eden空間與From Survivor空間獲取內存,當兩者中空間不足時,進行Minor GC,將Eden與From Survivor空間存活的對象,Copy到To Survivor空間,然后清空Eden與From Survivor空間,之后將To Survivor空間對象年齡加1,並將To Survivor空間設置為From Survivor空間,保證Minor GC時To Survivor空間始終為空;而當對象年齡為15后(默認是 15,可以通過參數-XX:MaxTenuringThreshold 來設定),將存活對象放入老年代。
例外情況:
1.對於一些較大的對象(即需要分配一塊較大的連續內存空間)則是直接進入到老年代。虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。避免在新生代采用復制算法收集內存時,在Eden區及兩個Survivor區之間發生大量的內存復制。
2.為了更好的適應不同的程序,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入Old Generation,無需等到MaxTenuringThreshold中要求的年齡。
這里的疑問,按照默認的8:1:1設置,一個To Survivor空間占10%空間,每次Minor GC能保證To Survivor空間夠用嗎?IBM的研究表明,98%的對象都是很快消亡的,大部分的對象在創建后很快就不再使用。這里可以根據GC detail來查看和分析比例設置是否合理。
- Full GC
工作:同時回收年輕代、年老代,按照配置的不同算法進行回收。
時機:在Minor GC觸發時,會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩余空間,如果大於,改為直接進行一次Full GC;如果小於則查看HandlePromotionFailure設置(是否允許擔保,使用Old Gerneration空間擔保),如果允許,那仍然進行Minor GC,如果不允許,則也要改為進行一次Full GC。
取平均值進行比較其實仍然是一種動態概率的手段,也就是說如果某次Minor GC存活后的對象突增,大大高於平均值的話,依然會導致擔保失敗,這樣就只好在失敗后重新進行一次Full GC。
回收算法
首先判斷對象是否存活,一般有兩種方式:
- 引用計數:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決對象相互循環引用的問題。
- 可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的、不可達對象。
在Java語言中,GC Roots包括:
--虛擬機棧中引用的對象。
--方法區中類靜態屬性實體引用的對象。
--方法區中常量引用的對象。
--本地方法棧中JNI引用的對象。
標記-清除算法:首先標記出所有需要回收的對象,在標記完成后統一回收掉所有被標記的對象。之所以說它是最基礎的收集算法,是因為后續的收集算法都是基於這種思路並對其缺點進行改進而得到的。
缺點:一個是效率問題,標記和清除過程的效率都不高;另外一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
復制算法:對空間問題的改進,它將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。
缺點是這種算法的代價是將內存縮小為原來的一半,持續復制長生存期的對象則導致效率降低。如果不想浪費50%的空間,就需要有額外的空間進行分配擔保(HandlePromotionFailure設置為true),以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
標記-壓縮/整理算法:對復制算法在老年代上的改進,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
分代收集算法:把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
而老年代中,因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。
回收器
新生代收集器
- Serial收集器
串行收集器是最古老,最穩定以及效率高的收集器,使用停止復制方法,只使用一個線程去串行回收;垃圾收集的過程中會Stop The World(服務暫停);
參數控制:使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內存回收(這也是虛擬機在Client模式下運行的默認值)
缺點是串行效率較低
- ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本,使用停止復制方法。新生代並行,其它工作線程暫停。
參數控制:使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內存;使用-XX:ParallelGCThreads來設置執行內存回收的線程數。
- Parallel收集器
Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關注CPU吞吐量,即運行用戶代碼的時間/總時間,使用停止復制算法。可以通過參數來打開自適應調節策略,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量;也可以通過參數控制GC的時間不大於多少毫秒或者比例。
參數控制:使用-XX:+UseParallelGC開關控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設置用戶執行時間占總時間的比例,默認99,即1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置GC的最大停頓時間(這個參數只對Parallel Scavenge有效),用開關參數-XX:+UseAdaptiveSizePolicy可以進行動態控制,如自動調整Eden/Survivor比例,老年代對象年齡,新生代大小等,這個參數在ParNew下沒有。
老年代收集器
-
Serial Old收集器:老年代收集器,單線程收集器,串行,使用"標記-整理"算法(整理的方法是Sweep(清理)和Compact(壓縮),
-
Parallel Old 收集器
多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復制到預先准備好的區域,而不是像Sweep(清理)那樣清理廢棄的對象)算法,在Parallel Old執行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。這個收集器是在JDK 1.6中,與Parallel Scavenge配合有很好的效果。
參數控制: 使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
- CMS收集器:老年代收集器(新生代使用ParNew)
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用都集中在互聯網站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基於“標記-清除”算法實現的,它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分為6個步驟,其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”,包括:
- 初始標記(CMS-initial-mark):為了收集應用程序的對象引用需要暫停應用程序線程,該階段完成后,應用程序線程再次啟動。
- 並發標記(CMS-concurrent-mark):從第一階段收集到的對象引用開始,遍歷所有其他的對象引用,此階段會打印2條日志:CMS-concurrent-mark-start,CMS-concurrent-mark。
- 並發預清理(CMS-concurrent-preclean):改變當運行第二階段時,由應用程序線程產生的對象引用,以更新第二階段的結果。 此階段會打印2條日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean。
- 下一階段是CMS-concurrent-abortable-preclean階段,加入此階段的目的是使cms gc更加可控一些,作用也是執行一些預清理,以減少Rescan階段造成應用暫停的時間.
通過兩個參數來來控制是否進行下一階段:
-XX:CMSScheduleRemarkEdenSizeThreshold(默認2M):即當eden使用小於此值時;
-XX:CMSScheduleRemarkEdenPenetratio(默認50%):當Eden區占用比例此比例時
在concurrent preclean階段之后,如果Eden占用率高於CMSScheduleRemarkEdenSizeThreshold,開啟'concurrent abortable preclean',並且持續的precleanig直到Eden占比超過CMSScheduleRemarkEdenPenetratio,之后,開啟remark階段,參考1、參考2
另外,-XX:CMSMaxAbortablePrecleanTime:當abortable-preclean階段執行達到這個時間時會結束進入下一階段。 - 重標記CMS-concurrent-remark:由於上面三階段是並發的,對象引用可能會發生進一步改變。因此,應用程序線程會再一次被暫停以更新這些變化,並且在進行實際的清理之前確保一個正確的對象引用視圖。這一階段十分重要,因為必須避免收集到仍被引用的對象。
- 並發清理CMS-concurrent-sweep:所有不再被應用的對象將從堆里清除掉。
- 並發重置CMS-concurrent-reset:收集器做一些收尾的工作,以便下一次GC周期能有一個干凈的狀態。
盡管CMS收集器為老年代垃圾回收提供了幾乎完全並發的解決方案,然而年輕代仍然通過“stop-the-world”方法來進行收集。對於交互式應用,停頓也是可接受的,背后的原理是年輕帶的垃圾回收時間通常是相當短的。
優點:並發收集、低停頓
缺點:產生大量空間碎片、並發階段會降低吞吐量
- 堆碎片:CMS收集器並沒有任何碎片整理的機制,可能出現總的堆大小遠沒有耗盡,但因為沒有足夠連續的空間卻不能分配對象,只能觸發Full GC來解決,造成應用停頓。
- 對象分配率高:如果獲取對象實例的頻率高於收集器清除堆里死對象的頻率,並發算法將再次失敗,從某種程度上說,老年代將沒有足夠的可用空間來容納一個從年輕代提升過來的對象。經常被證實是老年代有大量不必要的對象。一個可行的辦法就是增加年輕代的堆大小,以防止年輕代短生命的對象提前進入老年代。另一個辦法就似乎利用分析器,快照運行系統的堆轉儲,並且分析過度的對象分配,找出這些對象,最終減少這些對象的申請。
參數控制
-XX:+UseConcMarkSweepGC 使用CMS收集器
當使用-XX:+UseConcMarkSweepGC時,-XX:UseParNewGC會自動開啟。因此,如果年輕代的並行GC不想開啟,可以通過設置-XX:-UseParNewGC來關掉
-XX:+CMSClassUnloadingEnabled相對於並行收集器,CMS收集器默認不會對永久代進行垃圾回收。
-XX:+CMSConcurrentMTEnabled當該標志被啟用時,並發的CMS階段將以多線程執行(因此,多個GC線程會與所有的應用程序線程並行工作)。該標志已經默認開啟,如果順序執行更好,這取決於所使用的硬件,多線程執行可以通過-XX:-CMSConcurremntMTEnabled禁用(注意是-號)。
-XX:+UseCMSCompactAtFullCollection Full GC后,進行一次碎片整理;整理過程是獨占的,會引起停頓時間變長
-XX:+CMSFullGCsBeforeCompaction 設置進行幾次Full GC后,進行一次碎片整理
-XX:ParallelCMSThreads 設定CMS的線程數量(一般情況約等於可用CPU數量)
-XX:CMSMaxAbortablePrecleanTime:當abortable-preclean階段執行達到這個時間時才會結束
-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly來決定什么時間開始垃圾收集;如果設置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有當old代占用確實達到了-XX:CMSInitiatingOccupancyFraction參數所設定的比例時才會觸發cms gc;如果沒有設置-XX:+UseCMSInitiatingOccupancyOnly,那么系統會根據統計數據自行決定什么時候觸發cms gc;因此有時會遇到設置了80%比例才cms gc,但是50%時就已經觸發了,就是因為這個參數沒有設置的原因.
G1收集器
G1 GC是Jdk7的新特性之一、Jdk7+版本都可以自主配置G1作為JVM GC選項;作為JVM GC算法的一次重大升級、DK7u后G1已相對穩定、且未來計划替代CMS。
不同於其他的分代回收算法、G1將堆空間划分成了互相獨立的區塊。每塊區域既有可能屬於O區、也有可能是Y區,且每類區域空間可以是不連續的(對比CMS的O區和Y區都必須是連續的)。區別如下:
- G1在壓縮空間方面有優勢
- G1通過將內存空間分成區域(Region)的方式避免內存碎片問題
- Eden, Survivor, Old區不再固定、在內存使用效率上來說更靈活
- G1可以通過設置預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象
- G1在回收內存后會馬上同時做合並空閑內存的工作、而CMS默認是在STW(stop the world)的時候做
- G1會在Young GC中使用、而CMS只能在O區使用
就目前而言、CMS還是默認首選的GC策略、可能在以下場景下G1更適合:
- 大量內存的系統提供一個保證GC低延遲的解決方案,也就是說堆內存在6GB及以上,穩定和可預測的暫停時間小於0.5秒。
- Full GC 次數太頻繁或者消耗時間太長
- 應用在運行過程中會產生大量內存碎片、需要經常壓縮空間
- 想要更可控、可預期的GC停頓周期;防止高並發下應用雪崩現象
- 對象分配的頻率或代數提升(promotion)顯著變化
- 受夠了太長的垃圾回收或內存整理時間(超過0.5~1秒)
注意: 如果正在使用CMS或ParallelOldGC,而應用程序的垃圾收集停頓時間並不長,那么建議繼續使用現在的垃圾收集器。使用最新的JDK時,並不要求切換到G1收集器。
- 內存模型:
G1堆由多個區(region)組成,每個區大小1M~32M,邏輯上區有3種類型,包括(Eden、Survivor、Old),按分代划分包括:年輕代(Young Generation)和老年代(Old Generation)。
如果從 ParallelOldGC 或者 CMS收集器遷移到 G1,可能會看到JVM進程占用更多的內存(a larger JVM process size)。 這在很大程度上與 “accounting” 數據結構有關,如 Remembered Sets 和 Collection Sets。
Remembered Sets 簡稱 RSets。跟蹤指向某個heap區內的對象引用。 堆內存中的每個區都有一個 RSet。 RSet 使heap區能並行獨立地進行垃圾集合。 RSets的總體影響小於5%。
Collection Sets 簡稱 CSets。收集集合, 在一次GC中將執行垃圾回收的heap區。GC時在CSet中的所有存活數據(live data)都會被轉移(復制/移動)。集合中的heap區可以是 Eden, survivor, 和/或 old generation。CSets所占用的JVM內存小於1%。
- Young GC
Young GC是stop-the-world活動,會導致整個應用線程的停止。其過程如下:
- 新對象進入Eden區
- 存活對象拷貝到Survivor區;
- 存活時間達到年齡閾值時,對象晉升到Old區。
young gc是多線程並行執行的。
- Old GC階段描述為:
- 初始化標記(stop_the_world事件):這是一個stop_the_world的過程,是隨着年輕代GC做的,標記survivor區域(根區域),這些區域可能含有對老年代對象的引用。
- 根區域掃描:掃描survivor區域中對老年代的引用,這個過程和應用程序一起執行的,這個階段必須在年輕代GC發生之前完成。
- 並發標記:查找整個堆中存活的對象,這也是和應用程序一起執行的。這個階段可以被年輕代的垃圾收集打斷。
- 重新標記(stop-the-world事件):完成堆內存活對象的標記。使用了一個叫開始前快照snapshot-at-the-beginning (SATB)的算法,這個會比CMS collector使用的算法快。
- 清理(stop-the-world事件,並且是並發的):對存活的對象和完全空的區域進行統計(stop-the-world)、刷新Remembered Sets(stop-the-world)、重置空的區域,把他們放到free列表(並發)(譯者注:大體意思就是統計下哪些區域又空了,可以拿去重新分配了)
- 復制(stop-the-world事件):這個stop-the-world的階段是來移動和復制存活對象到一個未被使用的區域,這個可以是年輕代區域,打日志的話就標記為 [GC pause (young)]。或者老年代和年輕代都用到了,打日志就會標記為[GC Pause (mixed)]。
- 參數分析
-XX:+UseG1GC使用G1 GC
-XX:MaxGCPauseMillis=n設置一個暫停時間期望目標,這是一個軟目標,JVM會近可能的保證這個目標
-XX:InitiatingHeapOccupancyPercent=n內存占用達到整個堆百分之多少的時候開啟一個GC周期,G1 GC會根據整個棧的占用,而不是某個代的占用情況去觸發一個並發GC周期,0表示一直在GC,默認值是45
-XX:NewRatio=n年輕代和老年代大小的比例,默認是2
-XX:SurvivorRatio=n eden和survivor區域空間大小的比例,默認是8
-XX:MaxTenuringThreshold=n晉升的閾值,默認是15(譯者注:一個存活對象經歷多少次GC周期之后晉升到老年代)
-XX:ParallelGCThreads=n GC在並行處理階段試驗多少個線程,默認值和平台有關。(譯者注:和程序一起跑的時候,使用多少個線程)
-XX:ConcGCThreads=n並發收集的時候使用多少個線程,默認值和平台有關。(譯者注:stop-the-world的時候,並發處理的時候使用多少個線程)
-XX:G1ReservePercent=n預留多少內存,防止晉升失敗的情況,默認值是10
-XX:G1HeapRegionSize=n G1 GC的堆內存會分割成均勻大小的區域,這個值設置每個划分區域的大小,這個值的默認值是根據堆的大小決定的。最小值是1Mb,最大值是32Mb
轉載請注明出處,百度搜“成金之路”。
調優
命令行工具(以Linux場景進行詳解)
- GC監控:
./jstat -gc pid 3s對pid GC每隔3s進行監控
- 誰動了我的CPU
- top查看CPU使用情況,或通過CPU使用率收集,找到CPU占用率高Java進程,假設其進程編號為pid;
- 使用top -Hp pid(或ps -Lfp pid或者ps -mp pid -o THREAD,tid,time)查看pid進程中占用CPU較高的線程,假設其編號為tid;
- 使用Linux命令,將tid轉換為16進制數,printf '%0x\n' tid,假設得到值txid;
- 使用jstack pid | grep txid查看線程CPU占用代碼,然后根據得到的對象信息,去追蹤代碼,定位問題。
- 死鎖
另外,jstack -l long listings,會打印出額外的鎖信息,在發生死鎖時可以用jstack -l pid來觀察鎖持有情況。
- Memory
- 查看java 堆(heap)使用情況jmap -J-d64 -heap pid,其中-J-d64為64位機器標識,啟動
- 查看堆內存(histogram)中的對象數量及大小jmap -J-d64 -histo pid;而查看存活對象-histo:live,這個命令執行,JVM會先觸發gc,然后再統計信息。
如果有大量對象在持續被引用,並沒有被釋放掉,那就產生了內存泄露,就要結合代碼,把不用的對象釋放掉。
其中class name中B 代表 byte,C 代表 char,D 代表 double,F 代表 float,I 代表 int,J 代表 long,Z 代表 boolean,前邊有 [ 代表數組, [I 就相當於 int[],對象用 [L+ 類名表示 - 程序內存不足或者頻繁GC,很可能存在內存泄露將內存使用的詳細情況輸出到文件,執行命令:jmap -J-d64 -dump:format=b,file=heapDump pid;然后用jhat命令可以參看 jhat -port 5000 heapDump 在瀏覽器中訪問:http://localhost:5000/ 查看詳細信息
--
內存泄漏OOM,通常做法:
方法1. 首先配置JVM啟動參數,讓JVM在遇到OutOfMemoryError時自動生成Dump文件-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path
方法2. 使用上面第3步進行進行分析;
方法3. 使用eclipse的MAT分析工具對dump文件進行分析
- 場景,參考
- 發現問題
- 使用uptime命令查看CPU的Load情況,Load越高說明問題越嚴重;
- 使用jstat查看FGC發生的頻率及FGC所花費的時間,FGC發生的頻率越快、花費的時間越高,問題越嚴重;
- 使用jmap -head 觀察老年代大小,當快達到配置的閥值CMSInitiatingOccupancyFraction時,將對象導出進行分析;
- 對head中對象進行命令行排序分析:./jmap -J-d64 -histo pid | grep java | sort -k 3 -n -r | more按照bytes大小進行排序;數量最多排序將-k 3換成-k 2即可
圖形化工具jvisualvm.exe
一般結合JMX分析,分析遠程tomcat狀態
- tomcat配置
修改遠程tomcat的catalina.sh配置文件,在其中增加(不走權限校驗。只是打開jmx端口):
JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=192.168.122.128 -Dcom.sun.management.jmxremote.port=18999 -Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false"
--
如果連接的是公網上的Tomcat,那么就要注意安全性了,接下來看看使用用戶名和密碼連接
JAVA_OPTS='-Xms128m -Xmx256m -XX:MaxPermSize=128m -Djava.rmi.server.hostname=10.10.23.10
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.password.file=/path/to/passwd/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=/path/to/passwd/jmxremote.access'
以下分別編輯jmxremote.password與jmxremote.access兩個文件
jmxremote.password
monitorRole 123456
controlRole 123456789
jmxremote.access
monitorRole readonly
controlRole readwrite
完成這兩個文件后修改jmxremote.password的權限chmod 600 jmxremote.password
- 工具使用
配置后重啟tomcat,然后從本機上使用jvisualvm.exe,按照配置輸入遠程地址、用戶名密碼,即可進行監控,包括CPU、Head、Thread、Objects等分析
優化策略使用
本質上是減少Full GC的次數(Minor GC很快基本不會有影響)
- 第一個問題既是目前年輕代、年老代分別使用什么收集器;
- 針對不同的收集器,進行優化:串行改並行、針對目前的Head對象情況,根據實際場景設置SurvivorRatio與NewRatio;
- 根據對象回收情況,判斷是否需要壓縮,減小碎片化。
如果是頻繁創建對象的應用,可以適當增加新生代大小。常量較多可以增加持久代大小。對於單例較多的對象可以增加老生代大小。比如spring應用中。
GC選擇,在JDK5.0以后,JVM會根據當前系統配置進行判斷。一般執行-Server命令便可以。gc包括三種策略:串行,並行,並發(使用CMS收集器老年代)。
吞吐量大的應用,一般采用並行收集,開啟多個線程,加快gc的速率。
響應速度高的應用,一般采用並發收集,比如應用服務器。
年老代建議配置為並發收集器,由於並發收集器不會壓縮和整理磁盤碎片,因此建議配置:
-XX:+UseConcMarkSweepGC #並發收集年老代
-XX:CMSInitiatingOccupancyFraction=80 # 表示年老代空間到80%時就開始執行CMS
-XX:+UseCMSInitiatingOccupancyOnly #是閥值生效
-XX:+UseCMSCompactAtFullCollection#打開對年老代的壓縮。可能會影響性能,但是可以消除內存碎片。
-XX:CMSFullGCsBeforeCompaction=10 # 由於並發收集器不對內存空間進行壓縮、整理,所以運行一段時間以后會產生“碎片”,使得運行效率降低。此參數設置運行次FullGC以后對內存空間進行壓縮、整理。
如果覺得不錯,還請不吝推薦,您的認可是我分享的動力,同時也希望幫助更多的人走上自己“成金之路”。謝謝!轉載請注明出處,百度搜“成金之路”。