天天看點

深入了解Java虛拟機|JVM03-垃圾收集器與記憶體配置設定政策01第3章 垃圾回收器與記憶體配置設定政策參考

深入了解Java虛拟機

  • 第3章 垃圾回收器與記憶體配置設定政策
    • 3.2 對象已死?
      • 3.2.1 引用計數法
      • 3.2.2可達性分析算法
      • 3.2.3 再談引用
      • 3.2.4 生存還是死亡
      • 3.2.5 回收方法區
    • 3.3 垃圾收集算法
      • 3.3.1 分代收集理論
        • GC分類
      • 3.3.2 标記-清除算法
      • 3.3.3 标記-複制算法
      • 3.3.4 标記-整理算法
      • 概念補充:
      • 對象配置設定的過程:
        • 為對象配置設定記憶體:TLAB
  • 參考

第3章 垃圾回收器與記憶體配置設定政策

Java與C++之間有一堵由記憶體動态配置設定和垃圾收集技術所圍成的"高牆",牆外面的人想進去,牆裡面的人卻想出來。

GC需要完成的3件事情:

  • 哪些記憶體需要回收?
  • 什麼時候回收?
  • 如何回收?

前面提到的Java記憶體運作時區域的各部分,其中程式計數器、虛拟機棧、本地方法在3各區域随線程而生,随線程而滅,棧中的棧幀随着方法進入和退出而出棧入棧,而每個棧幀配置設定多少記憶體基本在類結構确定下來時就已知。是以,這三個區域的記憶體配置設定和回收都具備确定性,不需要考慮去回收問題,線程結束時自然就回收了。

本章主要關注于Java堆和方法區這兩個具有顯著不确定性的區域。

3.2 對象已死?

垃圾收集器在做垃圾回收的時候,首先需要判定的就是哪些記憶體是需要被回收的,哪些對象是「存活」的,是不可以被回收的;哪些對象已經「死掉」了,需要被回收。

3.2.1 引用計數法

Java 堆 中每個具體對象(不是引用)都有一個引用計數器。當一個對象被建立并初始化指派後,該變量計數設定為1。每當有一個地方引用它時,計數器值就加1。當引用失效時,即一個對象的某個引用超過了生命周期(出作用域後)或者被設定為一個新值時,計數器值就減1。任何引用計數為0的對象可以被當作垃圾收集。當一個對象被垃圾收集時,它引用的任何對象計數減1。

優點:

引用計數收集器執行簡單,判定效率高,交織在程式運作中。對程式不被長時間打斷的實時環境比較有利。

缺點:

難以檢測出對象之間的循環引用。同時,引用計數器增加了程式執行的開銷。是以Java語言并沒有選擇這種算法進行垃圾回收。

3.2.2可達性分析算法

可達性分析算法又叫根搜尋算法,該算法的基本思想就是通過一系列稱為“GC Roots”的根對象作為起始節點集,從這些起始點開始根據引用鍊往下搜尋,搜尋所走過的路徑稱為引用鍊,當一個對象到 GC Roots 對象之間沒有任何引用鍊的時候(不可達),證明該對象是不可用的,于是就會被判定為可回收對象。

如下圖所示: Object5、Object6、Object7 雖然互有關聯, 但它們到GC Roots是不可達的, 是以也會被判定為可回收的對象。

深入了解Java虛拟機|JVM03-垃圾收集器與記憶體配置設定政策01第3章 垃圾回收器與記憶體配置設定政策參考

GC Roots的對象包括固定的幾種和一些運作時臨時加入的。

固定的GC Roots有:

1)在虛拟機棧中引用的對象,如各個線程被調用的方法堆棧中使用到的參數,局部變量,臨時變量

2)在方法區中類靜态屬性引用的對象,如Java類引用類型靜态變量

3)在方法區中常量引用對象,如字元串常量池裡的引用

