JVM篇--垃圾回收算法精講(嘔心瀝血整理)

爲什麼要垃圾回收?

 答:java語言中一個顯著的特點就是引入垃圾回收機制,使C++程序員最頭疼的內存管理的問題影刃而解。由於有個垃圾回收機制,java對象不再有“作用域”的概念,只有對象的引用菜有“作用域”。垃圾回收可以有效的防止內存泄露,有效的使用空閒內存。

垃圾收集器在對堆區和方法區進行回收前,首先要確定這些區域對象哪些可以被回收,哪些暫時還不能回收,這就要用到判斷對象是否存活的算法!

 

1.引用計數法

引用計數法方法:引用方對對象進行調用的時候就給它的引用值+1,當引用斷開之後進行-1

優缺點:

  • 引用計數器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。
  • 但是無法檢測出循環引用,如果兩個對象互相引用,會發生類似死鎖的問題,引用計數法一直覺得有對象在引用就一直釋放不了內存

1.1 對象是否存活判斷?

堆中每個對象實例都有一個引用計數。當一個對象被創建時,且將該對象實例分配給一個變量,該變量計數設置爲1。當任何其他變量被賦值爲這個對象的引用時,計數加1(a=b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例都可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1。

JVM虛擬機不是通過引用計數法來收集的,而是用對象存活算法可達性分析來標記要清除的垃圾的。

1.1 對象存活算法可達性分析

 

Java爲了解決引用計數法循環引用不了的問題,採取了可達性分析算法,該方法的基本思想是通過一系列的”GC Roots”對象作爲起點進行搜索,如果在”GC Roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的,不過要注意被判定不可達的對象不一定就會成爲可回收對象,被判定不可達對象要成爲可回收對象至少精力兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成爲可回收對象的可能性,則基本上就真的成爲可回收對象了。

 

在Java中可作爲”GC ROOT”的對象有:

  1. 虛擬機棧中引用的對象(本地變量表)(可以理解爲:引用棧幀中的本地變量表所有對象)
  2. 方法區中靜態屬性引用的對象(可以理解爲:引用方法區該靜態屬性的所有對象)
  3. 方法區中常量引用的對象(可以理解爲:引用方法區中常量的所有對象)
  4. 本地方法棧中引用的對象(可以理解爲:引用Native方法的所有對象)

 

  • 首先第一種是虛擬機棧中的引用的對象,我們在程序中正常創建一個對象,對象會在堆上開闢一塊空間,同時會將這塊空間的地址作爲引用保存到虛擬機棧中,如果對象生命週期結束了,那麼引用就會從虛擬機棧中出棧,因此如果在虛擬機棧中有引用,就說明這個對象還是有用的,這種情況是最常見的。
  • 第二種是我們在類中定義了全局的靜態的對象也就是使用了static關鍵字,由於虛擬機棧是線程私有的,所以這種對象的引用會保存在共有的方法區中,顯然將方法區中的靜態引用作爲GC Roots是必須的。
  • 第三種便是常量引用,就是使用了static final關鍵字,由於這種引用初始化之後不會修改,所以方法區常量池裏的引用的對象也應該作爲GC Roots。最後一種是在使用JNI技術時,有時候單純的Java代碼並不能滿足我們的需求,我們可能需要在Java中調用C或C++的代碼,因此會使用native方法,JVM內存中專門有一塊本地方法棧,用來保存這些對象的引用,所以本地方法棧中引用的對象也會被作爲GC Roots

JDK1.2之後,Java對引用這個概念進行了擴充,也就是對象不僅僅只有引用和沒有引用兩個概念,而是擴展到了4個:

  • 強引用:類似於“Object obj=new Object()只要強引用在,垃圾收集器永遠不會回收掉被引用的對象。
  • 軟引用:是用來描述一些還有用但是並非必需的對象,對於軟引用對象,在內存溢出異常之前,會把這些對象列進回收範圍之中進行第二次回收。
  • 弱引用,比軟引用更弱一點,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集發生時無論內存是否足夠,都會只回收弱引用的對象。
  • 虛引用,最弱的引用關係,對象是否有虛引用對其生存時間是沒有影響的。唯一目的就是能在這個對象被收集器回收時收到一個系統通知

對象要想真正宣告死亡需要至少兩次的標記過程,當對象在可達性分析時候發現沒有被GC Roots鏈到那麼對象就會進行第一次標記並且進行第一次篩選,篩選的條件就是判斷該對象有沒有必要執行finalize()方法,需要執行的話就會把對象放入F-Queue的對列中去執行該對象中的finalize()方法。如果finalize()方法讓對象重新被GC Roots鏈到那麼對象就重新活下來,否則就會進行第二次標記,等待垃圾回收的到來

 

2.標記清除

最基礎的收集算法“標記-清除”(Mark-Sweep)算法,如同它的名字一樣,算法分爲“標記”和“清除”兩個階段,首先標記出所有需要回收的對象,在標記完成後統一回收所欲被標記的對象,它的標記過程其實就是可達性分析法方式

當黑色區域的內存回收以後,就會出現回收後的圖,變成一個不連續的內存,假如此刻我需要五個可用內存,就會無法找到足夠的連續內存的情況。也就是說會出現空間碎片。

標記-清除在被回收垃圾比較少的情況下比較高效,也有不足的地方,主要有兩個:

  • 效率問題,標記和清除的兩個過程的效率都不高;
  • 空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集操作。

接下來能猜到,一個缺陷總會隨着時間開發人員作出相應修改,作出更優的方法,那麼,複製算法應運而生

3.複製算法

3.1 爲什麼出現複製算法?

  1. 爲了解決效率問題,一種稱爲複製的收集算法出現了,它將可用內存按量劃分爲大小相等的兩塊,每次只使用其中的一塊。
  2. 當這一塊的內存用完了,就將還存活的對象複製到另外一塊上面,然後再把已使用過的內存一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。

 

注意:複製算法過程中沒有在使用可達性分析方法,因爲過程中不用標記是否是要刪除的內容,而是遍歷把存活的內容進行復,然後直接清除另一個內存即可。

3.2 複製算法使用場景?

Java堆能分成新生代和老年代,所有新生成的對象首先都是放在新生代的。新生代的目標就是儘可能快速的收集掉那些生命週期短的對象。那麼這裏的複製算法主要就是收集新生代的垃圾對象。現在的商業虛擬機都採用這種收集算法來回收新生代,研究表明,新生代中的對象98%是“朝生夕死”的,所以並不需要按照1:1的比例來劃分內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間(一般而言),每次使用Eden和其中一塊Survivor。

新生代分三個區,一個Eden區,兩個Survivor區,大部分對象在Eden區生成,當Eden區滿了還存活的對象就會被複制到Survivor區(兩個Survivor中的一個)。

Survivor分兩個空間,from Survivor和to Survivor,那麼Eden、from  Survivor、to Survivor的內存比例是8:1:1,劃分的目的是因爲HotSpot採用複製算法來回收新生代,設置這個比例是爲了充分利用內存空間,減少浪費。

3.3 堆結構分代的意義? 

Java虛擬機根據對象存活的週期不同,把堆內存劃分爲幾塊,一般分爲新生代、老年代和永久代(對HotSpot虛擬機而言,jdk1.8之後永久代變爲metaspace,也就是叫元空間),這就是JVM的內存分代策略。
  堆內存是虛擬機管理的內存中最大的一塊,也是垃圾回收最頻繁的一塊區域,我們程序所有的對象實例都存放在堆內存中。給堆內存分代是爲了提高對象內存分配和垃圾回收的效率。試想一下,如果堆內存沒有區域劃分,所有的新創建的對象和生命週期很長的對象放在一起,隨着程序的執行,堆內存需要頻繁進行垃圾收集,而每次回收都要遍歷所有的對象,遍歷這些對象所花費的時間代價是巨大的,會嚴重影響我們的GC效率。
  有了內存分代,情況就不同了,新創建的對象會在新生代中分配內存,經過多次回收仍然存活下來的對象存放在老年代中,靜態屬性、類信息等存放在永久代中,新生代中的對象存活時間短,只需要在新生代區域中頻繁進行GC,老年代中對象生命週期長,內存回收的頻率相對較低,不需要頻繁進行回收,永久代中回收效果太差,一般不進行垃圾回收,還可以根據不同年代的特點採用合適的垃圾收集算法。分代收集大大提升了收集效率,這些都是內存分代帶來的好處。

 3.4 新生代回收的流程?

新生成的對象在Eden區分配(大對象除外,大對象直接進入老年代),當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC

 GC開始時,對象只會存在於Eden區和From Survivor區,To Survivor區是空的(作爲保留區域)。GC進行時,Eden區中所有存活的對象都會被複制到To Survivor區,而在From Survivor區中,仍存活的對象會根據它們的年齡值決定去向,年齡值達到年齡閥值(默認爲15,新生代中的對象每熬過一輪垃圾回收,年齡值就加1,GC分代年齡存儲在對象的header中)的對象會被移到老年代中,沒有達到閥值的對象會被複制到To Survivor區。接着清空Eden區和From Survivor區,新生代中存活的對象都在To Survivor區。接着, From Survivor區和To Survivor區會交換它們的角色,也就是新的To Survivor區就是上次GC清空的From Survivor區,新的From Survivor區就是上次GC的To Survivor區,總之,不管怎樣都會保證To Survivor區在一輪GC後是空的。GC時當To Survivor區沒有足夠的空間存放上一次新生代收集下來的存活對象時,需要依賴其他內存(這裏指老年代)進行分配擔保。

3.5 標記整理算法與分代收集算法

標記整理算法是回收老年代方法。

3.5.1 標記整理算法解決了什麼問題?

複製收集算法在對象存活率較高時要進行較多的複製操作、效率就會變低。更關鍵的是,如果不想浪費50%的空間,就需要用額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以老年代一般不能直接選用這種算法。

3.5.2 標記整理回收的流程?

根據老年代的特點,有人提出了另外一種”標記-整理”算法,標記過程仍然與標記清除算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然後直接清理掉邊界以外的內存

作爲對比說一下標記-清除:首先標記出所有需要回收的對象,在標記完成之後統一回收所有標記的對象

3.5.3 分代收集

分代收集算法是目前大部分JVM的垃圾收集器採用的算法。

一般把java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。

在新生代中,每次垃圾收集時都會發現有大批對象死去,只有少量對象存活,那就使用複製算法,只需要付出少量存活對象的複製成本就可以完成收集,而老年代中因爲對象存活率高,沒有額外空間對它進行分配擔保,就必須使用“標記-整理”或者“標記-清除”算法來進行回收。

可算碼完了,整理以後還是發覺知識點很多,需要學的也很多,大家加油把!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章