JVM架構和GC垃圾回收機制詳解
JVM架構圖分析
下圖:參考網絡+書籍,如有侵權請見諒 (想了解Hadoop記憶體溢出請看: Hadoop記憶體溢出(OOM)分類、參數調優化)
JVM被分為三個主要的子系統
(1)類加載器子系統(2)運作時資料區(3)執行引擎
1. 類加載器子系統
Java的動态類加載功能是由類加載器子系統處理。當它在運作時(不是編譯時)首次引用一個類時,它加載、連結并初始化該類檔案。
1.1 加載
類由此元件加載。啟動類加載器 (BootStrap class Loader)、擴充類加載器(Extension class Loader)和應用程式類加載器(Application class Loader) 這三種類加載器幫助完成類的加載。
1. 啟動類加載器 – 負責從啟動類路徑中加載類,無非就是rt.jar。這個加載器會被賦予最高優先級。
2. 擴充類加載器 – 負責加載ext 目錄(jre\lib)内的類.
3. 應用程式類加載器 – 負責加載應用程式級别類路徑,涉及到路徑的環境變量等etc.
上述的類加載器會遵循委托層次算法(Delegation Hierarchy Algorithm)加載類檔案。
1.2 連結
1. 校驗 – 位元組碼校驗器會校驗生成的位元組碼是否正确,如果校驗失敗,我們會得到校驗錯誤。
2. 準備 – 配置設定記憶體并初始化預設值給所有的靜态變量。
3. 解析 – 所有符号記憶體引用被方法區(Method Area)的原始引用所替代。
1.3 初始化
這是類加載的最後階段,這裡所有的靜态變量會被賦初始值, 并且靜态塊将被執行。
2. 運作時資料區(Runtime Data Area)
The 運作時資料區域被劃分為5個主要元件:
2.1 方法區(Method Area)
所有類級别資料将被存儲在這裡,包括靜态變量。每個JVM隻有一個方法區,它是一個共享的資源。
2.2 堆區(Heap Area)
所有的對象和它們相應的執行個體變量以及數組将被存儲在這裡。每個JVM同樣隻有一個堆區。由于方法區和堆區的記憶體由多個線程共享,是以存儲的資料不是線程安全的。
2.3 棧區(Stack Area)
對每個線程會單獨建立一個運作時棧。對每個函數呼叫會在棧記憶體生成一個棧幀(Stack Frame)。所有的局部變量将在棧記憶體中建立。棧區是線程安全的,因為它不是一個共享資源。棧幀被分為三個子實體:
a 局部變量數組 – 包含多少個與方法相關的局部變量并且相應的值将被存儲在這裡。
b 操作數棧 – 如果需要執行任何中間操作,操作數棧作為運作時工作區去執行指令。
c 幀資料 – 方法的所有符号都儲存在這裡。在任意異常的情況下,catch塊的資訊将會被儲存在幀資料裡面。
如上是JVM三大核心區域
2.4 PC寄存器
每個線程都有一個單獨的PC寄存器來儲存目前執行指令的位址,一旦該指令被執行,pc寄存器會被更新至下條指令的位址。
2.5 本地方法棧
本地方法棧儲存本地方法資訊。對每一個線程,将建立一個單獨的本地方法棧。
3. 執行引擎
配置設定給運作時資料區的位元組碼将由執行引擎執行。執行引擎讀取位元組碼并逐段執行。
3.1 解釋器:
解釋器能快速的解釋位元組碼,但執行卻很慢。 解釋器的缺點就是,當一個方法被調用多次,每次都需要重新解釋。
編譯器
JIT編譯器消除了解釋器的缺點。執行引擎利用解釋器轉換位元組碼,但如果是重複的代碼則使用JIT編譯器将全部位元組碼編譯成本機代碼。本機代碼将直接用于重複的方法調用,這提高了系統的性能。
a. 中間代碼生成器 – 生成中間代碼
b. 代碼優化器 – 負責優化上面生成的中間代碼
c. 目标代碼生成器 – 負責生成機器代碼或本機代碼
d. 探測器(Profiler) – 一個特殊的元件,負責尋找被多次調用的方法。
3.3 垃圾回收器:
收集并删除未引用的對象。可以通過調用"System.gc()"來觸發垃圾回收,但并不保證會确實進行垃圾回收。JVM的垃圾回收隻收集哪些由new關鍵字建立的對象。是以,如果不是用new建立的對象,你可以使用finalize函數來執行清理。
Java本地接口 (JNI): JNI 會與本地方法庫進行互動并提供執行引擎所需的本地庫。
本地方法庫:它是一個執行引擎所需的本地庫的集合。
通過一個小程式認識JVM
- package com.spark.jvm;
- /**
- * 從JVM調用的角度分析java程式堆記憶體空間的使用:
- * 當JVM程序啟動的時候,會從類加載路徑中找到包含main方法的入口類HelloJVM
- * 找到HelloJVM會直接讀取該檔案中的二進制資料,并且把該類的資訊放到運作時的Method記憶體區域中。
- * 然後會定位到HelloJVM中的main方法的位元組碼中,并開始執行Main方法中的指令
- * 此時會建立Student執行個體對象,并且使用student來引用該對象(或者說給該對象命名),其内幕如下:
- * 第一步:JVM會直接到Method區域中去查找Student類的資訊,此時發現沒有Student類,就通過類加載器加載該Student類檔案;
- * 第二步:在JVM的Method區域中加載并找到了Student類之後會在Heap區域中為Student執行個體對象配置設定記憶體,
- * 并且在Student的執行個體對象中持有指向方法區域中的Student類的引用(記憶體位址);
- * 第三步:JVM執行個體化完成後會在目前線程中為Stack中的reference建立實際的應用關系,此時會指派給student
- * 接下來就是調用方法
- * 在JVM中方法的調用一定是屬于線程的行為,也就是說方法調用本身會發生線上程的方法調用棧:
- * 線程的方法調用棧(Method Stack Frames),每一個方法的調用就是方法調用棧中的一個Frame,
- * 該Frame包含了方法的參數,局部變量,臨時資料等 student.sayHello();
- */
- public class HelloJVM {
- //在JVM運作的時候會通過反射的方式到Method區域找到入口方法main
- public static void main(String[] args) { //main方法也是放在Method方法區域中的
- /**
- * student(小寫的)是放在主線程中的Stack區域中的
- * Student對象執行個體是放在所有線程共享的Heap區域中的
- */
- Student student = new Student( "spark");
- /**
- * 首先會通過student指針(或句柄)(指針就直接指向堆中的對象,句柄表明有一個中間的,student指向句柄,句柄指向對象)
- * 找Student對象,當找到該對象後會通過對象内部指向方法區域中的指針來調用具體的方法去執行任務
- */
- student.sayHello();
- }
- }
- class Student {
- // name本身作為成員是放在stack區域的但是name指向的String對象是放在Heap中
- private String name;
- public Student(String name) {
- this.name = name;
- }
- //sayHello這個方法是放在方法區中的
- public void sayHello() {
- System. out.println( "Hello, this is " + this.name);
- }
- }
JVM三大性能調優參數:-Xms –Xmx –Xss
-Xms –Xmx是對堆的性能調優參數,一般兩個設定是一樣的,如果不一樣,當Heap不夠用,會發生記憶體抖動。一般都調大這兩個參數,并且兩個大小一樣。
-Xss是對每一個線程棧的性能調優參數,影響堆棧調用的深度
實戰示範從OOM推導出JVM GC時候基于的記憶體結構:Young Generation(Eden、From、To)、OldGeneration、Permanent Generation
JVMHeap區域(年輕代、老年代)和方法區(永久代)結構圖:
從Java GC的角度解讀代碼:程式20行new的Person對象會首先會進入年輕代的Eden中(如果對象太大可能直接進入年老代)。在GC之前對象是存在Eden和from中的,進行GC的時候Eden中的對象被拷貝到To這樣一個survive空間(survive(幸存)空間:包括from和to,他們的空間大小是一樣的,又叫s1和s2)中(有一個拷貝算法),From中的對象(算法會考慮經過GC幸存的次數)到一定次數(門檻值(如果說每次GC之後這個對象依舊在Survive中存在,GC一次他的Age就會加1,預設15就會放到OldGeneration。但是實際情況比較複雜,有可能沒有到門檻值就從Survive區域直接到Old Generation區域。在進行GC的時候會對Survive中的對象進行判斷,Survive空間中有一些對象Age是一樣的,也就是經過的GC次數一樣,年齡相同的這樣一批對象的總和大于等于Survive空間一半的話,這組對象就會進入old Generation中,(是一種動态的調整))),會被複制到OldGeneration,如果沒到次數From中的對象會被複制到To中,複制完成後To中儲存的是有效的對象,Eden和From中剩下的都是無效的對象,這個時候就把Eden和From中所有的對象清空。在複制的時候Eden中的對象進入To中,To可能已經滿了,這個時候Eden中的對象就會被直接複制到Old Generation中,From中的對象也會直接進入Old Generation中。就是存在這樣一種情況,To比較小,第一次複制的時候空間就滿了,直接進入old Generation中。複制完成後,To和From的名字會對調一下,因為Eden和From都是空的,對調後Eden和To都是空的,下次配置設定就會配置設定到Eden。一直循環這個流程。好處:使用對象最多和效率最高的就是在Young Generation中,通過From to就避免過于頻繁的産生FullGC(Old Generation滿了一般都會産生FullGC)
虛拟機在進行MinorGC(新生代的GC)的時候,會判斷要進入OldGeneration區域對象的大小,是否大于Old Generation剩餘空間大小,如果大于就會發生Full GC。
剛配置設定對象在Eden中,如果空間不足嘗試進行GC,回收空間,如果進行了MinorGC空間依舊不夠就放入Old Generation,如果OldGeneration空間還不夠就OOM了。
比較大的對象,數組等,大于某值(可配置)就直接配置設定到老年代,(避免頻繁記憶體拷貝)
年輕代和年老代屬于Heap空間的
Permanent Generation(永久代)可以了解成方法區,(它屬于方法區)也有可能發生GC,例如類的執行個體對象全部被GC了,同時它的類加載器也被GC掉了,這個時候就會觸發永久代中對象的GC。
如果OldGeneration滿了就會産生FullGC
滿原因:1,from survive中對象的生命周期到一定門檻值
2,配置設定的對象直接是大對象
3、由于To 空間不夠,進行GC直接把對象拷貝到年老代(年老代GC時候采用不同的算法)
如果Young Generation大小配置設定不合理或空間比較小,這個時候導緻對象很容易進入Old Generation中,而Old Generation中回收具體對象的時候速度是遠遠低于Young Generation回收速度。
是以實際配置設定要考慮年老代和新生代的比例,考慮Eden和survives的比例
Permanent Generation中發生GC的時候也對性能影響非常大,也是Full GC
JVM GC時候核心參數:
-XX:NewRatio –XX:SurvivorRatio –XX:NewSize –XX:MaxNewSize
–XX:NewSize–XX:MaxNewSize指定新生代初始大小和最大大小。
1,-XX:NewRatio 是年老代 新生代相對的比例,比如NewRatio=2,表明年老代是新生代的2倍。老年代占了heap的2/3,新生代占了1/3
2,-XX:SurvivorRatio 配置的是在新生代裡面Eden和一個Servive的比例
如果指定NewRatio還可以指定NewSizeMaxNewSize,如果同時指定了會如何???
NewRatio=2,這個時候新生代會嘗試配置設定整個Heap大小的1/3的大小,但是配置設定的空間不會小于-XX:NewSize也不會大于 –XX:MaxNewSize
3,-XX:NewSize –XX:MaxNewSize
實際設定比例還是設定固定大小,固定大小理論上速度更高。
-XX:NewSize –XX:MaxNewSize理論越大越好,但是整個Heap大小是有限的,一般年輕代的設定大小不要超過年老代。
-XX:SurvivorRatio新生代裡面Eden和一個Servive的比例,如果SurvivorRatio是5的話,也就是Eden區域是SurviveTo區域的5倍。Survive由From和To構成。結果就是整個Eden占用了新生代5/7,From和To分别占用了1/7,如果配置設定不合理,Eden太大,這樣産生對象很順利,但是進行GC有一部分對象幸存下來,拷貝到To,空間小,就沒有足夠的空間,對象會被放在old Generation中。如果Survive空間大,會有足夠的空間容納GC後存活的對象,但是Eden區域小,會被很快消耗完,這就增加了GC的次數。
JVM的GC日志解讀:
一、 JVM YoungGeneration下MinorGC日志詳解
[GC (Allocation Failure) [PSYoungGen:2336K->288K(2560K)] 8274K->6418K(9728K), 0.0112926 secs] [Times:user=0.06 sys=0.00, real=0.01 secs]
PSYoungGen(是新生代類型,新生代日志收集器),2336K表示使用新生代GC前,占用的記憶體,->288K表示GC後占用的記憶體,(2560K)代表整個新生代總共大小
8274K(GC前整個JVM Heap對記憶體的占用)->6418K(MinorGC後記憶體占用總量)(9728K)(整個堆的大小)0.0112926 secs(Minor GC消耗的時間)] [Times: user=0.06 sys=0.00, real=0.01 secs] 使用者空間,核心空間時間的消耗,real整個的消耗
二、 JVM的GC日志Full GC日志每個字段徹底詳解
[Full GC (Ergonomics) [PSYoungGen: 984K->425K(2048K)] [ParOldGen:7129K->7129K(7168K)] 8114K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1022588 secs] [Times: user=0.56 sys=0.02,real=0.10 secs]
[Full GC (Allocation Failure) [PSYoungGen: 425K->425K(2048K)][ParOldGen: 7129K->7129K(7168K)] 7555K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1003696 secs] [Times: user=0.64 sys=0.03,real=0.10 secs]
[Full GC(表明是Full GC) (Ergonomics) [PSYoungGen:FullGC會導緻新生代Minor GC産生]984K->425K(2048K)][ParOldGen:(老年代GC)7129K(GC前多大)->7129K(GC後,并沒有降低記憶體占用,因為寫的程式不斷循環一直有引用)(7168K) (老年代總容量)] 8114K(GC前占用整個Heap空間大小)->7555K (GC後占用整個Heap空間大小) (9216K) (整個Heap大小,JVM堆的大小), [Metaspace: (java6 7是permanentspace,java8改成Metaspace,類相關的一些資訊) 2613K->2613K(1056768K) (GC前後基本沒變,空間很大)], 0.1022588 secs(GC的耗時,秒為機關)] [Times: user=0.56 sys=0.02, real=0.10 secs](使用者空間耗時,核心空間耗時,真正的耗時時間)
三、 Java8中的JVM的MetaSpace
Metaspace的使用C語言實作的,使用的是OS的空間,Native Memory Space可動态的伸縮,可以根據類加載的資訊的情況,在進行GC的時候進行調整自身的大小,來延緩下一次GC的到來。
可以設定Metaspace的大小,如果超過最大大小就會OOM,不設定如果把整個作業系統的記憶體耗盡了出現OOM,一般會設定一個足夠大的初始值,安全其間會設定最大值。
永久代發生GC有兩種情況,類的所有的執行個體被GC掉,且class load不存。
對于中繼資料空間 簡化了GC, class load不存在了就需要進行GC。
三種基本的GC算法基石
一、 标記/清除算法
記憶體中的對象構成一棵樹,當有效的記憶體被耗盡的時候,程式就會停止,做兩件事,第一:标記,标記從樹根可達的對象(途中水紅色),第二:清除(清楚不可達的對象)。标記清除的時候有停止程式運作,如果不停止,此時如果存在新産生的對象,這個對象是樹根可達的,但是沒有被标記(标記已經完成了),會清除掉。
缺點:遞歸效率低性能低;釋放空間不連續容易導緻記憶體碎片;會停止整個程式運作;
二、 複制算法
把記憶體分成兩塊區域:空閑區域和活動區域,第一還是标記(标記誰是可達的對象),标記之後把可達的對象複制到空閑區,将空閑區變成活動區,同時把以前活動區對象1,4清除掉,變成空閑區。
速度快但耗費空間,假定活動區域全部是活動對象,這個時候進行交換的時候就相當于多占用了一倍空間,但是沒啥用。
三、 标記整理算法
平衡點
标記誰是活躍對象,整理,會把記憶體對象整理成一課樹一個連續的空間,
JVM垃圾回收分代收集算法
綜合了上述算法優略
1, 分代GC在新生代的算法:采用了GC的複制算法,速度快,因為新生代一般是新對象,都是瞬态的用了可能很快被釋放的對象。
2, 分代GC在年老代的算法 标記/整理算法,GC後會執行壓縮,整理到一個連續的空間,這樣就維護着下一次配置設定對象的指針,下一次對象配置設定就可以采用碰撞指針技術,将新對象配置設定在第一個空閑的區域。
JVM垃圾回收器串行、并行、并發垃圾回收器概述
1, JVM中不同的垃圾回收器
2, 串行,并行,并發垃圾回收器(和JVM曆史有關系,剛開始串行)
Java中Stop-The-World機制簡稱STW,是在執行垃圾收集算法時,Java應用程式的其他所有線程都被挂起(除了垃圾收集幫助器之外)。Java中一種全局暫停現象,全局停頓,所有Java代碼停止,native代碼可以執行,但不能與JVM互動;這些現象多半是由于gc引起。
JVM中Serial收集器、ParNew收集器、Parallel收集器解析
Serial收集器 單線程方式(沒有線程切換開銷,如果受限實體機器單線程可采用)串行且采用stop the world在工作的時候程式會停止
Serial和serial old
ParNew收集器:多線程(多CPU和多Core的環境中高效),生産環境對低延時要求高的話,就采用ParNew和CMS組合來進行server端的垃圾回收
Parallel 收集器:多線程,并行, 它可以控制JVM吞吐量的大小,吞吐量優先的收集器,一般設定1%,可設定程式暫停的時間,會通過把新生代空間變小,來完成回收,頻繁的小規模垃圾回收,會影響程式吞吐量大小
JVM中CMS收集器解密
低延遲進行垃圾回收,線上服務和處理速度要求高的情況下很重要
配置:XX:UseConcMarkSweepGC
concurrence(并發) Mark(标記)Sweep(清理)
低延時
把垃圾回收分成四個階段
CMS-initial-mark初始标記階段會stop the world,短暫的暫停程式根據跟對象标記的對象所連接配接的對象是否可達來标記出哪些可到達
CMS-concurrent-mark并發标記,根據上一次标記的結果确定哪些不可到達,線程并發或交替之行,基本不會出現程式暫停。
CMS-remark再次标記,會出現程式暫停,所有記憶體那一時刻靜止,確定被全部标記,有可能第二階段之前有可能被标記為垃圾的對象有可能被引用,在此标記确認。
CMS-concurrent-sweep并發清理垃圾,把标記的垃圾清理掉了,沒有壓縮,有可能産生記憶體碎片,不連續的記憶體塊,這時候就不能更好的使用記憶體,可以通過一個參數配置,根據記憶體的情況執行壓縮。
JVM中G1收集器
可以像CMS收集器一樣,GC操作與應用的現場一起并發執行
緊湊的空閑記憶體區域且沒有很長的GC停頓時間
需要可預測的GC暫停耗時
不想犧牲太多吞吐量性能
啟動後不需要請求更大的Java堆
通過案例瞬間了解JVM中PSYoungGen、ParOldGen、MetaSpace
- Heap
- PSYoungGen total K, used K[ 00000, , )
- eden space K, % used[ 00000, 50568, 00000)
- from space K, % used[ 00000, 00000, 80000)
- to space K, % used[ 80000, 80000, )
- ParOldGen total K, used K[ 600000, 00000, 00000)
- object space K, %used [ 600000, cee7b8, 00000)
- Metaspace used K, capacity K, committed4864K, reserved K
- class space used K, capacity K, committed K,reserved K
PSYoungGen是eden + from
使用MAT對Dump檔案進行分析實戰
導出Dump檔案