每個java開發同學不管是日常工作中還是面試裡,都會遇到JDK、JVM和GC的問題。本文會從以下10個問題為切入點,帶着大家一起全面了解一下JVM的方方面面。
- JVM、JRE和JDK的差別和聯系
- JVM是什麼?以及它的主要作用
- JVM的核心功能有哪些
- 類加載機制和過程
- 運作時資料區的邏輯結構
- JVM的記憶體模型
- 如何确定對象是垃圾
- 垃圾收集的算法有哪些
- 各種問世的垃圾收集器
- JVM調優的參數配置
上一篇文章結尾時我們談到,就JVM的設計規範,從使用用途角度JVM的記憶體大體的分為:線程私有記憶體區 和 線程共享記憶體區。
線程私有記憶體區在類加載器編譯某個class檔案時就确定了執行時需要的“程式計數器”和“虛拟棧幀”等所需的空間,并且會伴随着目前執行線程的産生而産生,執行線程的消亡而消亡,是以“線程私有記憶體區”并不需要考慮記憶體管理和垃圾回收的問題。線程共享記憶體區在虛拟機啟動時建立,被所有線程共享,是Java虛拟機所管理記憶體中最應該關注的和最大的一塊。首先我們來一起看一下“線程共享記憶體區”的記憶體模型是什麼樣的?
6、JVM的記憶體模型
如圖所示,JVM的記憶體結構分為堆和非堆兩大塊區域。
- 其中“非堆”就是上篇文章我們提到的方法區或叫中繼資料區,用來存儲class類資訊的。
- 而“堆”是用來存儲JVM各線程執行期間所建立的執行個體對象或數組的。堆區分為兩大塊,一個是Old區,一個是Young區。Young區分為兩大塊,一個是Survivor區(S0+S1),一塊是Eden區S0和S1一樣大,也可以叫From和To。
之是以這樣劃分,設計者的目的無非就是為了記憶體管理,也就是我們說的垃圾回收。那麼什麼樣的對象是垃圾?垃圾回收算法有哪些?目前常用的垃圾回收器又有哪些?這篇文章我們一起弄清楚這些問題和知識點。
7、如何确定一個對象是垃圾?
要想進行垃圾回收,得先知道什麼樣的對象是垃圾。目前确認對象是否為垃圾的算法主要有兩種:引用計數法和可達性分析法。
- 1、引用計數法:在對象中添加了一個引用計數器,當有地方引用這個對象時,引用計數器的值就加1,當引用失效的時候,引用計數器的值就減1。當引用計數器的值為0時,JVM就開始回收這個對象。
對于某個對象而言,隻要應用程式中持有該對象的引用,就說明該對象不是垃圾,如果一個對象沒有任何指針對其引用,它就是垃圾。這種方法雖然很簡單、高效,但是JVM一般不會選擇這個方法,因為這個方法會出現一個弊端:當對象之間互相指向時,兩個對象的引用計數器的值都會加1,而由于兩個對象時互相指向,是以引用不會失效,這樣JVM就無法回收。
- 2、可達性分析法:針對引用計數算法的弊端,JVM采用了另一種算法,以一些"GC Roots"的對象作為起始點向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的,即可以進行垃圾回收。否則,證明這個對象有用,不是垃圾。
上圖中的obj7和obj8雖然它們互相引用,但從GC Roots出發這兩個對象不可達,是以會被标記為垃圾。JVM會把以下幾類對象作為GC Roots:
- (1) 虛拟機棧(棧幀中本地變量表)中引用的對象;
- (2) 方法區中類靜态屬性引用的對象;
- (3) 方法區中常量引用的對象;
- (4) 本地方法棧中JNI(Native方法)引用的對象。
注:在可達性分析算法中不可達的對象,并不是直接被回收,這時它們處于緩刑狀态,至少需要進行兩次标記才會确定該對象是否被回收:
第一次标記:如果對象在進行可達性分析後發現沒有與GC Roots相連接配接的引用鍊,那它将會被第一次标記;
第二次标記:第一次标記後接着會進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法(該方法可将此對象與GC Roots建立聯系)。在finalize()方法中沒有重新與引用鍊建立關聯關系的,将被進行第二次标記。
第二次标記成功的對象将真的會被回收,如果對象在finalize()方法中重新與引用鍊建立了關聯關系,那麼将會逃離本次回收,繼續存活。
8、垃圾收集的算法有哪些
知道了如何JVM确定哪些對象是垃圾後,下面我們來看一下,面對這些垃圾對象,JVM的回收算法都有哪些。
1、 标記-清除算法(Mark-Sweep)
- 第一步“标記”,如下圖所示把堆裡所有的對象都掃描一遍,找出哪些是垃圾需要回收的對象,并且把它們标記出來。
- 第二步“清除”,把第一步标記為“UnReference Object”(無引用或不可達)的對象清除掉,釋放記憶體空間。
這種算法的缺點主要有兩點:
(1) 标記和清除兩個過程都比較耗時,效率不高
(2) 清除後會産生大量不連續的記憶體碎片空間,碎片空間太多可能會導緻當程式後續需要建立較大對象時,無法找到足夠連續的記憶體空間而不得不再次觸發垃圾回收。
2、 标記-複制算法(Mark-Copying)
将記憶體劃分為兩塊區域,每次使用其中一塊,當其中一塊用滿,觸發垃圾回收的時候,将存活的對象複制到另一塊上去,然後把之前使用的那一塊進行格式化,一次性清除幹淨。
(清除前)
(清除後)
“标記-複制”算法的缺點顯而易見,就是記憶體空間使用率低。
3、 标記-整理算法(Mark-Compact)
标記整理算法标記過程仍然與"标記-清除"算法一樣,但是後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。
将所有存活的對象向一邊移動,清理掉存活邊界以外的全部記憶體空間。
結合這三種算法我們可以看到,
- “标記-複制”算法的優點是回收效率高,但空間使用率上有一定的浪費。
- 而“标記-整理”算法由于需要向一側移動等一系列操作,其效率相對低一些,但對記憶體空間管理上十分優異。
- 是以,“标記-複制”算法适用于那些生命周期短、回收頻率高的記憶體對象,
- 而标記-整理”算法适用于那些生命周期長、回收頻率低,但注重回收一次記憶體空間得到足夠釋放的場景。
是以JVM的設計者将JVM的堆記憶體,分為了兩大塊區域Young區和Old區,Young區存儲的就是那些生命周期短,使用一兩次就不再使用的對象,回收一次基本上該區域十之有八的對象全部被回收清理掉,是以Young區采用的垃圾回收算法也就是“标記-複制”算法。Old區存儲的是那些生命周期長,經過多次回收後仍然存活的對象,就把它們放到Old區中,平時不再去判斷這些對象的可達性,直到Old區不夠用為止,再進行一次統一的回收,釋放出足夠的連續的記憶體空間。
9、各種問世的垃圾收集器
鑒于Young區和Old區需要采用不同的垃圾回收算法,是以在JVM的整個垃圾收集器的演進各個時代裡,針對Young區和Old區每個時代都是不同的垃圾收集機制。從JDK1.3開始到目前,JVM垃圾收集器的演進大體分為四個時代:串行時代、并行時代、并發時代和G1時代。
1、串行時代:Serial(Young區)+ Serial Old(Old區)
JDK3(1.3)的時候,大概是2000年左右,那個時代基本計算機都是單核一個CPU的,是以垃圾回收最初的設計實作也是基于單核單線程工作的。并且垃圾回收線程的執行相對于正常業務線程執行來說還是STW(stop the world)的,使用一個CPU或者一條收集線程去完成垃圾收集工作,這個線程執行的時候其它線程需要停止。
串行收集器采用單線程stop-the-world的方式進行收集。當記憶體不足時,串行GC設定停頓辨別,待所有線程都進入安全點(Safepoint)時,應用線程暫停,串行GC開始工作,采用單線程方式回收空間并整理記憶體。單線程也意味着複雜度更低、占用記憶體更少,但同時也意味着不能有效利用多核優勢。是以,串行收集器特别适合堆記憶體不高、單核甚至雙核CPU的場合。
2、并行時代:Parallel Scavenge(Young區) + Parallel Old(Old區)
并行收集器是以關注吞吐量為目标的垃圾收集器,也是server模式下的預設收集器配置,對吞吐量的關注主要展現在年輕代Parallel Scavenge收集器上。
并行收集器與串行收集器工作模式相似,都是stop-the-world方式,隻是暫停時并行地進行垃圾收集。年輕代采用複制算法,老年代采用标記-整理,在回收的同時還會對記憶體進行壓縮。關注吞吐量主要指年輕代的Parallel Scavenge收集器,通過兩個目标參數-XX:MaxGCPauseMills和-XX:GCTimeRatio,調整新生代空間大小,來降低GC觸發的頻率。并行收集器适合對吞吐量要求遠遠高于延遲要求的場景,并且在滿足最差延時的情況下,并行收集器将提供最佳的吞吐量。
3、 并發時代:CMS(Old區)
并發标記清除(CMS)是以關注延遲為目标、十分優秀的垃圾回收算法,CMS是針對Old區的垃圾回收實作。
老年代CMS每個收集周期都要經曆:初始标記、并發标記、重新标記、并發清除。其中,初始标記以STW的方式标記所有的根對象;并發标記則同應用線程一起并行,标記出根對象的可達路徑;在進行垃圾回收前,CMS再以一個STW進行重新标記,标記那些由mutator線程(指引起資料變化的線程,即應用線程)修改而可能錯過的可達對象;最後得到的不可達對象将在并發清除階段進行回收。值得注意的是,初始标記和重新标記都已優化為多線程執行。CMS非常适合堆記憶體大、CPU核數多的伺服器端應用,也是G1出現之前大型應用的首選收集器。
- 但CMS有以下兩個缺陷:
- (1)由于它是标記-清除不是标記-整理,是以會産生記憶體碎片,Old區會随着時間的推移而終究被耗盡或産生無法配置設定大對象的情況。最後不得不通過底層的擔保機制(CMS背後有串行的回收作為兜底)進行一次Full GC,并進行記憶體壓縮。
- (2)由于标記和清除都是通應用線程并發進行,兩類線程同時執行時會增加堆記憶體的占用,一旦某一時刻記憶體不夠用,就會觸發底層擔保機制,又采用串行回收進行一次STW的垃圾回收。
4、G1時代:Garbage First
G1收集器時代,Java堆的記憶體布局與就與其他收集器有很大差别,它将整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是實體隔離的了,它們都是一部分Region(不需要連續)的集合。
如上圖所示,每一個Region(分區)大小都是一樣的,1~32M之間的數值,但必須是2的指數。設定Region大小通過以下參數:-XX:G1HeapRegionSize=M。
G1收集器的原理或特點主要有以下三點:
(1)記憶體邏輯上仍保留的分代的概念,每一個Region同一時間要麼被标記為新生代,要麼被标記為老年代,要麼處于空閑;
(2)整體上采用了“标記-整理算法”,不會産生記憶體碎片
(3)可預測的停頓,G1整體采用的政策是“篩選回收”,也就是回收前會對各個待回收的Region的回收價值和成本進行排序,根據G1配置所期望的回收時間,選擇排在前面的幾個Region進行回收。
其實之是以叫G1(Garbage First)就是因為它優先選擇回收垃圾比較多的Region分區。
整體G1的垃圾回收工作步驟分為:初始标記、并發标記、最終标記和篩選回收。
5、ZGC:Zero GC
這篇文章簡單提一下這個最新問世的垃圾收集器,之是以叫“Zero GC”是因為它追求的是更低的GC停頓時間,追求的目标是:支援TB級堆記憶體(最大4T)、最大GC停頓10ms。JDK11新引入的ZGC收集器,不管是實體上還是邏輯上,ZGC中已經不存在新老年代的概念了會分為一個個page,當進行GC操作時會對page進行壓縮,是以沒有碎片問題。由于其是JDK11和隻能在64位的linux上使用,是以目前用得還比較少。
結語
以上總體兩篇文章七千字,就是我從JVM的作用、設計架構到JVM記憶體管理的整體的體系化了解。感謝。
拓展閱讀:
十個問題弄清JVM&GC(二)作者:宜信技術學院 譚文濤