《深入理解java虛擬機》讀書筆記——GC與內存分配策略

一、垃圾收集器如何判斷堆裏的哪些對象需要回收

1、引用計數算法:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用,作爲"死對象"被垃圾收集器回收。

主流的java虛擬機沒有選用引用計數算法來管理內存,主要的原因是它很難解決對象之間循環引用的問題。比如對象A和對象B都有字段obj,其中A.obj = B且B.obj = A,除此之外無其他引用,引用計數算法無法通知垃圾收集器回收它們,因爲它們相互引用,彼此的引用計數都不爲0,但事實上這兩個對象可能再無任何引用。

2、可達性分析算法(引用鏈法):這個算法的基本思想就是通過一系列被稱爲"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,如果一個對象到“GC Roots”沒有任何引用鏈(搜索所走過的路徑稱爲引用鏈)相連時,則說明此對象不可用。

在java中,可作爲GC Roots的對象包括下面幾種:

①虛擬機棧(棧幀的局部變量表)中引用的對象。

②方法區中類靜態屬性引用的對象。

③方法區中常量引用的對象。

④本地方法棧中JNI(即一般說的Native方法)引用的對象。


二、垃圾收集器眼中的對象是不是隻有"被引用"和"沒有被引用"兩種狀態呢

在JDK1.2之後,java對引用的概念進行了擴充,將引用分爲強引用軟引用弱引用以及虛引用四種,引用強度依次逐漸減弱。

強引用:指的是類似"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。


軟引用:用來描述一些還有用但並非必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍進行第二次回收。若回收後還沒有足夠的內存,纔會拋出內存溢出。JDK1.2後,提供了SoftReference類來實現軟引用。(軟引用一般用來做緩存)


弱引用:也是用來描述非必需對象,強度比軟引用更弱,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前,無論當前內存是否足夠。JDK1.2後,提供了WeakReference類來實現弱引用。


虛引用:也稱幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時受到一個系統通知。JDK1.2後,提供了PhantomReference類來實現虛引用。


三、可達性分析算法中不可達的對象的處理

如果對象在進行可達性分析後沒有發現與GC Roots想連接的引用鏈,那他將會被第一次標記並且進行一次篩選,篩選的條件是此對象有沒有必要執行finalize()方法。


沒有必要執行finalize()方法的情況:若對象沒有實現finalize()方法或者finalize()方法已經被虛擬機調用過。


被判定有必要執行finalize()方法之後,那麼這個對象將會放在一個稱爲F-Queue的對隊列中,虛擬機會觸發一個Finalizer線程去執行,此線程是低優先級的,並且虛擬機不會承諾一直等待它運行完,這是因爲如果finalize()執行緩慢或者發生了死鎖,那麼就會造成F-Queue隊列一直等待,造成了內存回收系統的崩潰。GC對處於F-Queue中的對象進行第二次被標記,這時,該對象將被移除”即將回收”集合,等待回收。對象也可以在finalize()中重新與引用鏈上的任何一個對象建立關聯就可以逃掉被回收的命運(比如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量)。


四、方法區的回收

方法區進行GC性價比比較低,主要包括兩部分:廢棄常量無用的類

回收廢棄常量與回收堆中的對象相似,以常量池中字面量的回收爲例,假如一個字符串字面量"abc"已經進入了常量池,但是沒有任何String對象引用這個"abc"變量,也沒有其他地方引用了這個字面量,發生內存回收時,有必要的話,這個"abc"常量就會被系統清理出常量池。
判斷"無用的類"的條件較苛刻,同時滿足下面3個條件纔算是無用的類:
1.該類的所有實例都已經被回收;
2.加載該類的ClassLoader已經被回收;
3.該類對應的java.lang.Class對象沒有在任何地方被引用,無法通過反射訪問該類的方法。

滿足上述三個條件的類可以進行回收,並不是和對象一樣必然會被回收。


五、垃圾收集算法:

1、標記-清除:
這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,導致以後程序在分配較大的對象時,由於沒有充足的連續內存而提前觸發一次GC動作。
2、複製算法:
爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,然後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,然後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。但是這種方式,內存的代價太高,每次基本上都要浪費一般的內存。
於是將該算法進行了改進,內存區域不再是按照1:1去劃分,而是將內存劃分爲8:1:1三部分,較大那份內存叫Eden區,其餘是兩塊較小的內存區叫Survivor區。每次都會優先使用Eden區,若Eden區滿,就將對象複製到第二塊內存區上,然後清除Eden區,如果此時存活的對象太多,以至於Survivor不夠時,會將這些對象通過分配擔保機制複製到老年代中。(java堆又分爲新生代和老年代)
3、標記-整理
該算法主要是爲了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了複製算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生內存碎片了。
4、分代收集 
現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生代老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。


六、內存分配與回收策略

1、對象優先在Eden分配

對象在新生代Eden區分配,當Eden區沒有足夠的空間進行分配時,虛擬機發起一次MinorGC;
2.大對象直接進入年老代
需要大量連續內存空間的Java大對象直接在年老代中分配,通過-XX:PretenureSizeThreshold參數調整門檻大小;
3.長期存活的對象將進入年老代

虛擬機給每個對象定義一個對象年齡計數器,每當該對象熬過一次Minor GC,年齡就加1,當加到一定程度(默認是15歲)時,就會晉升到年老代。通過-XX:MaxTenuringThreshold參數來設置門檻大小;

4.動態對象年齡判定

如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,那麼》=該年齡的對象可以直接進入年老代,無需滿足MaxTenuringThreshold要求的年齡;
5.空間分配擔保
在發生Minor GC時,虛擬機會檢測之前每次晉升到年老代的平均大小是否大於年老代的剩餘空間大小。如果大於,則改爲直接進行一次Full GC;如果小於,則查看-XX:HandlePromotionFailure設置是否允許擔保失敗,如果允許,只進行Minor GC,如果不允許,則還需要進行一次Full GC。一般情況爲了避免Full GC過於頻繁,會把HandlePromotionFailure開關打開。


七、新生代GC(Minor GC)與老年代GC(Major GC/Full GC)

當Eden區沒有足夠的空間進行分配時,虛擬機會執行一次Minor GC.Minor GC通常發生在新生代的Eden區,在這個區的對象生存期短,往往發生GC的頻率較高,回收速度比較快。

Full GC/Major GC 發生在老年代,一般情況下,觸發老年代GC的時候不會觸發Minor GC,但是通過配置,可以在Full GC之前進行一次Minor GC這樣可以加快老年代的回收速度。





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