存緊張時(shí)它說(shuō)沒(méi)就沒(méi)了)
「Java 進(jìn)階之路」系列 Day35寫在前面前幾篇一直在講垃圾怎么回收標(biāo)記清除/整理/復(fù)制、分代收集、CMS/G1/ZGC但更前置的問(wèn)題其實(shí)是JVM怎么判斷一個(gè)對(duì)象是不是垃圾很多人第一反應(yīng)是引用計(jì)數(shù)但Java壓根沒(méi)用這個(gè)方案還有人寫緩存時(shí)納悶——明明一個(gè)Map里還攥著對(duì)象的引用為什么內(nèi)存緊張時(shí)這些對(duì)象說(shuō)沒(méi)就沒(méi)了。這篇把這兩個(gè)問(wèn)題一次講清楚。一、是什么可達(dá)性分析怎么判斷一個(gè)對(duì)象活著還是死了為什么不用引用計(jì)數(shù)最直觀的判活方案是給每個(gè)對(duì)象掛一個(gè)計(jì)數(shù)器被引用一次加一引用失效減一計(jì)數(shù)器歸零就判定為垃圾——這就是引用計(jì)數(shù)法簡(jiǎn)單直接但有一個(gè)硬傷處理不了循環(huán)引用。對(duì)象A持有對(duì)B的引用對(duì)象B持有對(duì)A的引用如果A和B互相引用即使外部所有代碼都不再持有A、B的引用了它們兩個(gè)的計(jì)數(shù)器依然都是1永遠(yuǎn)不會(huì)歸零會(huì)被誤判為還活著造成內(nèi)存泄漏。正是因?yàn)檫@個(gè)致命缺陷主流JVM都沒(méi)有采用引用計(jì)數(shù)法Python等語(yǔ)言用了引用計(jì)數(shù)但要靠額外的循環(huán)檢測(cè)器來(lái)彌補(bǔ)這個(gè)漏洞。可達(dá)性分析從根出發(fā)摸得到就是活的Java采用的方案是可達(dá)性分析Reachability Analysis選定一批特殊的對(duì)象作為GC Roots從這些Roots出發(fā)沿著引用鏈一路往下摸凡是能夠被摸到可達(dá)的對(duì)象都判定為存活摸不到的對(duì)象才判定為垃圾。GC Roots對(duì)象A對(duì)象B對(duì)象C對(duì)象D對(duì)象E對(duì)象F上圖里A、B、C、D、E都能從GC Roots出發(fā)摸到判定為存活F雖然自己引用著E但沒(méi)有任何Roots能摸到F本身F就是垃圾即使F和E之間有引用關(guān)系也無(wú)濟(jì)于事——這正好解決了引用計(jì)數(shù)法處理不了的循環(huán)引用問(wèn)題即使F和E互相引用只要它們整體上摸不到GC Roots照樣會(huì)被正確回收。常見(jiàn)能作為GC Roots的對(duì)象包括各個(gè)線程當(dāng)前調(diào)用棧里的局部變量、方法區(qū)里的靜態(tài)變量、被JNINative方法引用的對(duì)象、以及一些和類加載、類型相關(guān)的固定引用。二、為什么明明有引用還是被回收強(qiáng)引用之外還有三種引用如果只有有引用就存活、沒(méi)引用就回收這一條規(guī)則會(huì)有一類場(chǎng)景很難處理想讓JVM在內(nèi)存充裕時(shí)盡量保留一批對(duì)象比如緩存但內(nèi)存緊張、快要OOM之前又希望能優(yōu)先把它們清掉而不是眼睜睜看著系統(tǒng)因?yàn)樗辣е彺娌环哦罎?。Java用四種強(qiáng)度不同的引用類型來(lái)滿足這類需求引用類型什么時(shí)候被回收典型場(chǎng)景強(qiáng)引用Strong Reference只要還可達(dá)永遠(yuǎn)不會(huì)被回收哪怕內(nèi)存溢出也不放日常的new Object()賦值軟引用SoftReference內(nèi)存足夠時(shí)不回收內(nèi)存不足、快要OOM之前會(huì)被回收內(nèi)存敏感的緩存弱引用WeakReference不管內(nèi)存夠不夠只要發(fā)生一次GC就會(huì)被回收ThreadLocal的Entry key虛引用PhantomReference形同虛設(shè)隨時(shí)可能被回收拿不到引用的對(duì)象本身只用來(lái)在對(duì)象被回收時(shí)收到一個(gè)通知配合Cleaner做資源清理跟蹤強(qiáng)引用 永遠(yuǎn)不回收軟引用 內(nèi)存不足才回收弱引用 只要GC就回收虛引用 完全不影響生存 只用來(lái)收通知回到開(kāi)頭的疑問(wèn)如果緩存的Map里存的是軟引用而不是強(qiáng)引用即使這個(gè)引用鏈條一直存在、對(duì)象在可達(dá)性分析里依然可達(dá)只要JVM快要內(nèi)存不足了垃圾回收器依然會(huì)主動(dòng)把軟引用指向的對(duì)象清掉把內(nèi)存騰出來(lái)——這不是可達(dá)性分析出了錯(cuò)而是軟引用本身在設(shè)計(jì)上就允許JVM在特定條件下選擇性地把它們視為可以犧牲的對(duì)象。三、怎么用軟引用做緩存弱引用防內(nèi)存泄漏軟引用一個(gè)天然的、內(nèi)存敏感的緩存實(shí)現(xiàn)MapString,SoftReferencebyte[]cachenewHashMap();// 存入緩存cache.put(key1,newSoftReference(loadBigData()));// 取用時(shí)要判斷是否已經(jīng)被回收SoftReferencebyte[]refcache.get(key1);byte[]data(ref!null)?ref.get():null;if(datanull){dataloadBigData();// 已被回收重新加載cache.put(key1,newSoftReference(data));}用軟引用包一層緩存就能自動(dòng)獲得內(nèi)存寬裕時(shí)盡量保留內(nèi)存緊張時(shí)自動(dòng)讓路的能力不需要手寫LRU淘汰邏輯去猜測(cè)什么時(shí)候該清理——代價(jià)是每次取用都要判空重新加載且軟引用對(duì)象本身如果引用關(guān)系復(fù)雜也會(huì)帶來(lái)一定的額外管理開(kāi)銷所以規(guī)模較小、訪問(wèn)頻繁的緩存用現(xiàn)成的Guava Cache/Caffeine這類工具會(huì)比自己手搓軟引用更省心。弱引用ThreadLocal為什么用它做key回到Day09講過(guò)的ThreadLocal內(nèi)存泄漏問(wèn)題——ThreadLocalMap的Entry對(duì)key也就是ThreadLocal實(shí)例本身用的就是弱引用只要一次GC發(fā)生只要沒(méi)有其他強(qiáng)引用指著這個(gè)ThreadLocal實(shí)例它就會(huì)被回收Entry里的key會(huì)自動(dòng)變成null。這樣設(shè)計(jì)是為了讓外部代碼已經(jīng)不再持有這個(gè)ThreadLocal引用這件事能盡快被JVM感知到而不需要用戶顯式調(diào)用remove()才能讓key被回收——但value依然是強(qiáng)引用不會(huì)跟著自動(dòng)清理這才是ThreadLocal真正的內(nèi)存泄漏風(fēng)險(xiǎn)點(diǎn)key沒(méi)了、value還在占著位置出不去所以remove()該調(diào)用還是要調(diào)用。虛引用連拿到對(duì)象這件事都不允許虛引用是四種里最特殊的一種——通過(guò)PhantomReference.get()永遠(yuǎn)返回null也就是說(shuō)你壓根不能靠虛引用去訪問(wèn)這個(gè)對(duì)象它唯一的作用是配合一個(gè)引用隊(duì)列ReferenceQueue在對(duì)象被垃圾回收器真正回收前的某個(gè)時(shí)機(jī)往隊(duì)列里塞一個(gè)通知。這給了開(kāi)發(fā)者一個(gè)對(duì)象即將/已經(jīng)被回收的鉤子常用來(lái)替代已經(jīng)過(guò)時(shí)的finalize()方法做一些堆外內(nèi)存、文件句柄之類的資源清理收尾工作JDK 9之后的java.lang.ref.Cleaner底層就是基于虛引用實(shí)現(xiàn)的。四、面試追問(wèn)Q1為什么Java不用引用計(jì)數(shù)法判斷對(duì)象是否存活引用計(jì)數(shù)法無(wú)法正確處理循環(huán)引用的場(chǎng)景兩個(gè)互相引用的對(duì)象即使外部已經(jīng)沒(méi)有任何代碼持有它們各自的計(jì)數(shù)器依然不為零永遠(yuǎn)不會(huì)被判定為垃圾導(dǎo)致內(nèi)存泄漏。Java采用的可達(dá)性分析是從GC Roots出發(fā)摸引用鏈天然能正確處理循環(huán)引用——只要整體上摸不到GC Roots即使對(duì)象之間互相引用也會(huì)被回收。Q2哪些對(duì)象可以作為GC Roots常見(jiàn)的包括各線程當(dāng)前調(diào)用棧里的局部變量、方法區(qū)里的靜態(tài)變量、被JNINative方法引用的對(duì)象以及一些和類加載、類型信息相關(guān)的固定引用。可達(dá)性分析就是從這批對(duì)象出發(fā)判斷哪些對(duì)象能被引用鏈摸到。Q3軟引用和弱引用的核心區(qū)別是什么軟引用只有在內(nèi)存不足、即將發(fā)生OutOfMemoryError之前才會(huì)被回收內(nèi)存充裕時(shí)會(huì)盡量保留適合做內(nèi)存敏感的緩存弱引用則不管內(nèi)存夠不夠只要發(fā)生一次垃圾回收就會(huì)被清理生存周期比軟引用短得多典型應(yīng)用是ThreadLocalMap里Entry對(duì)key的引用。Q4ThreadLocal為什么會(huì)內(nèi)存泄漏跟弱引用有什么關(guān)系ThreadLocalMap的Entry對(duì)keyThreadLocal實(shí)例用的是弱引用只要沒(méi)有其他強(qiáng)引用一次GC后key就會(huì)被回收變成null但Entry對(duì)value的引用依然是強(qiáng)引用不會(huì)自動(dòng)清理。如果線程是線程池里的核心線程、長(zhǎng)期存活且用完ThreadLocal后沒(méi)有調(diào)用remove()這些key已經(jīng)為null、但value還在的Entry會(huì)一直占用內(nèi)存無(wú)法被回收這才是真正的內(nèi)存泄漏點(diǎn)不是弱引用本身導(dǎo)致的問(wèn)題而是value沒(méi)有配套清理導(dǎo)致的。Q5虛引用有什么實(shí)際用途虛引用無(wú)法通過(guò)get()方法獲取到對(duì)象本身永遠(yuǎn)返回null唯一作用是配合引用隊(duì)列在對(duì)象被垃圾回收前后收到一個(gè)通知。它常被用來(lái)替代已經(jīng)過(guò)時(shí)、不推薦使用的finalize()方法跟蹤對(duì)象生命周期的結(jié)束時(shí)機(jī)去做一些堆外內(nèi)存釋放、文件句柄關(guān)閉之類的資源清理工作JDK 9之后推薦的java.lang.ref.Cleaner底層就是基于虛引用實(shí)現(xiàn)的。下一篇預(yù)告Day36 結(jié)合線上實(shí)戰(zhàn)場(chǎng)景講講OOM排查和連接池問(wèn)題該怎么定位——從這幾篇講的GC原理落地到真實(shí)的故障排查思路上。