4)在本地方法棧中JNI,如基本資料類型對應的Class對象,還有系統類加載器

5)所有被同步鎖(synchronized關鍵字)持有的對象

6)反映Java虛拟機内部情況的JMXBean、JVMTI中注冊的回調,本地代碼緩存等。

一個判斷的小方法:由于Root采用的是棧方法存放變量和指針,是以如果一個指針儲存了堆裡面的對象,但是自己又不存放在堆記憶體中, 則它是一個Root。

動态的加入則是考慮了分代收集和局部回收,是以收集某個區域對象時,要将其它區域的對象加入到GC Roots中。

判斷可達性分析時,要確定根節點的美劇要在一個一緻性的快照中進行,也就是說要避免根節點集合的對象引用關系在枚舉期間不斷變化,是以垃圾收集過程必須停頓所有使用者線程,即“stop the world”,類似的停頓在垃圾收集時的标記過程也會發生,隻是後者時間更短。

3.2.3 再談引用

無論是通過引用計數器還是通過可達性分析來判斷對象是否可以被回收都設計到“引用”的概念。JDK1.2之後, Java 中根據引用關系的強弱不一樣,将引用類型劃為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)。

  • 強引用:Object obj = new Object()這種方式就是強引用,隻要這種強引用存在,垃圾收集器就永遠不會回收被引用的對象。JDK1.2之前的傳統引用。
  • 軟引用:用來描述一些有用但非必須的對象。在 OOM 之前垃圾收集器會把這些被軟引用的對象列入回收範圍進行二次回收。如果本次回收之後還是記憶體不足才會觸發 OOM。在 Java 中使用 SoftReference 類來實作軟引用。
  • 弱引用:同軟引用一樣也是用來描述非必須對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象。在 Java 中使用 WeakReference 類來實作。
  • 虛引用:是最弱的一種引用關系,一個對象是否有虛引用的存在完全不影響對象的生存時間,也無法通過虛引用來擷取一個對象的執行個體。一個對象使用虛引用的唯一目的是為了在被垃圾收集器回收時收到一個系統通知。在 Java 中使用 PhantomReference 類來實作。

3.2.4 生存還是死亡

一個對象是否應該在垃圾回收器在GC時回收,至少要經曆兩次标記過程。

第一次标記:如果對象在進行可達性分析後被判定為不可達對象,那麼它将被第一次标記并且進行一次篩選。篩選的條件是此對象是否有必要執行 finalize() 方法。對象沒有覆寫 finalize() 方法或者該對象的 finalize() 方法曾經被虛拟機調用過,則判定為沒必要執行。

finalize()第二次标記:如果被判定為有必要執行 finalize() 方法,那麼這個對象會被放置到一個 F-Queue 隊列中,并在稍後由虛拟機自動建立的、低優先級的 Finalizer 線程去執行該對象的 finalize() 方法。但是虛拟機并不承諾會等待該方法結束,這樣做是因為,如果一個對象的 finalize() 方法比較耗時或者發生了死循環,就可能導緻 F-Queue 隊列中的其他對象永遠處于等待狀态,甚至導緻整個記憶體回收系統崩潰。finalize() 方法是對象逃脫死亡命運的最後一次機會,如果對象要在 finalize() 中挽救自己,隻要重新與 GC Roots 引用鍊關聯上就可以了。這樣在第二次标記時它将被移除「即将回收」的集合,如果對象在這個時候還沒有逃脫,那麼它基本上就真的被回收了。對象的finalize()方法隻會執行一次。

是以根據finalize的存在,是否可回收就可以分為三個判斷狀态:

  • 可觸及的:從根節點出發能到達該點。
  • 可複活的:對象的所有引用被釋放,但是對象可能在finalize中複活。
  • 不可觸及的:對象的finalize被調用,并且沒有複活,則進入不可觸及狀态。不可觸及對象不可能複活,因為對象的finalize()方法隻會執行一次。

