1、前言 理解JVM的垃圾回收機制(簡稱GC)有什麼好處呢?作為一名軟體開發者,滿足自己的好奇心將是一個很好的理由,不過更重要的是,理解GC工作機制可以幫助你寫出更好的Java程式。 在學習GC前,你應該知道一個技術名詞:“stop the world” ,無論你選擇哪種GC演算法,“stop the ...
1、前言
理解JVM的垃圾回收機制(簡稱GC)有什麼好處呢?作為一名軟體開發者,滿足自己的好奇心將是一個很好的理由,不過更重要的是,理解GC工作機制可以幫助你寫出更好的Java程式。
在學習GC前,你應該知道一個技術名詞:“stop-the-world” ,無論你選擇哪種GC演算法,“stop-the-world”都會發生。“stop-the-world”意味著JVM停止應用程式,而去進行垃圾回收。當“stop-the-world”發生時,除了進行垃圾回收的線程,其他所有線程都將停止運行。被中斷的任務將在GC任務完成後恢復執行。GC調優往往意味著減少“stop-the-world”的時間。
2、分代垃圾收集機制
在HotSpot虛擬機中,將記憶體分為 年輕代(young generation)、老年代(old generation) 和 永久代(permanent generation)
我們看一下這幅圖:
年輕代: 新創建的對象都存放在這裡。因為大多數對象很快變得不可達,所以大多數對象在年輕代中創建,然後消失。當對象從這塊記憶體區域消失時,我們說發生了一次“minor GC”。
老年代: 沒有變得不可達,存活下來的年輕代對象被覆制到這裡。這塊記憶體區域一般大於年輕代。因為它更大的規模,GC發生的次數比在年輕代的少。對象從老年代消失時,我們說“major GC”(或“full GC”)發生了。
永久代: 永久代(permanent generation) 也稱為“方法區(method area)”,它存儲class對象和字元串常量。所以這塊記憶體區域絕對不是永久的存放從老年代存活下來的對象的。發生在這裡的垃圾回收也被稱為major GC。
3、垃圾收集演算法
垃圾收集演算法如下:
- 年輕代-複製演算法
- 老年代-標記清除演算法
- 老年代-標記整理演算法
- 永久代-方法區回收
年輕代-複製演算法
該演算法的核心是將可用記憶體按容量劃分為大小相等的兩塊,每次只用其中一塊,當這一塊的記憶體用完,就將還存活的對象複製到另外一塊上面,然後把已使用過的記憶體空間一次清理掉。這使得每次只對其中一塊記憶體進行回收,分配也就不用考慮記憶體碎片等複雜情況,實現簡單且運行高效。如下圖:
老年代-標記清除演算法
該演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象(可達性分析),在標記完成後統一清理掉所有被標記的對象。如下圖:
該演算法會有以下兩個問題:
- 效率問題:標記過程和清除過程的效率都不高;
- 空間問題:標記清除後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致在運行過程中需要分配較大對象時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集。
老年代-標記整理演算法
標記清除演算法會產生記憶體碎片,而複製演算法需要有額外的記憶體擔保空間,於是針對老年代的特點,又有了標記整理演算法。標記整理演算法的標記過程與標記清除演算法相同,但後續步驟不再對可回收對象直接清理,而是讓所有存活的對象都向一端移動,然後清理掉這一端邊界以外的記憶體。如下圖:
永久代-方法區回收
在方法區進行垃圾回收一般“性價比”較低,因為在方法區主要回收兩部分內容:廢棄常量 和 無用的類 。
回收廢棄常量與回收其他年代中的對象類似。
但是判斷一個類是不是無用的類的條件則相當苛刻:
- 該類所有的實例都已經被回收,Java堆中不存在該類的任何實例;
- 該類對應的Class對象沒有在任何地方被引用;
- 載入該類的ClassLoader已經被回收;
但即使滿足以上條件也未必一定會回收,Hotspot VM還提供了-Xnoclassgc參數控制(關閉CLASS的垃圾回收功能)。因此在大量使用動態代理、CGLib等位元組碼框架的應用中一定要關閉該選項,開啟VM的類卸載功能,以保證方法區不會溢出。
4、垃圾回收的兩個重要方法
System.gc()方法 和 finalize()方法
System.gc()方法
使用System.gc() 可以不管JVM使用的是哪一種垃圾回收的演算法,都可以請求Java的垃圾回收。在命令行中有一個參數-verbosegc
可以查看Java使用的堆記憶體的情況,它的格式如下:java -verbosegc classfile
由於這種方法會影響系統性能,不推薦使用,所以不詳細介紹。
finalize()方法
JVM垃圾回收器在回收一個對象之前,一般要求程式調用適當的方法釋放資源,但在沒有明確釋放資源的情況下,Java提供了預設機制來終止該對象從而釋放資源,這個方法就是finalize() 。它的原型為:protected void finalize() throws Throwable
在finalize() 方法返回之後,對象消失,垃圾收集開始執行。原型中的throws Throwable 表示它可以拋出任何類型的異常。
之所以要使用finalize(),是因為存在著垃圾回收器不能處理的特殊情況。例如:
由於在分配記憶體的時候可能採用了類似 C語言的做法,而非Java通常的new 。這種情況主要發生在 native 方法中,比如 native 方法調用了
C/C++
的malloc()
函數來分配存儲空間,除非調用 free() 函數,否則這些記憶體空間將不會得到釋放,那麼這個時候就可能造成記憶體泄漏。但是由於 free() 是C/C++
中的函數,所以 finalize() 中可以用本地方法來調用它。以釋放這些“特殊”的記憶體空間。或者是打開的文件資源,這些資源不屬於垃圾回收器的回收範圍。
5、觸發GC(Garbage Collector)的條件
GC在優先順序最低的線程中運行,一般在應用程式空閑即沒有應用線程在運行時被調用。但下麵的條件例外。
Java堆記憶體不足時,GC會被調用。當應用線程在運行,併在運行過程中創建新對象,若這時記憶體空間不足,JVM就會強制調用GC線程。若GC一次之後仍不能滿足記憶體分配,JVM會再進行兩次GC,若仍無法滿足要求,則JVM將報“out of memory”的錯誤,Java應用將停止。
6、減少GC開銷的措施
不要顯式調用System.gc() 。此函數建議JVM進行主GC,雖然只是建議而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數。大大的影響系統性能。
儘量減少臨時對象的使用。臨時對象在跳出函數調用後,會成為垃圾,少用臨時變數就相當於減少了垃圾的產生,從而延長了出現上述第二個觸發條件出現的時間,減少了主GC的機會。
對象不用時最好顯式置為Null。一般而言,為Null的對象都會被作為垃圾處理,所以將不用的對象顯式地設為Null,有利於GC收集器判定垃圾,從而提高了GC的效率。
儘量使用StringBuffer,而不用String來累加字元串。由於String是固定長的字元串對象,累加String對象時,並非在一個String對象中擴增,而是重新創建新的String對象,如
Str5=Str1+Str2+Str3+Str4;
這條語句執行過程中會產生多個垃圾對象,因為每次作“+”操作時都必須創建新的String對象,但這些過渡對象對系統來說是沒有實際意義的,只會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字元串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。能用基本類型如
int, long
就不用包裝類型Integer, Long
。基本類型變數占用的記憶體資源比包裝類型占用的少得多,如果沒有必要,最好使用基本變數。儘量少用靜態對象變數。靜態變數屬於全局變數,不會被GC回收,它們會一直占用記憶體。
分散對象創建或刪除的時間。集中在短時間內大量創建新對象,特別是大對象,會導致突然需要大量記憶體,JVM在面臨這種情況時,只能進行主GC,以回收記憶體或整合記憶體碎片,從而增加主GC的頻率。集中刪除對象,道理也是一樣的。它使得突然出現了大量的垃圾對象,空閑空間必然減少,從而大大增加了下一次創建新對象時強制主GC的機會。
7、幾種垃圾收集器
在JDK7中,有5種垃圾收集器:
- Serial收集器
- Parallel收集器
- Parallel Old收集器 (Parallel Compacting GC)收集器
- Concurrent Mark & Sweep GC (or “CMS”)收集器
- Garbage First (G1) 收集器
其中,Serial 收集器一定不能用於伺服器端。這個收集器類型僅應用於單核CPU桌面電腦。使用Serial收集器會顯著降低應用程式的性能。