Java 垃圾收集技術
前言
在計算機科學中,垃圾收回(GC: garbage collection)是記憶體自動管理的一種方式,它并不是同 Java 語言一起誕生的,實際上,早在 1959 年為了簡化 Lisp 語言的手動記憶體管理,該語言的作者就開始使用了記憶體自動管理技術。 垃圾收集和手動記憶體管理剛好相反,後者需要程式設計人員自己去指定需要釋放的對象然後将記憶體歸還給作業系統,而前者不需要關心給對象配置設定的記憶體回收問題。Java 語言使用自動垃圾收集器來管理對象生命周期中的記憶體,要進行垃圾收集首先需要明确三個問題:1. 哪些記憶體需要回收、2. 什麼時候進行回收、3. 怎麼進行記憶體回收。接下來讓我們一起看看 Java 語言對這些問題是如何處理的。
哪些記憶體需要回收
為了友善管理和跨平台,Java 虛拟機規範規定在執行 Java 程式的時候把它所管理的記憶體劃分為若幹個不同的資料區域。這些區域都有着各自不同的用途以及建立和銷毀的時間,有的資料區域随着使用者線程的啟動和結束而建立和銷毀,有的區域會随着虛拟機程序的啟動和停止而存在和銷毀。更多有關運作時資料區域的内容請看 Java 運作時資料區域。
由于 Java 運作時資料區域中的 程式計數器、虛拟機棧和本地方法棧和線程的生命周期一緻,随線程的啟動和結束而建立和銷毀。而且當我們的類結構确定了之後,在編譯期間,一個棧幀需要配置設定記憶體的大小基本上也就确定下來了,這三個區域的記憶體配置設定和收回都是具備确定性的,不需要我們過多的去考慮記憶體回收問題。主要考慮Java 堆和方法區的記憶體回收的問題。
什麼時候進行回收
在 Java 語言中,一個對象的生命周期分為以下三個階段:
對象建立階段 通常我們使用 new 關鍵字進行對象建立 e.g. Object obj = new Object();,當我們建立對象時,Java 虛拟機将配置設定一定大小的記憶體來存儲該對象,配置設定的記憶體量可能會根據虛拟機廠商的不同而有所不同。
對象使用階段 在這個階段,對象被應用程式的其它對象使用(其它活動對象擁有指向它的引用)。在使用期間,該對象會一直駐留在記憶體當中,并且可能包含對其它對象的引用。
對象銷毀階段 垃圾收集系統監視對象,如果發現對象不被任何對象引用了,則進行該對象記憶體回收操作。
那麼問題來了,該如何去判斷一個對象有沒有被引用呢?目前,主要有兩種判斷對象是否存活的算法,分别是 引用計數算法(Reference counting algorithm)和可達性分析算法(Accessibility analysis algorithm)。
引用計數算法
首先我們看看引用計數算法是如何判斷的,該算法的主要思想就是給每個對象都添加一個引用計數器,當該對象被變量或者另一個對象引用時該計數器值就會加 1,同時當對象的一個引用無效時,對象計數器的值會相應的減 1。當對象引用計數器的值為 0 時,說明該對象已經不再被引用了,那麼就可以銷毀對象進行記憶體回收操作了。這個算法的實作比較簡單,對象是否“存活”的判斷效率也比較高,這個算法看起來确實不錯,但是它有個緻命的缺點就是:無法解決對象間互相引用的問題。互相引用簡單來說就是,有兩個對象 object1 和 object2 都有一個引用類型字段 ref,并且做了如下指派操作:
object1.ref = object2;
object2.ref = object1;
這兩個對象除了上面這個指派之外,不被其它任何對象引用,實際上這兩個對象都不可能再被通路了,但是因為它們倆都互相引用了對方,導緻引用計數器不為 0,導緻使用引用計數器算法的 垃圾收集器 無法收集它們,它們就會一直存在于記憶體之中直到虛拟機程序結束。正是因為這個原因,市場上主流的 Java 虛拟機大部分都沒有選用這個算法來管理記憶體,下面介紹的 可達性分析算法 就可以很好的避免了對象間互相引用的問題。
可達性分析算法
Java 虛拟機是通過可達性分析算法來判斷對象是否存活的,該算法的主要思想是将一系列稱為 GC Root 的對象作為起點,向下進行搜尋,搜尋經過的路徑稱為引用鍊(Reference chain),當一個對象到 GC Root 對象沒有任何引用鍊的時候,則表示該對象是不可達的,可以對其進行記憶體回收。
在 Java 虛拟機中,規定以下幾種情況可以作為 GC Root 對象:
虛拟機棧中引用的對象
方法區中類靜态屬性引用的對象
方法區中常量引用的對象
本地方法棧中 Native 方法引用的對象
怎麼進行記憶體回收
當我們建立的對象不可達之後,Java 虛拟機會在背景自動去收集回收不可達對象的記憶體,自 Java 語言誕生以來,在垃圾收集算法上進行了許多更新,主要有标記-清除算法(Mark and sweep algorithm)、複制算法(Copying algorithm)、标記—整理算法(Mark and compact algorithm)和分代收集算法(Generational collection algorithm),根據這些算法實作的垃圾收集器在背景默默運作以釋放記憶體,下面讓我們看看它們是如何工作的。
标記-清除算法(mark and sweep algorithm)
标記—清除算法是初始且非常基本的算法,主要分為以下兩個階段:
标記需要回收對象,找出程式中所有需要回收的對象并标記。
清除所有标記對象,在标記完成後統一回收被标記對象。
首先标記出需要回收的對象,标記完成後再統一回收被标記對象。這個算法是最基礎的垃圾收集算法,後面将要介紹的幾個算法都是在它的基礎上優化改進的,算法主要有兩個不足的地方:① 效率不高,标記和清除過程的效率都不高。② 空間使用率不高,标記清除之後會産生大量不連續的記憶體碎片,後面如果要配置設定大對象的時候由于連續記憶體不足可能會再次觸發垃圾收集操作。
複制算法(copying algorithm)
複制算法就是為了解決标記—清除算法的效率問題的,主要思想就是将可用的記憶體分為大小相等的兩個部分,每一次都隻使用其中的一塊,當這塊記憶體使用完了之後,就将依然存活的對象複制到另一塊記憶體上去,然後再把這塊含有可回收對象的記憶體清理掉,這樣每次都是清理一半的連續記憶體了,就不會存在記憶體碎片的情況。但是這個算法的缺點也很明顯,它把可用記憶體的大小縮小到了一半。
标記-整理算法(mark and compact algorithm)
如果對象的存活率比較低的情況下,上面介紹的複制算法效率還是很高的,畢竟隻要複制少部分存活對象到另一塊記憶體中即可,但是當對象的存活率比較高時就會進行多次複制操作。比如老年代,老年代的對象是經過多次垃圾回收依然存活的對象,對象的存活率相對來說比較高,根據老年代的這個特點,于是針對這種情況就有了另一個算法稱之為标記-整理算法,主要思想和其名字一樣也是分為标記和整理兩個階段,第一個标記階段依然和标記—清除算法一樣,後面的第二個整理階段就不是直接對可回收對象進行清理了,而是讓所有存活的對象都向記憶體的同一側移動,然後就直接清除掉另一側的記憶體。
分代收集算法(generational collection algorithm)
根據不同分代的特點,現在商業上的虛拟機針對不同的分代采取适合的垃圾收集,一般是把 Java 堆分為新生代和老年代。在新生代中,對象大部分存活時間都很短每次垃圾收集都會有很多的對象被清除,隻有少部分對象可以存活下來,那麼此時就可以使用複制算法,隻需要複制出少部分存活的對象即可效率高。然而在老年代中大部分對象的存活時間比較長,則需采用标記-清除算法或者标記-整理算法來進行垃圾收集。
垃圾收集算法對于垃圾回收來說類似于我們程式中的接口,是一套垃圾回收的指導算法,算法的具體實作我們稱之為垃圾收集器。但是 Java 虛拟機規範中并沒有對垃圾收集器的實作有任何規定。是以不同的廠商和不同版本的虛拟機實作的垃圾收集器也不一樣,不過一般都會提供一些配置參數來讓使用者根據自身情況來設定所需的垃圾收集器。
JVM 相關 GC 配置
Java 虛拟機部分垃圾收集(Garbage Collection,GC)相關配置如下
參數 描述
-Xms2048m 設定初始堆大小(新生代 + 老年代)
-XX:InitialHeapSize=3g 設定初始堆大小(新生代 + 老年代)
-Xmx3g 設定最大堆大小(新生代 + 老年代)
-XX:MaxHeapSize=3g 設定最大堆大小(新生代 + 老年代)
-XX:NewSize=128m 設定堆初始新生代大小
-XX:MaxNewSize=128m 設定堆最大新生代大小
-XX:PermSize=512m(JDK 1.7) 設定初始永久代(元空間)大小
-XX:MetaspaceSize=512m(JDK 1.8+) 設定初始永久代(元空間)大小
-XX:MaxPermSize=1g(JDK 1.7) 設定最大永久代(元空間)大小
-XX:MaxMetaspaceSize=1g(JDK 1.8+) 設定最大永久代(元空間)大小
-XX:+DisableExplicitGC 忽略應用程式對 System.gc() 方法的任何調用
-XX:+PrintGCDetails 列印輸出 GC 收集相關資訊
參考文章
深入了解Java虛拟機(第2版)
Garbage collection (computer science)
The Java® Virtual Machine Specification(Java SE 8 Edition)
原文位址
https://www.cnblogs.com/mghio/p/12539566.html