JVM 垃圾回收機制

牛牛的程式設計之路發表於2019-01-05

首先JVM的記憶體結構包括五大區域: 程式計數器、虛擬機器棧、本地方法棧、方法區、堆區。其中程式計數器、虛擬機器棧和本地方法棧3個區域隨執行緒啟動與銷燬, 因此這幾個區域的記憶體分配和回收都具有確定性,不需要過多考慮回收的問題。而Java堆區和方法區則不一樣,這部分記憶體的分配和回收是動態的,正式垃圾回收需要關注的部分。

垃圾回收在堆記憶體進行回收前, 要先確定區域的哪些物件是可以被回收的、那些物件暫時還不能回收,下面談一談判斷物件是否存活的演算法。

判斷物件是否存活的演算法

1.引用計數演算法

引用計數演算法:堆中的每個物件例項都有一個引用計數器,當一個物件被建立時,就將該物件例項分配給一個變數,該引用計數器設定為1,當任何其他變數被賦值為這個物件的引用時,計數加1,當一個物件例項的某個引用超過了生命週期或被賦為一個新值時, 引用計數減1。

任何引用計數器為0的物件例項都可以進行垃圾回收。當一個物件例項被垃圾回收時,它引用的所有物件例項引用計數器減1.

優點:引用計數器可以很快的執行,對程式不需要長時間的打斷

缺點:無法檢測出迴圈引用。如物件A有物件B的引用,物件B又有物件A的引用,這樣他們的引用計數永遠都不為0

2.可達性分析演算法

可達性演算法:將所有的引用關係看作一張圖,從一個節點GC Root開始,尋找對應的引用節點,找到後繼續尋找這個節點的引用節點,當所有引用節點尋找完畢後,剩餘的節點就被認為是沒有被引用的節點,即無用節點,無用節點被判定為可回收物件。

Java中可以作為GC Root的包括下面幾種:

  1. 虛擬機器棧中的引用物件
  2. 方法區中類靜態屬性引用的物件
  3. 方法區中常量引用的物件
  4. 本地方法棧中引用的物件

對於Java中的引用型別可以看這篇文章Java 控制類的引用型別,合理使用記憶體

常用的垃圾回收演算法

1.標記-清除演算法

標記-清除演算法採用從根集合(GC Roots)進行掃描,對存活的物件進行標記,標記完畢後,再掃描整個空間中未被標記的物件,進行垃圾回收

這種演算法實現起來比較容易,但是會造成記憶體碎片

2.標記-複製演算法

複製演算法是為了解決標記-清除演算法的缺陷而提出的。

它將記憶體劃分為大小相等的兩塊,每次只使用其中的一塊。當這A快記憶體用完了,就將還存活的物件複製到B塊上面,然後把A塊的記憶體空間一次性清理掉

這種演算法雖然實現簡單,執行高效且不易產生記憶體碎片,但是卻對記憶體空間的使用做出了高昂的代價,因為能使用的空間縮減為原來的一半。很顯然,複製演算法的效率跟存活物件的數量有很大關聯,若存活物件很多,那麼效率將大大降低

3.標記-整理演算法

該演算法是為了解決複製演算法的缺陷,充分利用記憶體空間而提出的。

該演算法與標記-清除演算法一樣,但是在完成標記後,不直接清理可回收物件,而是將存活物件全部向一端移動,接著清理掉邊界以外的記憶體。

4.分代收集演算法

分代收集演算法是目前大部分JVM的垃圾收集器採用的演算法。其核心思想是根據物件存活的生命週期將記憶體劃分為若干個不同的區域。

將其分為年輕代、老年代和永久代。然後根據不同的區域採用合適的收集演算法。

Java一般將堆區分為年輕代和老年代,將方法區劃為永久代。

下面對不同的年齡代進行簡單說明

年輕代:新建立的物件都存放在這裡。因為年輕代會頻繁的進行GC清理,JVM在年輕代採用的是標記-複製演算法,先標記出存活的例項,然後清除掉無用例項,將存活的例項根據年齡(每個例項被經歷一次GC後年齡會加1)拷貝到不同的年齡代。

老年代:老年代中是經歷了N此垃圾禍首後仍然存活的物件,其中的N由JVM的引數決定。這塊記憶體區域一般大於年輕代。GC發生的次數也比年輕代要少。

永久代:用於存放靜態檔案,如Java類、方法等。為方法區。

方法區主要回收的內容有:廢棄的常量、無用的類,對與廢棄常量可以同過引用的可達性判斷,但是對於無用類需要同時滿足以下3個條件:

  1. 該類的所有例項都已經被回收了
  2. 載入該類的 ClassLoader 已經被回收了
  3. 該類對應的 java.lang.Class 物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

GC在什麼時候觸發

GC在優先順序最低的執行緒中執行,一般在應用程式空閒時被呼叫。當記憶體不足時才會主動呼叫

因為物件進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有如下兩種:

1.Scavenge GC

一般情況下,當新物件生成,並且在年輕代申請空間失敗時,會觸發Scavenge GC, 對年輕代進行垃圾回收。這種方式的GC不會影響到老年代。因為大部分物件都是年輕代開始的,同時年輕代記憶體不會分配的很大,所有年輕代的GC會頻繁的進行。所以在這裡要使用速度快、效率高的演算法,使其空間儘快空出來。

若GC一次後仍不能滿足記憶體分配,JVM會進行二次GC,若仍無法滿足,則報“out of memory”的錯誤,Java應用將停止

2.Full GC

對整個記憶體進行整理,包括年輕代、老年代和永久代,所以Full GC比Scavenge GC要慢, 因此應該儘量減少Full GC的次數。以下可能引發Full GC的原因:

  1. 老年代被寫滿
  2. 永久代被寫滿
  3. System.gc()被顯示呼叫
  4. 上一次GC後堆的各域分配策略動態變化。

Java的垃圾回收介紹到這,下面在說說如何在程式中減少GC的開銷的幾個建議:

  1. 不要顯式呼叫System.gc()。此函式建議JVM進行GC,雖然只是建議,但是大多數情況下會觸發GC,增加了間歇性停頓的次數,大大影響系統的效能
  2. 儘量減少臨時物件的使用。也就是減少Scavenge GC執行的機會
  3. 物件不用時最好顯式置為null。將不用的物件置為null,有利於GC收集器判定,從而提高GC的效率
  4. 儘量減少靜態物件變數。靜態變數屬於全域性變數,不會被GC禍首。
  5. 能有基本型別的就不要用包裝類。基本型別變數棧用的記憶體資源比對應的包裝類要少的多
  6. 使用StringBuffer 而不是String類累加字串。因為堆String型別進行加的時候,會建立新的String物件,而StringBuffer是可變長的,在原有基礎上進行擴增,不會產生中間物件
  7. 分散物件建立或刪除的時間。集中在短時間內大量建立新物件,特別是大物件,會突然需要大量記憶體,JVM在面臨這種情況時只能進行GC,以回收記憶體或整合記憶體碎片,從而增加GC的頻率。集中刪除物件,道理也是一樣的。它使得突然出現了大量的垃圾物件,空閒空間必然減少,從而大大增加了下一次建立新物件時強制主GC的機會。

相關文章