1. 程式人生 > >JVM 垃圾回收機制

JVM 垃圾回收機制

引用類型 引用 垃圾收集器 內容 清理 才會 空閑 碎片 刪除

首先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的機會。

JVM 垃圾回收機制