Java虛擬機器垃圾回收相關知識點全梳理(上)

木木匠發表於2019-04-28

Java虛擬機器垃圾回收相關知識點全梳理(上)

一、前言

筆者最近在複習JVM的知識,本著記錄分享的精神,整理下學習Java虛擬機器垃圾回收相關知識點,由於整個垃圾回收內容比較多,我將整理成上下兩篇文章去分享,上篇我會主要分享Java虛擬機器的執行時資料區域劃分,垃圾回收演算法。下篇文章主要分享Java虛擬機器的垃圾回收器以及一些虛擬機器調優建議。

二、執行時資料區

Java虛擬機器定義了程式在執行期間的多種資料區域,其中有些區域是在Java虛擬機器建立的時候就建立了,只有在虛擬機器退出後才會被銷燬。根據Java虛擬機器定義,我們可以資料區域做如下區分,分為:堆、Java虛擬機器棧、程式計數器、方法區(後設資料區、執行時常量池、本地方法棧。下面我們來詳細介紹下每個區域的作用。

Java虛擬機器垃圾回收相關知識點全梳理(上)

2.1 程式計數器

程式計數器是一塊執行緒私有的區域,是一個較小的記憶體塊,用來存放當前執行緒執行的位元組碼的指令地址,如果執行的是本地方法(Native),這個計數器就會為空(Undefined)。

2.2 Java虛擬機器棧

Java虛擬機器棧是執行緒私有的區域,生命週期與執行緒相同,它儲存的是棧幀(Stack Frame),棧幀會來儲存區域性變數表、運算元棧、動態連結、方法出口和返回地址等資訊。每一個方法從呼叫到執行完成的過程,都對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,就會丟擲StackOverflowError異常;如果申請棧記憶體不夠,也會導致丟擲OutOfMemoryError異常。

Jvm引數 
-Xss:棧空間大小;棧的空間大小決定了棧能建立的深度
複製程式碼

棧結構如下:

Java虛擬機器垃圾回收相關知識點全梳理(上)

2.3本地方法棧

本地方法棧和java方法棧非常類似,他們之前的區別主要是Java方法棧是提供給位元組碼服務的,本地方法棧是給本地方法(C語言實現)呼叫服務的。Java虛擬機器並沒有對本地方法棧中使用的語言、資料結構等進行強制規定,所以虛擬機器可以自行實現它。Sun HotSpot虛擬機器把虛擬機器棧和Java方法棧進行了合二為一。本地方法棧也會和虛擬機器棧一樣丟擲StackOverFlowError和OutOfMemoryError異常。

2.4 堆

Java堆是一個所有執行緒共享的區域,堆用來儲存幾乎所有物件的例項和陣列,堆按照分代的思想進行劃分,可以劃分了新生代(YoungGeneration)和老年代(Old/Tenured Generation),新生代又可進一步細分為 eden、survivor space0(s0 或者 from space)和 survivor space1(s1或者to space)。我們用圖來表示下堆的劃分:

Java虛擬機器垃圾回收相關知識點全梳理(上)

eden區:新建物件一般都放在該區域,除非是新建了大物件,該區域放不下就直接存放在老年代(Tenured)。 S0和S1區:該區域放置的物件至少經歷了一次垃圾回收(Minor GC),如果經歷了多次回收,到達指定次數還存活,那麼就會被轉移到老年代。

Java虛擬機器規範規定堆可以是物理上不連續的空間,只需要邏輯上連續即可,我們可以通過命令(-Xmx和-Xms )來調整堆空間,如果申請的堆記憶體超過了堆的最大記憶體,將會丟擲OutOfMemoryError異常。

Jvm引數
 -Xmx:最大堆空間大小
 -Xms:最小堆空間大小
 -Xmn:新生代空間大小
複製程式碼

2.5 方法區(後設資料區)

方法區是執行緒共享的區域,它用於存放已經被虛擬機器載入的類資訊、常量池、靜態變數、即時編譯器編譯後的程式碼等資料。類資訊包括類的完整名稱、父類的完整名稱、型別修飾符(public/protected/private)和型別的直接介面類表;常量池指執行時常量池(後面有介紹);方法區又被稱為非堆(Non-Heap)。

在Host Spot虛擬機器的實現中,方法區也被稱為永久區,是一塊獨立於 Java 堆的記憶體空間。雖然叫永久區,但是永久區中的物件同樣可以被 GC 回收的(注:方法區是 JVM 的一種規範,永久區是一種具體實現,在 Java8 中,永久區已經被 Metaspace 元空間取而代之。相應的,JVM引數 PermSize 和 MaxPermSize 被 MetaSpaceSize 和 MaxMetaSpaceSize 取代)。對永久區 GC 的回收,通常主要從兩個方面分析:一是 GC 對永久區常量池的回收;二是永久區對類後設資料的回收。

當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常。

2.5.1執行時常量池

執行時常量池(Run-Time Constant Pool)是方法區的一部分,它主要用來存放編譯期生成的各種字面量和符號引用,既然是執行時常量池,理所應當的可以存放執行時產生的常量,比如呼叫String.intern()方法產生的字串常量就會被放入執行池常量中。

三、垃圾判定演算法

3.1 引用計數演算法

引用計數法的思想比較簡單,每個物件都有一個引用計數器,只要物件被引用,計數器就+1,當物件不再被引用時候,計數器就減一。這種演算法很高效,但是有一個致命缺點,就是有迴圈引用的問題。對於兩個無用物件的互相引用,就會導致兩個物件的計數器不為0,從而無法被判定為無用物件,無法回收記憶體。

3.2 可達性分析演算法

由於引用計數法有互相引用的缺陷,所以Java虛擬機器採用了可達性分析演算法來判定垃圾物件。這個演算法的思想是,以一系列稱為“GC Roots”的物件作為起始點,從這些起點往下搜尋,搜尋所走過的路徑稱為引用鏈(Referenc Chain),當一個物件到GC Roots沒有任何引用鏈(從GC Roots到這個物件不可達)時,就說明這個物件不可用,可以被回收。

可以作為GC Roots的物件包括:

  • 虛擬機器棧中引用的物件(區域性變數)。
  • 方法區中類靜態屬性引用的物件(靜態變數)。
  • 方法區中常量引用的物件(常量)。
  • 本地方法棧中JNI(即本地方法)引用的物件。

那為什麼上面四種物件就可以作為GC Roots呢?

1.虛擬機器棧中當前引用的物件,因為虛擬機器棧中的物件是隨著執行緒的生命週期存活的,那麼在垃圾判斷的時候,當前執行緒還存活,也就意味著棧中持有的物件肯定是存活的,所以可以作為GC Roots,本地方法棧也是一樣的道理。

2.對於方法區中的靜態變數引用和常量,我的理解是使用方法區中的物件作為GC Roots並不是一定就會以裡面所有的物件作為GC Roots,雖然Java虛擬機器並沒有規定方法區要進行回收,但是該區域在目前的JVM實現中都有回收,由於方法區也會對“廢棄常量”和“無用類”進行回收,所以選擇GC Roots只會選擇方法區內的有效物件。"廢棄常量"判斷比較簡單,對於“無用類”的判斷,Java虛擬機器只會判斷動態載入的類,對於原始載入的類,虛擬機器永遠不會自動解除安裝。所以判斷動態載入的類為無用類可以有以下原則:

  • 該類所有的例項都已經被回收,堆中不存在該類的任何例項。
  • 該類對於的類載入器已經被回收
  • 該類對應的java.lang.Class物件沒有在任何地方引用,無法通過反射訪問該類的方法。

Java虛擬機器垃圾回收相關知識點全梳理(上)

四、垃圾回收演算法

4.1 標記-清除演算法

標記-清除演算法分為“標記”和“清除”兩個階段,首先需要標記出需要回收的物件,標記完成後再進行統一的垃圾回收。該演算法有兩個缺點:1.效率不高;2.清除後會產生大量不連續的記憶體碎片,記憶體碎片會導致分配大物件時候,無法找到足夠的記憶體,從而提前觸發一次GC.

Java虛擬機器垃圾回收相關知識點全梳理(上)

4.2 複製演算法

上面的標記清除演算法效率不高,為了解決這個問題,就有了複製演算法,複製演算法就是把記憶體容量劃分為大小相等的兩塊,每次只用其中一塊,當一塊記憶體用完後就將存活的物件複製到另外一塊記憶體上,然後再對原記憶體塊進行清理。這種演算法的優點就是記憶體分配不用考慮碎片的問題,只需要移動堆頂的記憶體指標,按順序分配記憶體即可。但是這演算法的缺點就是空間利用率不高,將記憶體縮小為原來的一半,有一半的記憶體沒有被真正利用起來。

雖然記憶體利用率不高,但是目前的虛擬機器中堆中的新生代就是採用這種演算法進行垃圾回收的。上面我們提到新生代分為 eden 空間、form 空間和 to空間3個部分。其中 from 和 to 空間可以視為用於複製的兩塊大小相同、地位相等,且可進行角色互換的空間塊。from 和 to 空間也稱為 survivor 空間,即倖存者空間,用於存放未被回收的物件。

在垃圾回收時,eden空間中存活的物件會被複制到未使用的survivor空間中(假設是 to),正在使用的survivor空間(假設是 from)中的年輕物件也會被複制到to空間中(大物件或者老年物件會直接進入老年代,如果to空間已滿,則物件也會進入老年代)。此時eden和from空間中剩餘物件就是垃圾物件,直接清空,to空間則存放此次回收後存活下來的物件。

Java虛擬機器垃圾回收相關知識點全梳理(上)

4.3 標記-壓縮演算法

複製演算法只適用於存活率較低的新生代中,如果存活率較高就需要進行過多的複製操作,效率將會降低。老年代的存活率比較高,所以複製演算法不適用於老年代的場景,之前提到的“標記-清除”演算法,如果不會產生記憶體碎片的話,還是可以滿足老年代的,那麼有沒有不產生碎片的類似演算法呢?答案是有,“標記-整理”演算法就派上用處了。它的核心思想是:先對可回收物件進行標記,然後把所有存活的物件移動到一端,接著直接清理掉邊界意外的記憶體區域。因為清理過後,存活物件都緊密的在一端,所以不會產生記憶體碎片。

Java虛擬機器垃圾回收相關知識點全梳理(上)

八、總結

本篇文章我整理了Java虛擬機器的執行區劃分,每個區域的作用,同時分享了垃圾判斷演算法和垃圾回收演算法。執行時資料區劃分為:程式計數器、Java虛擬機器棧、本地方法棧、堆、方法區、執行時常量池。有的文章中提到Jdk1.7及以後的版本把執行時常量從方法區移除,這裡我想說明下,Java虛擬機器規範還是要求在方法區分配,這只是個別虛擬機器的自己實現,比如說Hot Spot虛擬機器。

垃圾判定演算法現在虛擬機器主要使用可達性分析演算法,垃圾回收演算法有“標記-清除”演算法、“複製”演算法、“標記-整理”演算法。“複製”演算法比較適合存活物件較少的新生代,“標記-整理”演算法比較適合老年代,整理的作用就是為了有連續的記憶體空間,防止記憶體碎片太多無法存放大物件。

九、參考

相關文章