垃圾收集器關注的是Java堆和方法區這部分記憶體。
GC大概需要關注的事情有:
哪些記憶體需要回收
什麼時候回收
怎樣回收
複製程式碼
如何判斷物件是否存活
引用計數演算法
給一個物件新增一個引用計數器,每當有個地方引用時,計數器加1;引用失效時,計數器減1;引用計數器為0的物件不可能再被使用。
優點
實現簡單,判斷效率高。
複製程式碼
缺點
難解決物件之間存在迴圈引用的場景。
複製程式碼
主流的Java虛擬機器沒有用引用計數器
可達性分析演算法
以GC Roots物件作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑為引用鏈。從GC Roots到這個物件不可達時,說明此物件不可用,會被判斷為可回收的物件。
可作為GC Roots的物件
虛擬機器棧中引用的物件【棧幀中的本地變數表】
方法區中類靜態屬性引用的物件
方法區中常量引用的物件
本地方法棧中引用的物件
複製程式碼
引用
JDK1.2之後Java對引用做了進一步的細分。由強到弱依次分為:強引用、軟引用、弱引用、虛引用。
強引用
普遍存在的,類似於
Object obj = new Object();
複製程式碼
只要強引用還存在,垃圾收集器就不會回收被引用的物件
我們可以將物件的引用顯示地置為null:o=null; 【可以幫助垃圾收集器回收此物件】
軟引用
描述一些可能用到的物件但是非必需的。對於這種引用,在記憶體充足的時候垃圾回收器不會回收他,在記憶體不足的時候會回收。
軟引用非常適合於建立快取。當系統記憶體不足的時候,快取中的內容是可以被釋放的。
在Java中用java.lang.ref.SoftReference類來表示。
// 獲取物件並快取
Object object = new Object();
SoftReference softRef = new SoftReference(object);
// 從軟引用中獲取物件
Object object = (Object) softRef.get();
if (object == null){
// 當軟引用被回收後重新獲取物件
object = new Object();
}
複製程式碼
弱引用
弱引用用來描述非必需物件,其強度比軟引用更弱。被弱引用關聯的物件只能活到下次垃圾收集器回收之前,不管記憶體是否充足。
如果這個物件是偶爾的使用,並且希望在使用時隨時就能獲取到,但又不想影響此物件的垃圾收集,那麼你應該用 Weak Reference 來記住此物件。 可以解決記憶體洩露問題。
例如物件池、快取中的過期物件都有可能引發記憶體洩露的問題。用 WeakHashMap 來作為快取的容器可以有效解決這一問題。WeakHashMap 和 HashMap 幾乎一樣,唯一的區別就是它的鍵使用弱引用。當 WeakHashMap 的鍵標記為過期時,這個鍵對應的條目就會自動被移除。這就避免了記憶體洩漏問題。
虛引用
虛引用也稱為幻影引用,是最弱的一種引用方式。
一個物件是都有虛引用的存在都不會對生存時間都構成影響,也無法通過虛引用來獲取對一個物件的真實引用。
唯一的用處:能在物件被GC時收到系統通知,JAVA中用PhantomReference來實現虛引用。
String temp = "hello world";
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> phReference = new PhantomReference<String>(temp, queue);
System.out.println(phReference.get());//get返回null
複製程式碼
不同分類的引用,讓記憶體管理更容易,不同的物件例項有不同的回收管理方式
物件是死是活
對物件進行可達性分析發現他到GC ROOTS沒有可達的引用鏈,就會作為GC回收的物件,如果到GC Roots可達,那麼就還沒死,不會回收。
但是即使到GC Roots物件不可達,物件也還有自我救贖的機會,也並非死亡。
如果重寫了finalize方法,並且重新指向該物件,該物件還是存活,不會死亡。如果這個自我救贖的機會也錯失,那麼一般都會被回收掉。
public class FinalizeTest {
public static FinalizeTest testFinalize = null;
@Override
public void finalize() throws Throwable {
super.finalize();
System.out.println("正在執行finalize方法~!");
//自救
testFinalize = this;
}
public static void main(String[] args) throws InterruptedException {
testFinalize = new FinalizeTest();
//物件第一次成功拯救自己
testFinalize = null;
System.gc();
//因為finalize()方法優先順序很低,所以暫停1S等待它
Thread.sleep(1000);
//finalize()方法確實被GC觸發了,但是收集前成功逃脫了。
if (testFinalize != null) {
System.out.println("alive~!");
} else {
System.out.println("dead~!");
}
//物件第二次成功拯救自己未遂,因為任何一個物件的finalize()只會被系統呼叫一次。
testFinalize = null;
System.gc();
//因為finalize()方法優先順序很低,所以暫停1S等待它
Thread.sleep(1000);
if (testFinalize != null) {
System.out.println("alive~!");
} else {
System.out.println("dead~!");
}
}
}
複製程式碼
finalize方法實際中一般不會使用,執行代價大,不確定性大,可以用try-finally更好的關閉外部資源。
回收方法區
永久代主要可回收的兩部分分別是:
廢棄的常量
無用的類
複製程式碼
無用類要同時滿足3個條件:
該類的所有例項都被回收掉
載入該類的ClassLoader被回收
對應的Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類方法
複製程式碼
HotSpot虛擬機器提供了-Xnoclassgc引數進行控制是否回收無用。
還可以使用-verbose:class及-XX:+TraceClassLoading、 -XX:+TraceClassUnLoading檢視類的載入和解除安裝資訊。
垃圾收集演算法
標記-清除演算法【mark-sweep】
原理
1、標記階段。從根集合開始掃描,標記存活的物件
2、清除階段。掃描整個記憶體空間,回收未被標記的物件,使用free-list記錄被釋放的區域
優點
實現簡單
不需要額外的空間
複製程式碼
缺點
效率問題,標記、清除兩個階段掃描效能不高
會產生記憶體碎片。分配大物件時,無法找到匹配的記憶體,會導致另一次垃圾收集的觸發
複製程式碼
使用場景
針對老年代的CMS收集器;
複製程式碼
複製演算法
原理
從根集合開始掃描,從一塊記憶體中找到存活的物件,複製到另一塊空閒的記憶體中,然後回收第一塊記憶體中的物件。下次這兩塊記憶體交換身份。
優點
實現簡單,執行高效,沒有標記
沒有記憶體碎片
複製程式碼
缺點
記憶體使用縮小為原來的一半
複製程式碼
使用場景
現在商業JVM都採用這種演算法=來回收新生代;
Serial收集器、ParNew收集器、Parallel Scavenge收集器、G1;
複製程式碼
標記整理演算法
原理
1、標記階段。和標記清除演算法的一樣
2、整理階段。讓所有存活的物件向一端移動,然後直接清理掉端外界邊的記憶體
優點
沒有記憶體碎片
複製程式碼
缺點
需要移動物件,增加成本
複製程式碼
使用場景
很多垃圾收集器採用這種演算法來回收老年代;
如Serial Old收集器、G1;
複製程式碼
分代收集演算法
原理
當前商業虛擬機器基本上都是採用分代垃圾回收演算法來回收垃圾,思想也很簡單,就是根據物件的生命週期將記憶體劃分,然後進行分割槽管理,根據各個年代的特點採用最合適的收集演算法。
一般把Java堆分為新生代和老年代;
新生代
每次垃圾收集都有大批物件死去,只有少量存活;
所以可採用複製演算法;
複製程式碼
老年代
物件存活率高,沒有額外的空間可以分配擔保;
使用"標記-清理"或"標記-整理"演算法;
複製程式碼
優點
根據各個年代的特點採用最適當的收集演算法
複製程式碼
缺點
仍然不能控制每次垃圾收集的時間;
複製程式碼
使用場景
目前幾乎所有商業虛擬機器的垃圾收集器都採用分代收集演算法;
如HotSpot虛擬機器中全部垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
複製程式碼
垃圾收集器
JVM在進行GC時,並非每次都對新生代、舊生代、永久代一起回收的,大部分時候回收的都是指新生代。因此GC按照回收的區域又分了兩種型別,一種是普通GC(minor GC),一種是全域性GC(major GC or Full GC),它們所針對的區域如下。
普通GC(minor GC):只針對新生代區域的GC。
全域性GC(major GC or Full GC):針對年老代的GC,偶爾伴隨對新生代的GC以及對永久代的GC。
複製程式碼
由於年老代與永久代相對來說GC效果不好,而且二者的記憶體使用增長速度也慢,因此一般情況下,需要經過好幾次普通GC,才會觸發一次全域性GC。
新生代的收集器們
Serial收集器
單執行緒的收集器,進行垃圾收集時,必須暫停其他的工作執行緒,也就是“Stop The World”。
使用序列回收;複製演算法
使用場景
單CPU、新生代小、對暫停時間要求不高的應用
Client模式下的預設新生代收集器
複製程式碼
引數控制
-XX:+UseSerialGC 序列收集器
複製程式碼
ParNew收集器
ParNew收集器就是Serial收集器的多執行緒版本。通過多執行緒掃描並壓縮堆。
新生代並行,老年代序列;新生代複製演算法、老年代標記-整理
使用場景
Server模式下虛擬機器中首選的新生代收集器
複製程式碼
引數控制:
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制執行緒數量
複製程式碼
Parallel Scavenge收集器
新生代收集器,使用複製演算法,多個執行緒來通過掃描並壓縮堆。
Parallel Scavenge收集器的目標是達到一個可控制的吞吐量,也稱吞吐量優先的收集器。
吞吐量 = 執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集器時間)
複製程式碼
高吞吐量可以高效的利用CPU,儘快完成程式任務,適合在後臺運算而不需要太多互動任務。
使用場景
多CPU、對暫停時間要求比較短的應用
Server模式上的預設選擇
複製程式碼
控制引數
-XX:MaxGCPauseMillis 最大垃圾收集停頓時間
-XX:GCTimeRatio 設定吞吐量大小
-XX:+UseAdaptiveSizePolicy GC自適應的調節策略,把記憶體管理的調優任務交給虛擬機器去完成
複製程式碼
老年代的收集器們
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,使用標記-整理演算法
使用場景
這個收集器主要在於給Client模式下的虛擬機器使用。
如果在Server中,主要用途是:1,在JDK1.5前和Parallel Scavenge搭配使用。2,作為Concurrent Mode Failure時候使用。
複製程式碼
Parallel Old
Parallel Scanvenge收集器的老年隊收集器,使用標記-整理方式。
在這個方式沒有產生之前,Parallel Scavenge只能選擇Serial Old。
由於被拖了後腿,那麼Parallel Scavenge並不能在整體上獲取吞吐量最大化的效果。甚至比不上CMS+ParNew的吞吐量。
CMS收集器
併發標記-清除演算法。 以獲取最短回收停頓時間為目標的收集器
標記清除過程
初始化標記-stop the world【簡單標記下GC Roots能直接關聯到的物件】
併發標記-耗時【進行GC Roots Tracing 】
重新標記-stop the world【修正併發標記期間使用者程式繼續執行而導致標記發生變動那一部分對 象標記記錄】
併發清除-耗時
複製程式碼
缺點:
無法處理浮動垃圾
對CPU資源敏感
會產生大量的空間碎片
複製程式碼
使用場景
重視伺服器響應速度的應用
複製程式碼
引數控制
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC後,進行一次碎片整理;整理過程是獨佔的,會引起停頓時間變長
-XX:+CMSFullGCsBeforeCompaction 設定進行幾次Full GC後,進行一次碎片整理
-XX:ParallelCMSThreads 設定CMS的執行緒數量(一般情況約等於可用CPU數量)
複製程式碼
G1收集器
G1收集器時面向服務端應用的垃圾收集器。
在G1中,堆被劃分成 許多個連續的區域(region)。採用G1演算法進行回收,吸收了CMS收集器特點。
支援很大的堆,高吞吐量
可配置在N毫秒內最多隻佔用M毫秒的時間進行垃圾回收
特點
並行和併發
分代收集
空間整合【整體上:標記-整理演算法,區域性上:複製演算法】
可預測停頓【可配置在N毫秒內最多隻佔用M毫秒的時間進行垃圾回收】
複製程式碼
執行過程
初始標記;【標記GC Roots直接關聯的物件,stw】
併發標記;【進行GC Roots Tracing 】
最終標記;【再標記,會有短暫停頓(STW)。再標記階段是用來收集 併發標記階段 產生新的垃圾】
篩選回收【stw】
複製程式碼
使用場景
需要大堆空間、限制的垃圾回收延遲的應用
複製程式碼
引數控制
–XX:+UseG1GC 使用G1垃圾回收器
複製程式碼