JVM系列-垃圾回收

Java不需要我們手動的申請和釋放空間,而是由JVM自動的來進行分配和回收。既然是自動的,那跟手動釋放和回收相比,在精準性和效率上可能就會稍微差一些,所以Java虛擬機團隊也不斷地研發更優秀的垃圾收集器,也有了一代又一代的垃圾收集器。

既然要學習垃圾回收,那就會有如下的一些問題:回收哪些空間,怎麼判斷這個對象能不能回收,什麼時候進行回收,怎麼才能高效的回收。

回收哪些空間?
之前我們學習過JVM的內存劃分,裏面有些空間是線程私有的,有些是線程共享的。對於程序計數器、虛擬機棧、本地方法棧這些空間來說,屬於線程私有的空間。這些空間隨線程而生,隨線程而滅,不需要考慮回收的問題,因爲方法調用完成之後或者線程結束後,空間就自動釋放了。但是對於堆和方法區而言,是線程共享的,而且是在程序運行期間,動態進行空間分配和回收的。我們的垃圾回收算法,主要就是作用於這兩部分空間的。
回收堆的空間,就是對堆中的對象進行標記和回收,回收方法區的空間,一般就是回收一些廢棄的常量和無用的類。

如何判斷對象可以被回收?
判斷哪些對象可以被回收,其實就是判斷對象是否存活,活着的對象就不回收,死了的就回收。判斷對象是否存活,常見有兩種方法:引用計數算法和可達性分析算法。
1.引用計數算法
給對象中添加一個引用計數器,有地方引用時,計數器加1,引用失效時,計數器減1,計數器爲0時,說明沒有引用,可以回收。
Hotspot沒采用這種算法,因爲很難解決對象之間循環引用的問題。
2.可達性分析
通過一系列GC Roots的對象最爲起始點,一直向下搜索,走過的路徑成爲引用鏈,當一個對象到GC Roots沒有引用鏈時(也就是GC Roots到這個對象不可達),證明這個對象是不可用的。

常見的可以作爲GC Roots的對象爲:
1.虛擬機棧中,棧幀中的本地變量表引用的對象。
2.本地方法棧中引用的對象。
3.方法區中,類靜態屬性引用的對象。
4.方法區中,常量引用的對象。
5.已啓動且未停止的 Java 線程。

一般情況下,垃圾收集器會採用可達性分析算法來標記對象。這種方式看起來比較容易,只是從GC Roots開始標記一遍,然後回收就可以了,但是實際的操作過程還是比較複雜的。因爲我們程序在運行時是多線程運行的,垃圾回收的線程將對象標記完成之後,還可能有其他線程對該對象進行操作,比如把這個對象置爲null,成爲不可用對象。或者把這個對象又和GC Roots上的對象關聯起來。所以,之前標記的就是不準的。

Stop-the-world
爲了解決可達性分析標記不準的問題,就有了Stop-the-world,停止其他非垃圾回收線程的工作,直到完成垃圾回收。
Java 虛擬機中的 Stop-the-world 是通過安全點(safepoint)機制來實現的。當 Java 虛擬機收到 Stop-the-world 請求,它便會等待所有的線程都到達安全點,才允許請求 Stop-the-world 的線程進行獨佔的工作。安全點的目的就是找到一個穩定的狀態,在這個狀態下,JVM的堆棧不會發生變化,這樣,垃圾回收線程就可以進行可達性分析了。

常見的垃圾回收算法:
常見的垃圾回收算法有四種:標記-清除、複製、標記-整理、分代收集。
標記-清除算法:
在這裏插入圖片描述
標記-清除算法,分爲兩個階段,先標記,再清除。標記就是把所有可以進行回收的對象標記一下,標記完成後,就可以進行統一的回收。
這個算法有兩個缺點,一是標記和清除的性能不高,二是容易產生內存碎片。所以這個算法算是一個基礎的算法,其他算法會根據這個算法進行改進。

複製算法:
在這裏插入圖片描述
複製算法把內存分爲兩塊,一次只用一塊,回收時就把其中一塊存活的對象複製到另一塊。再把這一塊的空間清除。這種方法簡單高效,而且不存在碎片問題,但是成本較大,因爲只用了一半的內存。改進的方法是按照8:1:1的比例,分配三塊空間。最大的這個是Eden,兩塊小的是Survivor,每次使用一塊Eden和一塊Survivor,回收時,把Eden和Survivor的存活對象複製到另一塊Survivor上。這種算法適合新生代,因爲新生代對象大部分都是朝生夕死的,所以一般不會有問題。如果真的遇到了Survivor空間不夠用的情況,則需要擔保機制,把存活的對象直接進入老年代。

標記-整理算法:
在這裏插入圖片描述
標記-整理類似於標記-清除,也是先標記,只不過標記完之後,不是清除,而是把存活的對象往一端去移動,移動完之後,直接把邊界後面的空間全都清除。這種方式也可以減少內存碎片。

分代收集算法:
現在一般商用的JVM都會採用分代收集的算法,就是把堆分成新生代和老年代,不同的空間用不同的回收算法。對於新生代,大部分對象都是朝生夕死的,所以就用複製算法。對於老年代,對象的存活率較高,也沒有擔保機制,就用標記-整理或者標記-清除。

常見的垃圾收集器
在這裏插入圖片描述
Serial是最基本的收集器,適用於新生代,採用複製算法,單線程進行回收,回收的過程中,需要暫停所有的用戶線程,直到回收結束。
Serial Old是老年代收集器,也是單線程的,使用的標記-整理算法。
ParNew:相當於Serial的多線程版本,除了回收的時候用多線程以外,其他和Serial都一樣的,也需要暫停所有的用戶線程。不過ParNew的用途比較廣泛,因爲只有Serial和ParNew可以和CMS進行搭配。
Parallel Scavenge是個新生代收集器,用的複製算法,多線程並行回收。看起來和ParNew是一樣的,不過他們兩個收集器的關注點不同。ParNew關注停頓時間,讓停頓時間儘可能短。Parallel Scavenge關注吞吐量。
Parallel Old,是老年代的收集器,使用標記-整理算法。和Parallel Scavenge搭配使用,適合後臺程序,和用戶沒有交互的,這樣可以保證有較高的吞吐量。
CMS:老年代收集器,採用標記-清除算法,關注點在於減少停頓時間,所以適合用戶交互的系統。
在這裏插入圖片描述
CMS分爲四個步驟:初始標記、併發標記、重新標記、併發清除。

初始標記個重新標記這兩個步驟需要stop the world,但是這兩個步驟耗時一般會比較短。併發標記和併發清除這兩個步驟,都是可以和用戶線程併發執行的。

CMS是一款優秀的垃圾收集器,很多系統都會採用這個收集器。不過也有一些缺點,比如會產生內存碎片。

G1收集器:既可以新生代,也可以老年代,它的定位就是爲了取代CMS。G1把堆劃分爲多個Region,每個Region都可以充當 Eden 區、Survivor 區或者老年代中的一個。先回收價值大的Region,避免了在整個堆空間進行回收。從整體上看,G1採用的是標記-整理算法。

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