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