3.2.5 回收方法區

在 Java 虛拟機規範中沒有要求方法區實作垃圾收集,而且方法區垃圾收集的成本效益也很低。

方法區(永久代)的垃圾收集主要回收兩部分内容:廢棄常量和無用的類。

廢棄常量的回收和 Java 堆中對象的回收非常類似,這裡就不做過多的解釋了。

類的回收條件就比較苛刻了。要判定一個類是否可以被回收,要滿足以下三個條件:

  • 該類的所有執行個體已經被回收;
  • 加載該類的 ClassLoader 已經被回收;
  • 該類的 Class 對象沒有被引用,無法再任何地方通過反射通路該類的方法

3.3 垃圾收集算法

根據如何判斷對象消亡,垃圾收集器算法可分為“引用計數式垃圾收集”和“追蹤式垃圾收集”,而本文主要讨論的HotSpot VM是采用的可達性分析算法判斷,是以這裡讨論的垃圾收集算法都是追蹤式垃圾收集”。

這部分可以參考部落格圖檔:(10條消息) 深入了解Java虛拟機-垃圾回收器與記憶體配置設定政策_ThinkWon的部落格-CSDN部落格

3.3.1 分代收集理論

1)弱分代假說:絕大多樹對象都是朝生夕滅的。

2)強分代假說:熬過越多次垃圾收集過程的對象就越難被回收。

依據這兩個假說,奠定了多款常用垃圾收集器的一直設計原則:收集器應該将Java堆劃分出不同的區域,然後将回收對象依據其年齡(對象熬過的垃圾收集過程次數)配置設定到不同的區域中存儲。

GC分類

針對HotSpot的實作,其将GC按回收區域分為兩種類型:一種是部分收集(Partail GC),一種是整堆收集(Full GC)。

  • 部分收集(Partial GC):
    • 新生代收集(“Minor GC/Young GC”,收集新生代中的可回收對象)
    • 老年代收集(“Major GC/Old GC”,收集老年代中的可回收對象,有時候回合Full GC混淆使用。目前隻有CMS收集器支援單獨收集老年代)
    • 混合收集(“Mixed GC”,收集整個新生代及部分老年代的可回收對象,隻有G1收集器支援)
  • 整堆收集(“Full GC”)收集整個Java堆和方法區中的可回收對象。

但是這兩個理論尚不夠完整,未考慮到對象之間的跨代引用,于是有了第三個假說。

3)跨代引用假說:跨代引用相對于同代引用來說僅占極少數。

依據該假說,隻需要在新生代上建立一個記憶集(remembered set)将老年代劃分成若幹小塊,辨別出哪一塊會存在跨代引用,之後Minor GC時,指把該小塊記憶體裡包含了跨代引用的老年代對象加入到GC Roots中掃描,再使用可達性分析算法标記回收對象。

3.3.2 标記-清除算法

标記-清除算法(Mark-Sweep)是一種常見的基礎垃圾收集算法,它将垃圾收集分為兩個階段:

标記階段:标記出可以回收的對象。

清除階段:回收被标記的對象所占用的空間。

标記-清除算法之是以是基礎的,是因為後面講到的垃圾收集算法都是在此算法的基礎上進行改進的。

優點:實作簡單,不需要對象進行移動。

缺點:标記、清除過程效率低,産生大量不連續的記憶體碎片,提高了垃圾回收的頻率。

注意:這裡的清除是指下次配置設定空間時,如果要回收的對象空間大小足夠,則用該部分空間配置設定給新的對象。

3.3.3 标記-複制算法

為了解決标記-清除算法的效率不高的問題,産生了複制算法。它把記憶體空間劃為兩個相等的區域,每次隻使用其中一個區域。垃圾收集時,周遊目前使用的區域,把存活對象複制到另外一個區域中,最後将目前使用的區域的可回收的對象進行回收。

優點:按順序配置設定記憶體即可,實作簡單、運作高效,不用考慮記憶體碎片。

缺點:可用的記憶體大小縮小為原來的一半,對象存活率高時會頻繁進行複制。

商用虛拟機大多優先采用這種收集算法回收新生代。為了緩解記憶體嚴重浪費問題, 針對具有“朝生夕滅”特點的對象(新生代具有這樣特點)提出更優化的半區複制分代政策,叫做“Appel式回收”。其将新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次配置設定記憶體隻是用Eden和其中的一塊Survivor。垃圾收集時,将Eden和Survivor中仍然存活的對象一次性複制到另外一塊Survivor空間上,然後直接清理掉Eden和剛用過的那塊Survivor空間。HotSpot虛拟機預設的Eden和Survivor比例是8:1,即每次新生代中可用的記憶體空間為整個新生代空間的90%,因為有2個Survivor空間,則剛好是8:1:1。當然不能保證每次都隻有不多于10%的對象存活,是以需要有個逃生門的安全設計,當Survivor空間不足以容納一次Minor GC之後存貨的對象時,就要依賴于其它區域(老年代)做擔保配置設定,即通過配置設定擔保機制直接進入老年代。

3.3.4 标記-整理算法

在新生代中可以使用複制算法,但是在老年代就不能選擇複制算法了,因為老年代的對象存活率會較高,這樣會有較多的複制操作,導緻效率變低。标記-清除算法可以應用在老年代中,但是它效率不高,在記憶體回收後容易産生大量記憶體碎片。是以就出現了一種标記-整理算法(Mark-Compact)算法,與标記-整理算法不同的是,在标記可回收的對象後将所有存活的對象壓縮到記憶體的一端,使他們緊湊的排列在一起,然後對端邊界以外的記憶體進行回收。回收後,已用和未用的記憶體都各自一邊。

優點:解決了标記-清理算法存在的記憶體碎片問題。

缺點:仍需要進行局部對象移動,一定程度上降低了效率。移動存活的對象,那麼在移動對象的這個時候程式全部暫停一下,即“stop the world”現象。

總之,是否移動對象都存在弊端,移動則記憶體回收時更複雜,不移動則記憶體配置設定時更複雜。有一些則是采用二者綜合,即先采用标記-清除算法,知道額你存空間的碎片化程度大到影響對象配置設定,則采用一次标記-整理算法收集一次,活動規整的記憶體空間。

概念補充:

新生代(Young generation)

絕大多數最新被建立的對象會被配置設定到這裡,由于大部分對象在建立後會很快變得不可達,是以很多對象被建立在新生代,然後消失。對象從這個區域消失的過程我們稱之為 minor GC。

新生代 中存在一個Eden區和兩個Survivor區。新對象會首先配置設定在Eden中(如果新對象過大,會直接配置設定在老年代中)。在GC中,Eden中的對象會被移動到Survivor中,直至對象滿足一定的年紀(定義為熬過GC的次數),會被移動到老年代。

可以設定新生代和老年代的相對大小。這種方式的優點是新生代大小會随着整個堆大小動态擴充。參數 -XX:NewRatio 設定老年代與新生代的比例。例如 -XX:NewRatio=8 指定 老年代/新生代 為8/1. 老年代 占堆大小的 7/8 ,新生代 占堆大小的 1/8(預設即是 1/8)。

例如:

-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
           

老年代(Old generation)

對象沒有變得不可達,并且從新生代中存活下來,會被拷貝到這裡。其所占用的空間要比新生代多。也正由于其相對較大的空間,發生在老年代上的GC要比新生代要少得多。對象從老年代中消失的過程,可以稱之為major GC(或者full GC)。

永久代(permanent generation)

像一些類的層級資訊,方法資料 和方法資訊(如位元組碼,棧 和 變量大小),運作時常量池(JDK7之後移出永久代),已确定的符号引用和虛方法表等等。它們幾乎都是靜态的并且很少被解除安裝和回收,在JDK8之前的HotSpot虛拟機中,類的這些**“永久的”** 資料存放在一個叫做永久代的區域。

永久代一段連續的記憶體空間,我們在JVM啟動之前可以通過設定-XX:MaxPermSize的值來控制永久代的大小。但是JDK8之後取消了永久代,這些中繼資料被移到了一個與堆不相連的稱為元空間 (Metaspace) 的本地記憶體區域。

深入了解Java虛拟機|JVM03-垃圾收集器與記憶體配置設定政策01第3章 垃圾回收器與記憶體配置設定政策參考

對象配置設定的過程:

為新對象配置設定記憶體是一件非常嚴謹和複雜的任務,JVM的設計者們不僅需要考慮記憶體如何配置設定、在哪裡配置設定等問題,并且由于記憶體配置設定算法與記憶體回收算法密切相關,是以還需要考慮Gc執行完記憶體回收後是否會在記憶體空間中障生記憶體碎片。

  1. new的對象先放伊甸園區。此區有大小限制。
  2. 當伊甸園的空間填滿時,程式又需要建立對象,JVM的垃圾回收器将對伊甸園區進行垃圾回收(Minor GC),将伊甸園區中的不再被其他對象所引用的對象進行銷毀。再加載新的對象放到伊甸園區
  3. 然後将伊甸園中的剩餘對象移動到幸存者0區。
  4. 如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者0區的,如果沒有回收,就會I放到幸存者1區。
  5. 如果再次經曆垃圾回收,此時會重新放回幸存者0區,接着再去幸存者1區。
  6. 啥時候能去養老區呢?可以設定次數。預設是15次。·可以設定參數: -XX:MaxTenuringThreshold=進行設定。
  7. 在養老區,相對悠閑。當養老區記憶體不足時,再次觸發cC:Major Gc,進行養老區的記憶體清理。
  8. 若養老區執行了Major GC之後發現依然無法進行對象的儲存,就會産生ooM異常
深入了解Java虛拟機|JVM03-垃圾收集器與記憶體配置設定政策01第3章 垃圾回收器與記憶體配置設定政策參考

為對象配置設定記憶體:TLAB

TLAB(Thread Local Allocation Buffer)本地線程配置設定緩沖。

·堆區是線程共享區域,任何線程都可以通路到堆區中的共享資料,·由于對象執行個體的建立在JVM中非常頻繁,是以在并發環境下從堆區中劃分記憶體空間是線程不安全的。

(具體劃分記憶體空間有指針碰撞和空閑清單兩種方式,看2.3.1節對象的建立,這裡隻介紹解決空間配置設定的線程安全問題)

解決方案有兩種:

  • 一種是同步加鎖,為避免多個線程操作同一位址,需要使用加鎖等機制,但是這種方式很影響配置設定速度。
  • 一種是本地線程配置設定緩存(TLAB)。從記憶體模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM為每個線程配置設定了一個私有緩存區域,它包含在Eden空間内。哪個線程要配置設定記憶體就在哪個線程的本地緩沖區種配置設定。隻有本地緩存區用完了才需要同步鎖定來配置設定新的緩存區。

多線程同時配置設定記憶體時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升記憶體配置設定的吞吐量,是以我們可以将這種記憶體配置設定方式稱之為快速配置設定政策。

·據我所知所有openJDK衍生出來的JVM都提供了TLAB的設計。

小結

深入了解Java虛拟機|JVM03-垃圾收集器與記憶體配置設定政策01第3章 垃圾回收器與記憶體配置設定政策參考
深入了解Java虛拟機|JVM03-垃圾收集器與記憶體配置設定政策01第3章 垃圾回收器與記憶體配置設定政策參考

參考

筆記主要内容來自《深入了解Java虛拟機 第3版》,裡面的一些插圖來自尚矽谷的宋紅康老師的ppt。