本文介紹了學(xué)習(xí)垃圾回收器前應(yīng)掌握的儲備知識。
目錄
?1 垃圾回收概述
??1.1 手動gc理解不可達(dá)對象的回收行為
?2 內(nèi)存溢出與內(nèi)存泄漏
??2.1 內(nèi)存溢出
??2.2 內(nèi)存泄漏
?3 Stop The World
?4 垃圾回收的并行與并發(fā)
??4.1 并發(fā)(Concurrent)
??4.2 并行(Parallel)
??4.3 并發(fā)VS并行
??4.4 垃圾回收的并發(fā)與并行
?5 安全點與安全區(qū)域
?? 5.1 安全點(Safepoint)
?? 5.2 安全區(qū)域(Safe Region)
?6 引用
??6.1 強引用: 不回收
??6.2 軟引用: 內(nèi)存不足即回收
??6.3 弱引用: 發(fā)現(xiàn)即回收
??6.4 虛引用: 對象回收跟蹤
??6.5 終結(jié)器引用
1 System.gc()的理解
- 在默認(rèn)情況下,通過System.gc ()或者Runtime.getRuntime().gc()的調(diào)用沫屡,會顯式觸發(fā)Full GC金矛,同時對老年代和新生代進行回收驶俊,嘗試釋放被丟棄對象占用的內(nèi)存废睦。
- 然而System.gc()調(diào)用附帶一個免責(zé)聲明奈应,無法保證對垃圾收集器的調(diào)用(無法保證馬上觸發(fā)GC)杖挣。
- JVM實現(xiàn)者可以通過system.gc()調(diào)用來決定JVM的GC行為惩妇。而一般情況下歌殃,垃圾回收應(yīng)該是自動進行的,無須手動觸發(fā)波材,否則就太過于麻煩了。在一些特殊情況下贾铝,如我們正在編寫一個性能基準(zhǔn)大脉,我們可以在運行之間調(diào)用System.gc()镰矿。
public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
System.gc();//提醒jvm的垃圾回收器執(zhí)行g(shù)c,但是不確定是否馬上執(zhí)行g(shù)c
//與Runtime.getRuntime().gc();的作用一樣绝淡。
System.runFinalization();//強制調(diào)用使用引用的對象的finalize()方法
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest 重寫了finalize()");
}
}
1.1 手動gc理解不可達(dá)對象的回收行為
public class LocalVarGC {
public void localvarGC1() {
byte[] buffer = new byte[10 * 1024 * 1024];//10MB
System.gc();
//輸出: 不會被回收, FullGC時被放入老年代
//[GC (System.gc()) [PSYoungGen: 14174K->10736K(76288K)] 14174K->10788K(251392K), 0.0089741 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
//[Full GC (System.gc()) [PSYoungGen: 10736K->0K(76288K)] [ParOldGen: 52K->10649K(175104K)] 10788K->10649K(251392K), [Metaspace: 3253K->3253K(1056768K)], 0.0074098 secs] [Times: user=0.01 sys=0.02, real=0.01 secs]
}
public void localvarGC2() {
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc();
//輸出: 正常被回收
//[GC (System.gc()) [PSYoungGen: 14174K->544K(76288K)] 14174K->552K(251392K), 0.0011742 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[Full GC (System.gc()) [PSYoungGen: 544K->0K(76288K)] [ParOldGen: 8K->410K(175104K)] 552K->410K(251392K), [Metaspace: 3277K->3277K(1056768K)], 0.0054702 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
}
public void localvarGC3() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc();
//輸出: 不會被回收, FullGC時被放入老年代
//[GC (System.gc()) [PSYoungGen: 14174K->10736K(76288K)] 14174K->10784K(251392K), 0.0076032 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
//[Full GC (System.gc()) [PSYoungGen: 10736K->0K(76288K)] [ParOldGen: 48K->10649K(175104K)] 10784K->10649K(251392K), [Metaspace: 3252K->3252K(1056768K)], 0.0096328 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
}
public void localvarGC4() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int value = 10;
System.gc();
//輸出: 正常被回收
//[GC (System.gc()) [PSYoungGen: 14174K->496K(76288K)] 14174K->504K(251392K), 0.0016517 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
//[Full GC (System.gc()) [PSYoungGen: 496K->0K(76288K)] [ParOldGen: 8K->410K(175104K)] 504K->410K(251392K), [Metaspace: 3279K->3279K(1056768K)], 0.0055183 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
}
public void localvarGC5() {
localvarGC1();
System.gc();
//輸出: 正常被回收
//[GC (System.gc()) [PSYoungGen: 14174K->10720K(76288K)] 14174K->10744K(251392K), 0.0121568 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
//[Full GC (System.gc()) [PSYoungGen: 10720K->0K(76288K)] [ParOldGen: 24K->10650K(175104K)] 10744K->10650K(251392K), [Metaspace: 3279K->3279K(1056768K)], 0.0101068 secs] [Times: user=0.01 sys=0.02, real=0.01 secs]
//[GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] 10650K->10650K(251392K), 0.0005717 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[Full GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 10650K->410K(175104K)] 10650K->410K(251392K), [Metaspace: 3279K->3279K(1056768K)], 0.0045963 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
}
public static void main(String[] args) {
LocalVarGC local = new LocalVarGC();
local.localvarGC5();
}
}
2 內(nèi)存溢出與內(nèi)存泄漏
- 內(nèi)存溢出相對于內(nèi)存泄漏來說衙猪,盡管更容易被理解垫释,但是同樣的显蝌,內(nèi)存溢出也是引發(fā)程序崩潰的罪魁禍?zhǔn)字弧?/li>
- 由于GC一直在發(fā)展曼尊,所有一般情況下,除非應(yīng)用程序占用的內(nèi)存增長速度非掣感穑快屿岂,造成垃圾回收已經(jīng)跟不上內(nèi)存消耗的速度,否則不太容易出現(xiàn)O0M的情況带欢。
- 大多數(shù)情況下,GC會進行各種年齡段的垃圾回收渡贾,實在不行了就放大招空骚,來一次獨占式的Full GC操作熬甚,這時候會回收大量的內(nèi)存乡括,供應(yīng)用程序繼續(xù)使用诲泌。
- javadoc中對OutOfMemoryError的解釋是,沒有空閑內(nèi)存呻澜,并且垃圾收集器也無法提供更多內(nèi)存。
2.1 內(nèi)存溢出(OOM)
- 首先說沒有空閑內(nèi)存的情況:說明Java虛擬機的堆內(nèi)存不夠栅受。原因有二:
-
Java虛擬機的堆內(nèi)存設(shè)置不夠。
??比如:可能存在內(nèi)存泄漏問題而芥;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的數(shù)據(jù)量歌逢,但是沒有顯式指定JVM堆大小或者指定數(shù)值偏小。我們可以通過參數(shù)-Xms阱高、-Xmx來調(diào)整辟癌。 -
代碼中創(chuàng)建了大量大對象,并且長時間不能被垃圾收集器收集(存在被引用)
??對于老版本的Oracle JDK厂置,因為永久代的大小是有限的,并且JVM對永久代垃圾回收(如魂角,常量池回收昵济、卸載不再需要的類型)非常不積極,所以當(dāng)我們不斷添加新類型的時候野揪,永久代出現(xiàn)OutOfMemoryError也非常多見访忿,尤其是在運行時存在大量動態(tài)類型生成的場合;類似intern字符串緩存占用太多空間斯稳,也會導(dǎo)致OOM問題海铆。對應(yīng)的異常信息,會標(biāo)記出來和永久代相關(guān): "java. lang. OutOfMemoryError: PermGen space"。
-
Java虛擬機的堆內(nèi)存設(shè)置不夠。
??隨著元數(shù)據(jù)區(qū)的引入,方法區(qū)內(nèi)存已經(jīng)不再那么窘迫晓猛,所以相應(yīng)的OOM有所改觀磕秤,出現(xiàn)OOM蒙兰,異常信息則變成了:“java. lang. OutOfMemoryError: Metaspace"扳抽。 直接內(nèi)存不足贮尉,也會導(dǎo)致OOM。
- 這里面隱含著一層意思是,在拋出OutOfMemoryError之 前缚忧,通常垃圾收集器會被觸發(fā),盡其所能去清理出空間衡招。
- 例如:在引用機制分析中怀伦,涉及到JVM會去嘗試回收軟引用指向的對象等。
- 在java.nio.BIts.reserveMemory()方法中明也,我們能清楚的看到鹉胖,System.gc()會被調(diào)用愕撰,以清理空間仓犬。
- 當(dāng)然肌括,也不是在任何情況下垃圾收集器都會被觸發(fā)的
- 比如,我們?nèi)シ峙湟粋€超大對象橡羞,類似一個超大數(shù)組超過堆的最大值,JVM可以判斷出垃圾收集并不能解決這個問題丐吓,所以直接拋出OutOfMemoryError稚新。
2.2 內(nèi)存泄漏(Memory Leak)
- 也稱作“存儲滲漏”。嚴(yán)格來說,只有對象不會再被程序用到了悠栓,但是GC又不能回收他們的情況错洁,才叫內(nèi)存泄漏仍稀。
- 但實際情況很多時候一些不太好的實踐(或疏忽)會導(dǎo)致對象的生命周期變得很長甚至導(dǎo)致0OM,也可以叫做寬泛意義上的“內(nèi)存泄漏。
- 盡管內(nèi)存泄漏并不會立刻引起程序崩潰,但是一旦發(fā)生內(nèi)存泄漏晤硕,程序中的可用內(nèi)存就會被逐步蠶食,直至耗盡所有內(nèi)存蛔翅,最終出現(xiàn)0utOfMemory異常茂卦,導(dǎo)致程序崩潰。
-
注意钧嘶,這里的存儲空間并不是指物理內(nèi)存,而是指虛擬內(nèi)存大小校哎,這個虛擬內(nèi)存大小取決于磁盤交換區(qū)設(shè)定的大小灌危。
內(nèi)存泄漏舉例
- 單例模式
單例的生命周期和應(yīng)用程序是一樣長的,所以單例程序中,如果持有對外部對象的引用的話怎憋,那么這個外部對象是不能被回收的缤苫,則會導(dǎo)致內(nèi)存泄漏的產(chǎn)生。 - 一些提供close的資源未關(guān)閉導(dǎo)致內(nèi)存泄漏
數(shù)據(jù)庫連接(dataSourse. getConnection)),網(wǎng)絡(luò)連接(socket)和io連接必須手動close努酸,否則是不能被回收的。
3 Stop The World
-
Stop-the-World
肋僧,簡稱STW
,指的是GC事件發(fā)生過程中,會產(chǎn)生應(yīng)用程序的停頓,停頓產(chǎn)生時整個應(yīng)用程序線程都會被暫停趾盐,沒有任何響應(yīng)匾灶。- 可達(dá)性分析算法中枚舉根節(jié)點(GC Roots)會導(dǎo)致所有Java執(zhí)行線程停頓。.
- 分析工作必須在一個能確保一致性的快照 中進行
- 一致性指整個分析期間整個執(zhí)行系統(tǒng)看起來像被凍結(jié)在某個時間點上锡凝。
- 如果出現(xiàn)分析過程中對象引用關(guān)系還在不斷變化粘昨,則分析結(jié)果的準(zhǔn)確性無法保證
- 可達(dá)性分析算法中枚舉根節(jié)點(GC Roots)會導(dǎo)致所有Java執(zhí)行線程停頓。.
- 被STW中斷的應(yīng)用程序線程會在完成GC之后恢復(fù),頻繁中斷會讓用戶感覺像是網(wǎng)速不快造成電影卡帶一樣, 所以我們需要減少STW的發(fā)生张肾。
- STW事件和采用哪款GC無關(guān)芭析,所有的GC都有這個事件。
- 哪怕是G1也不能完全避免Stop-the-world情況發(fā)生吞瞪,只能說垃圾回收器越來越優(yōu)秀馁启,回收效率越來越高,盡可能地縮短了暫停時間芍秆。
- STW是JVM在后臺自動發(fā)起和自動完成的惯疙。在用戶不可見的情況下,把用戶正常的工作線程全部停掉妖啥。
- 開發(fā)中不要用System.gc()霉颠;會導(dǎo)致Stop-the-world的發(fā)生。
public class StopTheWorldDemo {
public static class WorkThread extends Thread {
List<byte[]> list = new ArrayList<byte[]>();
public void run() {
try {
while (true) {
for(int i = 0;i < 1000;i++){
byte[] buffer = new byte[1024];
list.add(buffer);
}
if(list.size() > 10000){
list.clear();
System.gc();//會觸發(fā)full gc荆虱,進而會出現(xiàn)STW事件
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static class PrintThread extends Thread {
public final long startTime = System.currentTimeMillis();
public void run() {
try {
while (true) {
// 每秒打印時間信息
long t = System.currentTimeMillis() - startTime;
System.out.println(t / 1000 + "." + t % 1000);
Thread.sleep(1000);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
WorkThread w = new WorkThread();
PrintThread p = new PrintThread();
w.start();
p.start();
}
}
4 垃圾回收的并行與并發(fā)
4.1 并發(fā)(Concurrent)
- 在操作系統(tǒng)中蒿偎,是指一個時間段中有幾個程序都處于己啟動運行到運行完畢之間,且這幾個程序都是在同一個處理器上運行怀读。
-
并發(fā)不是真正意義上的“同時進行”诉位,只是CPU把一個時間段劃分成幾個時間片段(時間區(qū)間),然后在這幾個時間區(qū)間之間來回切換菜枷,由于CPU處理的速度非巢钥罚快,只要時間間隔處理得當(dāng)啤誊,即可讓用戶感覺是多個應(yīng)用程序同時在進行岳瞭。
4.2 并行(Parallel)
- 當(dāng)系統(tǒng)有一個以上CPU時,當(dāng)一個CPU執(zhí)行一個進程時坷衍,另一個CPU可以執(zhí)行另一個進程寝优,兩個進程互不搶占CPU資源,可以同時進行枫耳,我們稱之為并行(Parallel)。
- 其實==決定并行的因素不是CPU的數(shù)量孟抗,而是CPU的核心數(shù)量==迁杨,比如一個CPU多個核也可以 并行。
-
適合科學(xué)計算凄硼,后臺處理等弱交互場景
4.3 并發(fā)VS并行
- 并發(fā)铅协,指的是多個事情,在
同一時間段
內(nèi)同時發(fā)生了摊沉。 - 并行狐史,指的是多個事情,在
同一時間點
上同時發(fā)生了。 - 并發(fā)的多個任務(wù)之間是互相搶占資源的骏全。
- 并行的多個任務(wù)之間是不互相搶占資源的苍柏。
- 只有在多CPU或者一個CPU多核的情況中,才會發(fā)生并行姜贡。否則试吁,看似同時發(fā)生的事情,其實都是并發(fā)執(zhí)行的楼咳。
4.4 垃圾回收的并發(fā)與并行
并發(fā)和并行熄捍,在談?wù)摾占鞯纳舷挛恼Z境中,它們可以解釋如下:
-
并行(Parallel)
- 指多條垃圾收集線程并行工作母怜,但此時用戶線程仍處于等待狀態(tài)余耽。
- 如ParNew、 Parallel Scavenge苹熏、 Parallel 0ld宾添;
-
串行(Serial)
- 相較于并行的概念,單線程執(zhí)行柜裸。
-
如果內(nèi)存不夠缕陕,則程序暫停,啟動JVM垃圾回收器進行垃圾回收疙挺】敢兀回收完,再啟動程序的線程铐然。
-
并發(fā)(Concurrent)
:指用戶線程與垃圾收集線程同時執(zhí)行(但不一定是并行的蔬崩,可能會交替執(zhí)行),垃圾回收線程在執(zhí)行時不會停頓用戶程序的運行搀暑。- 用戶程序在繼續(xù)運行沥阳,而垃圾收集程序線程運行于另一個CPU上;
- 如: CMS自点、G1
5 安全點與安全區(qū)域
5.1 安全點(Safepoint)
- 程序執(zhí)行時并非在所有地方都能停頓下來開始GC桐罕,只有在特定的位置才能停頓下來開始GC,這些位置稱為
安全點(Safepoint)
桂敛。 - Safe Point的選擇很重要功炮,如果太少可能導(dǎo)致GC等待的時間太長,如果太頻繁可能導(dǎo)致運行時的性能問題术唬。大部分指令的執(zhí)行時間都非常短暫薪伏,通常會根據(jù)是否具有讓程序長時間執(zhí)行的特征為標(biāo)準(zhǔn)。比如:選擇些執(zhí)行時間較長的指令作為Safe Point粗仓, 如
方法調(diào)用
嫁怀、循環(huán)跳轉(zhuǎn)
和異常跳轉(zhuǎn)
等设捐。
如何在GC發(fā)生時,檢查所有線程都跑到最近的安全點停頓下來呢塘淑?
-
搶先式中斷
: (目前沒有虛擬機采用了) 首先中斷所有線程萝招。如果還有線程不在安全點,就恢復(fù)線程朴爬,讓線程跑到安全點即寒。 -
主動式中斷
: 設(shè)置一個中斷標(biāo)志,各個線程運行到Safe Point的時候主動輪詢這個標(biāo)志召噩,如果中斷標(biāo)志為真母赵,則將自己進行中斷掛起。
5.2 安全區(qū)域(Safe Region)
??Safepoint機制保證了程序執(zhí)行時具滴,在不太長 的時間內(nèi)就會遇到可進入GC的Safepoint 凹嘲。但是,程序“不執(zhí)行”的時候呢构韵?例如線程處于Sleep 狀態(tài)或Blocked狀態(tài)周蹭,這時候線程無法響應(yīng)JVM的中斷請求,“走” 到安全點去中斷掛起疲恢,JVM也不太可能等待線程被喚醒凶朗。對于這種情況,就需要安全區(qū)域(Safe Region)來解決显拳。
??安全區(qū)域是指在一段代碼片段中棚愤,對象的引用關(guān)系不會發(fā)生變化,在這個區(qū)域中的任何位置開始GC都是安全的杂数。我們也可以把Safe Region 看做是被擴展了的Safepoint宛畦。
實際執(zhí)行時
- 當(dāng)線程運行到Safe Region的代碼時,首先標(biāo)識已經(jīng)進入了Safe Region揍移,如果這段時間內(nèi)發(fā)生GC次和,JVM會忽略標(biāo)識為Safe Region狀態(tài)的線程;
- 當(dāng)線程即將離開Safe Region時那伐, 會檢查JVM是否已經(jīng)完成GC踏施,如果完成了,則繼續(xù)運行喧锦,否則線程必須等待直到收到可以安全離開SafeRegion的信號為止读规;
6 引用
- 我們希望能描述這樣一類對象: 當(dāng)內(nèi)存空間還足夠時,則能保留在內(nèi)存中燃少;如果內(nèi)存空間在進行垃圾收集后還是很緊張,則可以拋棄這些對象铃在。
- 在JDK 1.2版之后阵具,Java對引用的概念進行了擴充碍遍,將引用分為
強引用(Strong Reference)
、軟引用(Soft Reference)
阳液、弱引用(Weak Reference)
和虛引用(Phantom Reference)
4種怕敬,這4種引用強度依次逐漸減弱。 -
除強引用外帘皿,其他3種引用均可以在java.lang.ref包中找到它們的身影东跪。如下圖,顯示了這3種引用類型對應(yīng)的類鹰溜,開發(fā)人員可以在應(yīng)用程序中直接使用它們虽填。
Reference子類中只有終結(jié)器引用是包內(nèi)可見的,其他3種引用類型均為public曹动,可以在應(yīng)用程序中直接使用
-
強引用(StrongReference)
:最傳統(tǒng)的“引用”的定義斋日,是指在程序代碼之中普遍存在的引用賦值,即類似“0bject obj=new object()”這種引用關(guān)系墓陈。無論任何情況下恶守,只要強引用關(guān)系還存在,垃圾收集器就永遠(yuǎn)不會回收掉被引用的對象贡必。 -
軟引用(SoftReference)
:在系統(tǒng)將要發(fā)生內(nèi)存溢出之前兔港,將會把這些對象列入回收范圍之中進行第二次回收。如果這次回收后還沒有足夠的內(nèi)存仔拟,才會拋出內(nèi)存溢出異常衫樊。 -
弱引用(WeakReference)
:被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集之前。當(dāng)垃圾收集器工作時理逊,無論內(nèi)存空間是否足夠橡伞,都會回收掉被弱引用關(guān)聯(lián)的對象。 -
虛引用(PhantomReference)
:一個對象是否有虛引用的存在晋被,完全不會對其生存時間構(gòu)成影響兑徘,也無法通過虛引用來獲得一個對象的實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知(回收跟蹤)羡洛。
6.1 強引用: 不回收
- 在Java程序中挂脑,最常見的引用類型是強引用(普通系統(tǒng)99%以上都是強引用),也就是我們最常見的普通對象引用欲侮,也是默認(rèn)的引用類型崭闲。
- 當(dāng)在Java語言中使用new操作符創(chuàng)建一個新的對象, 并將其賦值給一個變量的時候威蕉,這個變量就成為指向該對象的一個強引用刁俭。
- 強引用的對象是可觸及的,垃圾收集器就永遠(yuǎn)不會回收掉被引用的對象韧涨。
- 對于一個普通的對象牍戚,如果沒有其他的引用關(guān)系侮繁,只要超過了引用的作用域或者顯式地將相應(yīng)(強)引用賦值為null,就是可以當(dāng)做垃圾被收集了如孝,當(dāng)然具體回收時機還是要看垃圾收集策略宪哩。
- 相對的,軟引用第晰、 弱引用和虛引用的對象是軟可觸及锁孟、弱可觸及和虛可觸及的,在一定條件下茁瘦,都是可以被回收的品抽。所以,強引用是造成Java內(nèi)存泄漏的主要原因之一腹躁。
public class StrongReferenceTest {
public static void main(String[] args) {
StringBuffer str = new StringBuffer ("Hello");
StringBuffer str1 = str;
str = null;
System.gc();
try {
Thread.sleep(3000); //gc可能不會立即執(zhí)行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str1);
}
}
??局部變量str指向StringBuffer實例所在堆空間桑包,通過str可以操作該實例,那么str就是StringBuffer實例的強引用纺非,對應(yīng)內(nèi)存結(jié)構(gòu):
??此時,如果再運行一個賦值語句:
StringBuffer str1 = str;
對應(yīng)內(nèi)存結(jié)構(gòu):本例中的兩個引用哑了,都是強引用,強引用具備以下特點:
- 強引用可以直接訪問目標(biāo)對象烧颖。
- 強引用所指向的對象在任何時候都不會被系統(tǒng)回收弱左,虛擬機寧愿拋出OOM異常,也不會回收強引用所指向?qū)ο蟆?/li>
- 強引用可能導(dǎo)致內(nèi)存泄漏炕淮。
6.2 軟引用: 內(nèi)存不足即回收
- 軟引用是用來描述一 些還有用拆火,但非必需的對象。只被軟引用關(guān)聯(lián)著的對象涂圆,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前们镜,會把這些對象列進回收范圍之中進行第二次回收(一次回收:對不可達(dá)對象而言)抬探,如果這次回收還沒有足夠的內(nèi)存兑燥,才會拋出內(nèi)存溢出異常。
-
軟引用通常用來實現(xiàn)內(nèi)存敏感的緩存叫乌。比如:
高速緩存
就有用到軟引用踩衩。如果還有空閑內(nèi)存嚼鹉,就可以暫時保留緩存,當(dāng)內(nèi)存不足時清理掉驱富,這樣就保證了使用緩存的同時锚赤,不會耗盡內(nèi)存。 - 在JDK 1. 2版之后提供了java.lang.ref.SoftReference類來實現(xiàn)軟引用褐鸥。
Object obj = new object()线脚; //聲明強引用
SoftReference<0bject> sf = new SoftReference<0bject>(obj);
obj = null; //銷毀強引用
- 垃圾回收器在某個時刻決定回收軟可達(dá)的對象的時候酒贬,會清理軟引用又憨,并可選地把引用存放到一個引用隊列( Reference Queue)翠霍。
- 類似弱引用锭吨,只不過Java虛擬機會盡量讓軟引用的存活時間長一些,迫不得.已才清理寒匙。
- 軟引用:
- 當(dāng)內(nèi)存足夠: 不會回收軟引用的可達(dá)對象
- 當(dāng)內(nèi)存不夠時: 會回收軟引用的可達(dá)對象
/**
* 軟引用的測試:內(nèi)存不足即回收
* -Xms10m -Xmx10m -XX:+PrintGCDetails
*/
public class SoftReferenceTest {
public static class User {
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int id;
public String name;
@Override
public String toString() {
return "[id=" + id + ", name=" + name + "] ";
}
}
public static void main(String[] args) {
//創(chuàng)建對象零如,建立軟引用
// SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
//上面的一行代碼,等價于如下的三行代碼
User u1 = new User(1,"songhk");
SoftReference<User> userSoftRef = new SoftReference<User>(u1);
u1 = null;//取消強引用
//從軟引用中重新獲得強引用對象
System.out.println(userSoftRef.get());
System.gc();
System.out.println("After GC:");
// //垃圾回收之后獲得軟引用中的對象
System.out.println(userSoftRef.get());//由于堆空間內(nèi)存足夠锄弱,所有不會回收軟引用的可達(dá)對象考蕾。
//
try {
//讓系統(tǒng)認(rèn)為內(nèi)存資源緊張、不夠
// byte[] b = new byte[1024 * 1024 * 7];
byte[] b = new byte[1024 * 7168 - 399 * 1024];//恰好能放下數(shù)組又放不下u1的內(nèi)存分配大小 不會報OOM
} catch (Throwable e) {
e.printStackTrace();
} finally {
//再次從軟引用中獲取數(shù)據(jù)
System.out.println(userSoftRef.get());//在報OOM之前会宪,垃圾回收器會回收軟引用的可達(dá)對象肖卧。
}
}
}
6.3 弱引用: 發(fā)現(xiàn)即回收
- 弱引用也是用來描述那些非必需對象,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生為止掸鹅。在系統(tǒng)GC時塞帐,只要發(fā)現(xiàn)弱引用,不管系統(tǒng)堆空間使用是否充足巍沙,都會回收掉只被弱引用關(guān)聯(lián)的對象葵姥。
- 但是,由于垃圾回收器的線程通常優(yōu)先級很低句携,因此榔幸,并不一 定能很快地發(fā)現(xiàn)持有弱引用的對象。在這種情況下矮嫉,弱引用對象可以存在較長的時間削咆。
- 弱引用和軟引用一樣,在構(gòu)造弱引用時蠢笋,也可以指定一個引用隊列拨齐,當(dāng)弱引用對象被回收時,就會加入指定的引用隊列挺尿,通過這個隊列可以跟蹤對象的回收情況奏黑。
- 軟引用、弱引用都非常適合來保存那些可有可無的緩存數(shù)據(jù)编矾。如果這么做熟史,當(dāng)系統(tǒng)內(nèi)存不足時,這些緩存數(shù)據(jù)會被回收窄俏,不會導(dǎo)致內(nèi)存溢出蹂匹。而當(dāng)內(nèi)存資源充足時,這些緩存數(shù)據(jù)又可以存在相當(dāng)長的時間凹蜈,從而起到加速系統(tǒng)的作用限寞。
- 在JDK1.2版之后提后了java.lang.ref.WeakReference類來實現(xiàn)弱引用
Object obj = new object()忍啸; //聲明強引用
WeakReference<0bject> sf = new WeakReference<0bject>(obj);
obj = null履植; //銷毀強引用
- 弱引用對象與軟引用對象的最大不同就在于计雌,當(dāng)GC在進行回收時,需要通過算法檢查是否回收軟引用對象玫霎,而對于弱引用對象凿滤,GC總是進行回收。弱引用對象更容易庶近、更快被GC回收翁脆。
面試題:你開發(fā)中使用過WeakHashMap嗎?
* 通過查看WeakHashMap源碼,可以看到其內(nèi)部類Entry使用的就是弱引用
* line 702 ->private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {...}
public class WeakReferenceTest {
public static class User {
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int id;
public String name;
@Override
public String toString() {
return "[id=" + id + ", name=" + name + "] ";
}
}
public static void main(String[] args) {
//構(gòu)造了弱引用
WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));
//從弱引用中重新獲取對象
System.out.println(userWeakRef.get());
System.gc();
// 不管當(dāng)前內(nèi)存空間足夠與否鼻种,都會回收它的內(nèi)存
System.out.println("After GC:");
//重新嘗試從弱引用中獲取對象
System.out.println(userWeakRef.get());
}
}
6.4 虛引用: 對象回收跟蹤
- 虛引用(Phantom Reference),也稱為“幽靈引用”或者“幻影引用”反番,是所有引用類型中最弱的一個。
- 一個對象是否有虛引用的存在叉钥,完全不會決定對象的生命周期罢缸。如果一個對象僅持有虛引用,那么它和沒有引用幾乎是一樣的沼侣,隨時都可能被垃圾回收器回收祖能。
- 它不能單獨使用,也無法通過虛引用來獲取被引用的對象蛾洛。當(dāng)試圖通過虛引用的get()方法取得對象時养铸,總是null。
- 為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的在于跟蹤垃圾回收過程轧膘。比如:能在這個對象被收集器回收時收到一個系統(tǒng)通知钞螟。
- 由于虛引用可以跟蹤對象的回收時間,因此谎碍,也可以將一些資源釋放操作放置在虛引用中執(zhí)行和記錄鳞滨。
- 在JDK 1. 2版之后提供了PhantomReference類來實現(xiàn)虛引用。
object obj = new object();
ReferenceQueuephantomQueue = new ReferenceQueue( ) ;
PhantomReference<object> pf = new PhantomReference<object>(obj, phantomQueue);
obj = null;
- 虛引用必須和引用隊列一起使用蟆淀。虛引用在創(chuàng)建時必須提供一個引用隊列作為參數(shù)拯啦。當(dāng)垃圾回收器準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用熔任,就會在回收對象后褒链,將這個虛引用加入引用隊列,以通知應(yīng)用程序?qū)ο蟮幕厥涨闆r疑苔。
public class PhantomReferenceTest {
public static PhantomReferenceTest obj;//當(dāng)前類對象的聲明
static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用隊列
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
PhantomReference<PhantomReferenceTest> objt = null;
try {
objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (objt != null) {
System.out.println("追蹤垃圾回收過程:PhantomReferenceTest實例被GC了");
}
}
}
}
}
@Override
protected void finalize() throws Throwable { //finalize()方法只能被調(diào)用一次甫匹!
super.finalize();
System.out.println("調(diào)用當(dāng)前類的finalize()方法");
obj = this;
}
public static void main(String[] args) {
Thread t = new CheckRefQueue();
t.setDaemon(true);//設(shè)置為守護線程:當(dāng)程序中沒有非守護線程時,守護線程也就執(zhí)行結(jié)束。
t.start();
phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
obj = new PhantomReferenceTest();
//構(gòu)造了 PhantomReferenceTest 對象的虛引用兵迅,并指定了引用隊列
PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
try {
//不可獲取虛引用中的對象
System.out.println(phantomRef.get());
//將強引用去除
obj = null;
//第一次進行GC,由于對象可復(fù)活抢韭,GC無法回收該對象
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
System.out.println("第 2 次 gc");
obj = null;
System.gc(); //一旦將obj對象回收,就會將此虛引用存放到引用隊列中恍箭。
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6.5 終結(jié)器引用(Final reference)
- 它用以實現(xiàn)對象的finalize()方法刻恭。
- 無需手動編碼, 其內(nèi)部配合引用隊列使用季惯。
- 在GC時吠各, 終結(jié)器引用入隊。由Finalizer線程通過終結(jié)器引用找到被引用對象并調(diào)用它的finalize()方法勉抓,第二次GC時才能回收被引用對象。