Java 中的垃圾回收一般是在 Java 堆中進行,因為堆中幾乎存放了 Java 中所有的對象實例。談到 Java 堆中的垃圾回收,自然要談到引用。在 JDK1.2 之前,Java 中的引用定義很很純粹:如果 reference 類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表著一個引用。但在 JDK1.2 之后,Java 對引用的概念進行了擴充,將其分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強度依次減弱。
Java 堆中存放著幾乎所有的對象實例,垃圾收集器對堆中的對象進行回收前,要先確定這些對象是否還有用,判定對象是否為垃圾對象有如下算法:
引用計數算法
給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加 1,當引用失效時,計數器值就減1,任何時刻計數器都為 0 的對象就是不可能再被使用的。
引用計數算法的實現(xiàn)簡單,判定效率也很高,在大部分情況下它都是一個不錯的選擇,當 Java 語言并沒有選擇這種算法來進行垃圾回收,主要原因是它很難解決對象之間的相互循環(huán)引用問題。
根搜索算法
Java 和 C# 中都是采用根搜索算法來判定對象是否存活的。這種算法的基本思路是通過一系列名為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連時,就證明此對象是不可用的。在 Java 語言里,可作為 GC Roots 的兌現(xiàn)包括下面幾種:
實際上,在根搜索算法中,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行根搜索后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那它會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執(zhí)行 finalize()方法。當對象沒有覆蓋 finalize()方法,或 finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為沒有必要執(zhí)行。如果該對象被判定為有必要執(zhí)行 finalize()方法,那么這個對象將會被放置在一個名為 F-Queue 隊列中,并在稍后由一條由虛擬機自動建立的、低優(yōu)先級的 Finalizer 線程去執(zhí)行 finalize()方法。finalize()方法是對象逃脫死亡命運的最后一次機會(因為一個對象的 finalize()方法最多只會被系統(tǒng)自動調用一次),稍后 GC 將對 F-Queue 中的對象進行第二次小規(guī)模的標記,如果要在 finalize()方法中成功拯救自己,只要在 finalize()方法中讓該對象重引用鏈上的任何一個對象建立關聯(lián)即可。而如果對象這時還沒有關聯(lián)到任何鏈上的引用,那它就會被回收掉。
判定除了垃圾對象之后,便可以進行垃圾回收了。下面介紹一些垃圾收集算法,由于垃圾收集算法的實現(xiàn)涉及大量的程序細節(jié),因此這里主要是闡明各算法的實現(xiàn)思想,而不去細論算法的具體實現(xiàn)。
標記—清除算法是最基礎的收集算法,它分為“標記”和“清除”兩個階段:首先標記出所需回收的對象,在標記完成后統(tǒng)一回收掉所有被標記的對象,它的標記過程其實就是前面的根搜索算法中判定垃圾對象的標記過程。標記—清除算法的執(zhí)行情況如下圖所示:
回收前狀態(tài):
回收后狀態(tài):
復制算法比較適合于新生代,在老年代中,對象存活率比較高,如果執(zhí)行較多的復制操作,效率將會變低,所以老年代一般會選用其他算法,如標記—整理算法。該算法標記的過程與標記—清除算法中的標記過程一樣,但對標記后出的垃圾對象的處理情況有所不同,它不是直接對可回收對象進行清理,而是讓所有的對象都向一端移動,然后直接清理掉端邊界以外的內存。標記—整理算法的回收情況如下所示:
回收前狀態(tài):
回收后狀態(tài):
當前商業(yè)虛擬機的垃圾收集 都采用分代收集,它根據對象的存活周期的不同將內存劃分為幾塊,一般是把 Java 堆分為新生代和老年代。在新生代中,每次垃圾收集時都會發(fā)現(xiàn)有大量對象死去,只有少量存活,因此可選用復制算法來完成收集,而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。
垃圾收集器是內存回收算法的具體實現(xiàn),Java 虛擬機規(guī)范中對垃圾收集器應該如何實現(xiàn)并沒有任何規(guī)定,因此不同廠商、不同版本的虛擬機所提供的垃圾收集器都可能會有很大的差別。Sun HotSpot 虛擬機 1.6 版包含了如下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old。這些收集器以不同的組合形式配合工作來完成不同分代區(qū)的垃圾收集工作。
垃圾回收分析
在用代碼分析之前,我們對內存的分配策略明確以下三點:
對垃圾回收策略說明以下兩點:
下面我們來看如下代碼:
public class SlotGc{
public static void main(String[] args){
byte[] holder = new byte[32*1024*1024];
System.gc();
}
}
代碼很簡單,就是向內存中填充了 32MB 的數據,然后通過虛擬機進行垃圾收集。在 javac 編譯后,我們執(zhí)行如下指令:java -verbose:gc SlotGc 來查看垃圾收集的結果,得到如下輸出信息:
[GC 208K->134K(5056K), 0.0017306 secs]
[Full GC 134K->134K(5056K), 0.0121194 secs]
[Full GC 32902K->32902K(37828K), 0.0094149 sec
注意第三行,“->”之前的數據表示垃圾回收前堆中存活對象所占用的內存大小,“->”之后的數據表示垃圾回收堆中存活對象所占用的內存大小,括號中的數據表示堆內存的總容量,0.0094149 sec 表示垃圾回收所用的時間。
從結果中可以看出,System.gc()運行后并沒有回收掉這 32MB 的內存,這應該是意料之中的結果,因為變量holder 還處在作用域內,虛擬機自然不會回收掉 holder 引用的對象所占用的內存。
我們把代碼修改如下:
public class SlotGc{
public static void main(String[] args){
{
byte[] holder = new byte[32*1024*1024];
}
System.gc();
}
}
加入花括號后,holder 的作用域被限制在了花括號之內,因此,在執(zhí)行System.gc()時,holder 引用已經不能再被訪問,邏輯上來講,這次應該會回收掉 holder 引用的對象所占的內存。但查看垃圾回收情況時,輸出信息如下:
[GC 208K->134K(5056K), 0.0017100 secs]
[Full GC 134K->134K(5056K), 0.0125887 secs]
[Full GC 32902K->32902K(37828K), 0.0089226 secs]
很明顯,這 32MB 的數據并沒有被回收。下面我們再做如下修改:
public class SlotGc{
public static void main(String[] args){
{
byte[] holder = new byte[32*1024*1024];
holder = null;
}
System.gc();
}
}
這次得到的垃圾回收信息如下:
[GC 208K->134K(5056K), 0.0017194 secs]
[Full GC 134K->134K(5056K), 0.0124656 secs]
[Full GC 32902K->134K(37828K), 0.0091637 secs]
說明這次 holder 引用的對象所占的內存被回收了。我們慢慢來分析。
首先明確一點:holder 能否被回收的根本原因是局部變量表中的 Slot 是否還存有關于 holder 數組對象的引用。
在第一次修改中,雖然在 holder 作用域之外進行回收,但是在此之后,沒有對局部變量表的讀寫操作,holder 所占用的 Slot 還沒有被其他變量所復用(回憶 Java 內存區(qū)域與內存溢出一文中關于 Slot 的講解),所以作為 GC Roots 一部分的局部變量表仍保持者對它的關聯(lián)。這種關聯(lián)沒有被及時打斷,因此 GC 收集器不會將 holder 引用的對象內存回收掉。 在第二次修改中,在 GC 收集器工作前,手動將 holder 設置為 null 值,就把 holder 所占用的局部變量表中的 Slot 清空了,因此,這次 GC 收集器工作時將 holder 之前引用的對象內存回收掉了。
當然,我們也可以用其他方法來將 holder 引用的對象內存回收掉,只要復用 holder 所占用的 slot 即可,比如在 holder 作用域之外執(zhí)行一次讀寫操作。
為對象賦 null 值并不是控制變量回收的最好方法,以恰當的變量作用域來控制變量回收時間才是最優(yōu)雅的解決辦法。另外,賦 null 值的操作在經過虛擬機 JIT 編譯器優(yōu)化后會被消除掉,經過 JIT 編譯后,System.gc()執(zhí)行時就可以正確地回收掉內存,而無需賦 null 值。
Java 虛擬機的內存管理與垃圾收集是虛擬機結構體系中最重要的組成部分,對程序(尤其服務器端)的性能和穩(wěn)定性有著非常重要的影響。性能調優(yōu)需要具體情況具體分析,而且實際分析時可能需要考慮的方面很多,這里僅就一些簡單常用的情況作簡要介紹。
1、線程堆棧:可通過 -Xss 調整大小,內存不足時拋出 StackOverflowError(縱向無法分配,即無法分配新的棧幀)或 OutOfMemoryError(橫向無法分配,即無法建立新的線程)。
2、Socket 緩沖區(qū):每個 Socket 連接都有 Receive 和 Send 兩個緩沖區(qū),分別占用大約 37KB 和 25KB 的內存。如果無法分配,可能會拋出 IOException:Too many open files 異常。關于 Socket 緩沖區(qū)的詳細介紹參見我的 Java 網絡編程系列中深入剖析 Socket 的幾篇文章。
3、JNI 代碼:如果代碼中使用了JNI調用本地庫,那本地庫使用的內存也不在堆中。
4、虛擬機和 GC:虛擬機和 GC 的代碼執(zhí)行也要消耗一定的內存。
更多建議: