JVM之一:GC垃圾回收原理及算法分析

導讀

本人java小白一枚,寫博客用意一是做一個學習總結,二是作一個分享。所寫內容難免會有錯誤或者理解不到位的情況,懇請各位大佬不斷對我提出批評,用技術吊打我,鞭笞我。拜謝~~

一、java中的垃圾

1、什麼是垃圾

簡單來說,就是java內存中沒有用了的對象,或者說是已經被嫌棄,死亡的對象。

2、如何去判斷對象是屬於垃圾對象呢?

最開始,有一種算法叫做引用計數法。顧名思義就是當對象被引用時,通過對對象的引用情況進行登記,如果存在引用的話,則進行加1,否則減1。當該應用計數爲0時,則進行回收處理。
示例:
創建一個對象

String name = new String("csdn");

在這裏插入圖片描述
此時,name對象指向"csdn",所以計數器RC爲1.

name = null;

後面我們將name設置爲null,即將name指向爲空,此時“csdn”區域將會被回收。
在這裏插入圖片描述
一切看起來都是那麼簡單而美好。然後,還是存在一些缺點的。
問題1:
引用計數法並不遵守“STOP THE WORLD”原則,即在回收垃圾時,會將所有java應用程序掛起,而引用計數法是將垃圾回收進行分攤,分到整個的應用程序當中。
問題2:
引用計數算法無法解決循環依賴的問題


public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
 
        object1.object = object2;
        object2.object = object1;
 
        object1 = null;
        object2 = null;
    }
}
 
class MyObject{
	private String name;
    public Object object = null;
    public MyObject(String name){
		this.name = name;
	}
}

代碼流程示意圖如下:
在這裏插入圖片描述
對象A和B已經不可能訪問了,但是在兩者還是相互被引用,導致它們的引用數永遠都不可能爲0.因此有內存溢出的風險。


那麼,針對上述兩個問題,我們該如何進行改進呢?
在這裏插入圖片描述
實際上,在java中垃圾標記採取的是可達性分析法。可達性分析法的基本思路是通過GC-ROOTS對象作爲起點,通過該節點向下進行搜索,搜索所走過的路徑稱爲引用鏈,如果一個對象跟GC-ROOTS不存在引用鏈的話,即該對象無法到達GC-ROOTS,就判定該對象爲可回收對象。

GC-ROOTS對象主要有四種:
①虛擬機棧中引用的對象
②方法區中靜態屬性引用的對象
③方法區中常量引用的對象
④本地方法棧(native方法)中引用的對象

那麼,被判定爲可收回對象就一定會被回收嗎?
答案是否定的。
可達性分析法判定爲不可達的對象其實相當於一個被判死緩的人,此時的對象處於緩刑期間,在此期間,Object存在finalize方法,讓不可達對象做最後的垂死掙扎,如果不可達對象重寫了finalize方法,且尚未執行finalize方法,則該對象會進去隊列中等待,如果在該方法中重新變爲可達,則不會被回收。在隊列中的不可達對象如果通過低優先級線程調用執行finalize方法後,仍舊不可達則會被 第二次標記,進行垃圾回收。
在這裏插入圖片描述
事實上,finalize方法的實際用處並不是很大,該方法具有很大的不確定性,無法確定該方法是否執行完,運行的代價也比較高,十分雞肋。據不可靠消息,java之所以要設計該方法,主要是爲了向C語言程序員更容易接受,而做的一個妥協讓步。


二、垃圾回收機制

1、標記-清除算法

在這裏插入圖片描述
同過對對象進行掃描標記,然後將標記爲死亡對象或者說不可達對象進行清理。這種方法是最簡單粗暴和直接,哪裏有垃圾就掃哪裏。但是該方法同樣存在缺點。從圖中可以看出,清除後,內存空間存在很多的小碎片,這會影響後續的使用。例如爲大對象分配空間時,由於空間碎片化,大空間不足,將會提前觸發垃圾回收機制。

2、標記複製方法

在這裏插入圖片描述
爲了解決標記-清除所帶來的碎片過多的缺點,標記複製算法的提出就是在其基礎上所做的改進。該方法是先將內存空間劃分爲兩個部分,一個部分爲創建對象區域,一個部分爲空閒區域。首先是在創建對象區域對對象進行標記,標記完畢後,將存活對象挪至空閒區域,依次存放,挪動完畢後,一次性將原區域中的死亡對象清除,標記-複製算法解決了標記-清除算法造成碎片過多的弊端,但是同時,我們可以看到,標記-複製算法所能真正使用的空間只有原來的一半,這大大降低內存空間的使用效率。

3、標記-整理算法

在這裏插入圖片描述
爲了改進標記-複製算法所帶來的空間利用率不高的問題,人們又提出了標記-整理算法,該算法的主要思路是先將對象進行標記,標記完畢之後,將所有存活對象進行移動一端,移動完畢之後,將端邊界以外的不可達對象即死亡對象進行一次清除,釋放掉內存。該方法即解決了碎片了問題,也解決了空間利用率低的問題。但是,該方法仍舊不是垃圾回收機制的最優解,由於涉及到對象的移動,毫無疑問,程序需要額外增加工作,這就降低了程序回收垃圾的工作效率。就好比你在收拾東西一樣,不僅要把垃圾扔掉,還需要把留下的東西分門別類整理好,肯定會佔用你的時間。

4、分代回收

有個成語叫做揚長避短,即發揮東西的長處,規避掉它的短處。上述三種算法很難一下子說誰絕對是好,誰絕對的差。而且我們都知道,其實每個對象的生命週期都不盡相同,那麼針對不同生命週期的對象我們其實可以採取不同的垃圾回收算法。
那麼對象的生命週期我們可以先簡單分類一下:
1、臨時對象: 朝生夕死的對象,就是命不久矣的對象。可以看做是容易夭折的baby。例如方法內的局域變量,循環體內的臨時變量等
2、持久對象: 一般是指能夠存活較久的對象。可以看做是正常壽命的人。到達一定歲數還是會掛掉的。例如緩存對象,數據庫連接對象等等
3、永久對象: 此類對象可以看做是出生後幾乎就不死的對象,可以簡單理解爲千年老王八。也只是幾乎不會掛。例如String池中的對象(享元模式),加載過的類信息等等。
那麼,我接下來講解分代回收的算法實現:
首先,我們先了解一下分代算法的內存模型:
在這裏插入圖片描述
解析:
堆內存中分爲新生代和老年代,新生代佔1/3,老年代佔2/3。其中新生代又分爲三個區域,Eden:From survivor:To survivor = 8:1:1。其中有一個關鍵點需要注意,From區和To區並不是跟圖中所示一樣,是固定的,二是兩者的身份會出現互相變換,這個後面具體算法實現的時候會提到。我們在此先約定,黃色代表From區,綠色代表To區
第一次GC
首先,有對象進來,此時所有堆中都是空的,假設對象0是大對象,我們存放到old區,對象1、2、3、4對象都是小對象,此時他們的age都爲0,即代表尚未經歷GC回收。此時我們觸發一次新生代的minor GC,假設對象1、2爲不可達對象,此時我們會清空Eden區和From區(此時From區暫無對象)的不可達對象,並將存活對象挪至To區,並將其age加1.挪完畢後,此時的To區實際上已經變成了From區。思考一下爲什麼呢?
在這裏插入圖片描述
第二次GC
假設第一次GC完畢之後,又有對象5、6、7進入Eden區,此時又觸發了一次minor GC,假設對象6以及From區中的對象3都是是存活對象,那麼根據規則(minor GC回收後,Eden區和From區中的存活對象都會挪動到To區,這個就是爲什麼From和To區是動態交替變化的原因),對象3和對象6統一挪到當前的To區,同時age加1。清除其餘的不可達對象。
在這裏插入圖片描述
第N次GC
那麼,當GC次數很多時,假設我們的對象3非常之堅挺,撐過了15次minor GC,那麼我們就會判定該對象長大了,將其挪至老年代。判定的age其實是可以設置的,默認爲15。
在這裏插入圖片描述
那麼,老年代在什麼時候會觸發GC回收呢
一般是在老年代快滿了或者System.gc()時,會觸發Full GC。對全堆進行回收,同時包括幾次minor GC,因此運行時間會比較長。

分代算法總結:
1、新生代採用的算法是標記複製算法,其實是對原有的複製算法的優化,默認將空閒區域劃分爲10%,即犧牲了10%的空間來換內存的整齊度和GC效率。但是存在一個問題,就是新生代用於保存可達對象可能會存在內存不足的情況。
2、老年代採用的算法是標記-清除和標記-整理算法,具體使用哪種其實和垃圾回收器有關。這裏就不一一展開講了。感興趣的小夥伴可以自行學習。


大家好,我是一名正在找工作的java程序員,如果您這邊有合適的崗位請與我聯繫(頭像即微信)。
原創不易,如果對您有幫助,歡迎三連。如果文中有任何錯誤,請留言指出,感謝您!

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