前言
ThreadLocal 在什么情況下可能發(fā)生內(nèi)存泄漏柒昏?如果你想清楚這個(gè)問題的來龍去脈凳宙,看源碼是必不可少的,看了源碼之后你發(fā)現(xiàn)职祷, ThreadLocal 中用到 static class Entry extends WeakReference<ThreadLocal<?>> {} ,謎底實(shí)際就是使用了弱引用 WeakReference氏涩。
本文內(nèi)容概要
強(qiáng)引用:Object o = new Object()
軟引用:new SoftReference(o);
弱引用:new WeakReference(o);
虛引用:new PhantomReference(o);
ThreadLocal 的使用,及使用不當(dāng)發(fā)生內(nèi)存泄漏的原因
Jdk 1.2 增加了抽象類 Reference 和 SoftReference有梆、WeakReference是尖、PhantomReference,擴(kuò)展了引用類型分類泥耀,達(dá)到對(duì)內(nèi)存更細(xì)粒度的控制饺汹。
比如我們的緩存數(shù)據(jù),當(dāng)內(nèi)存不夠用的時(shí)候痰催,我希望緩存可以釋放內(nèi)存兜辞,或者將緩存存入到堆外等。
但我們?cè)趺磪^(qū)分哪些對(duì)象需要回收(垃圾回收算法陨囊,可達(dá)性分析),回收的時(shí)機(jī)弦疮,回收的時(shí)候可以讓我們拿到回收的通知,所以 JDK 1.2 帶來這幾個(gè)引用類型蜘醋。
強(qiáng)引用
強(qiáng)引用就是我們經(jīng)常用到的方式:Object o = new Object()胁塞。垃圾回收時(shí),強(qiáng)引用的變量是不會(huì)被回收压语,只有設(shè)置 o=null啸罢,jvm 通過可達(dá)性分析,沒有 GC root 到達(dá)對(duì)象胎食,垃圾回收器才會(huì)清理堆中的對(duì)象扰才,釋放內(nèi)存。 當(dāng)繼續(xù)申請(qǐng)內(nèi)存分配厕怜,就會(huì) oom衩匣。
定義一個(gè)類 Demo蕾总,Demo 實(shí)例占用內(nèi)存大小為 10m,不停往 list 添加 Demo 的示例琅捏,由于不能申請(qǐng)到內(nèi)存分配生百,程序拋出 oom 終止
// -Xmx600mpublicclassSoftReferenceDemo{// 1mprivatestaticint_1M =1024*1024*1;publicstaticvoidmain(String[] args) throws InterruptedException{? ? ? ? ArrayList objects = Lists.newArrayListWithCapacity(50);intcount =1;while(true) {? ? ? ? ? ? Thread.sleep(100);// 獲取 jvm 空閑的內(nèi)存為多少 mlongmeme_free = Runtime.getRuntime().freeMemory() / _1M;if((meme_free -10) >=0) {? ? ? ? ? ? ? ? Demo demo =newDemo(count);? ? ? ? ? ? ? ? objects.add(demo);? ? ? ? ? ? ? ? count++;? ? ? ? ? ? ? ? demo =null;? ? ? ? ? ? }? ? ? ? ? ? System.out.println("jvm 空閑內(nèi)存"+ meme_free +" m");? ? ? ? ? ? System.out.println(objects.size());? ? ? ? }? ? }? ? @DatastaticclassDemo{privatebyte[] a =newbyte[_1M *10];privateString str;publicDemo(inti){this.str = String.valueOf(i);? ? ? ? }? ? }
以上代碼運(yùn)行結(jié)果,拋出 oom 程序停止
jvm空閑內(nèi)存41 m54Exceptionin thread "main" java.lang.OutOfMemoryError: Java heap spaceatcom.fly.blog.ref.SoftReferenceDemo$Demo.<init>(SoftReferenceDemo.java:37)atcom.fly.blog.ref.SoftReferenceDemo.main(SoftReferenceDemo.java:25)
但是有的業(yè)務(wù)場(chǎng)景柄延,需要我們?cè)趦?nèi)存不夠用蚀浆,可以釋放掉一些不必要的數(shù)據(jù)。比如我們?cè)诰彺嬷写娴挠脩粜畔ⅰ?/p>
軟引用
jdk 從 1.2 開始加入了 Reference ,SoftReference 是其中一個(gè)分類搜吧,它的作用是市俊,通過 GC root 到達(dá)對(duì)象 a,僅有 SoftReference 滤奈,對(duì)象 a 將會(huì)在jvm oom 之前摆昧,被 jvm gc 釋放掉。
無限循環(huán)往 List 添加 10m 左右大小的數(shù)據(jù)(SoftReference)僵刮,發(fā)現(xiàn)沒有出現(xiàn) oom据忘。
// -Xmx600mpublicclassSoftReferenceDemo{// 1mprivatestaticint_1M =1024*1024*1;publicstaticvoidmain(String[] args) throws InterruptedException{? ? ? ? ArrayList objects = Lists.newArrayListWithCapacity(50);intcount =1;while(true) {? ? ? ? ? ? Thread.sleep(500);// 獲取 jvm 空閑的內(nèi)存為多少 mlongmeme_free = Runtime.getRuntime().freeMemory() / _1M;if((meme_free -10) >=0) {? ? ? ? ? ? ? ? Demo demo =newDemo(count);? ? ? ? ? ? ? ? SoftReference demoSoftReference =newSoftReference<>(demo);? ? ? ? ? ? ? ? objects.add(demoSoftReference);? ? ? ? ? ? ? ? count++;// demo 為 null,只有 demoSoftReference 一條引用到達(dá) Demo 的實(shí)例,GC 將會(huì)在 oom 之前回收 Demo 的實(shí)例demo =null;? ? ? ? ? ? }? ? ? ? ? ? System.out.println("jvm 空閑內(nèi)存"+ meme_free +" m");? ? ? ? ? ? System.out.println(objects.size());? ? ? ? }? ? }? ? @DatastaticclassDemo{privatebyte[] a =newbyte[_1M *10];privateString str;publicDemo(inti){this.str = String.valueOf(i);? ? ? ? }? ? }}
通過 jvisualvm 查看 jvm 堆的使用搞糕,可以看到堆在要溢出的時(shí)候就會(huì)回收掉勇吊,空閑的內(nèi)存很大的時(shí)候,你主動(dòng)執(zhí)行 執(zhí)行垃圾回收窍仰,內(nèi)存是不會(huì)回收的汉规。
弱引用
對(duì)象 demo 的引用只有 WeakReference 可達(dá)時(shí),會(huì)在 gc 之后回收 demo 釋放掉內(nèi)存驹吮。
以下程序也會(huì)一直不停的運(yùn)行针史,只是內(nèi)存釋放的時(shí)機(jī)不同而已
// -Xmx600m -XX:+PrintGCDetailspublicclassWeakReferenceDemo{// 1mprivatestaticint_1M =1024*1024*1;publicstaticvoidmain(String[] args) throws InterruptedException{? ? ? ? ArrayList objects = Lists.newArrayListWithCapacity(50);intcount =1;while(true) {? ? ? ? ? ? Thread.sleep(100);// 獲取 jvm 空閑的內(nèi)存為多少 mlongmeme_free = Runtime.getRuntime().freeMemory() / _1M;if((meme_free -10) >=0) {? ? ? ? ? ? ? ? Demo demo =newDemo(count);? ? ? ? ? ? ? ? WeakReference demoWeakReference =newWeakReference<>(demo);? ? ? ? ? ? ? ? objects.add(demoWeakReference);? ? ? ? ? ? ? ? count++;? ? ? ? ? ? ? ? demo =null;? ? ? ? ? ? }? ? ? ? ? ? System.out.println("jvm 空閑內(nèi)存"+ meme_free +" m");? ? ? ? ? ? System.out.println(objects.size());? ? ? ? }? ? }? ? @DatastaticclassDemo{privatebyte[] a =newbyte[_1M *10];privateString str;publicDemo(inti){this.str = String.valueOf(i);? ? ? ? }? ? }}
運(yùn)行結(jié)果,SoftReference 可用內(nèi)存在快用盡的時(shí)候就會(huì)釋放掉內(nèi)存碟狞,而 WeakReference 每次可用內(nèi)存達(dá)到 360m 左右會(huì)進(jìn)行垃圾啄枕,而釋放掉內(nèi)存
[GC (Allocation Failure) [PSYoungGen: 129159K->1088K(153088K)]129175K->1104K(502784K), 0.0007990secs][Times: user=0.00 sys=0.00, real=0.00 secs]jvm空閑內(nèi)存364m36jvm空閑內(nèi)存477m
虛引用
也有稱呼為 幻靈引用,因?yàn)槟悴恢朗裁磿r(shí)候被回收族沃,所需必須配合 ReferenceQueue频祝,當(dāng)對(duì)象回收時(shí),可以從這個(gè)隊(duì)列拿到 PhantomReference 的實(shí)例脆淹。
// -Xmx600m -XX:+PrintGCDetailspublicclassPhantomReferenceDemo{// 1mprivatestaticint_1M =1024*1024*1;privatestaticReferenceQueue referenceQueue =newReferenceQueue();publicstaticvoidmain(String[] args) throws InterruptedException{? ? ? ? ArrayList objects = Lists.newArrayListWithCapacity(50);intcount =1;newThread(() -> {while(true) {try{? ? ? ? ? ? ? ? ? ? Referenceremove= referenceQueue.remove();// objects 可達(dá)性分析常空,可以到達(dá) PhantomReference<Demo>,內(nèi)存是不能及時(shí)釋放的盖溺,我們需要在隊(duì)里中拿到那個(gè) Demo 被回收了漓糙,然后// 從 objects 移除這個(gè)對(duì)象if(objects.remove(remove)) {? ? ? ? ? ? ? ? ? ? ? ? System.out.println("移除元素");? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }).start();while(true) {? ? ? ? ? ? Thread.sleep(500);// 獲取 jvm 空閑的內(nèi)存為多少 mlongmeme_free = Runtime.getRuntime().freeMemory() / _1M;if((meme_free -10) >40) {? ? ? ? ? ? ? ? Demo demo =newDemo(count);? ? ? ? ? ? ? ? PhantomReference demoWeakReference =newPhantomReference<>(demo, referenceQueue);? ? ? ? ? ? ? ? objects.add(demoWeakReference);? ? ? ? ? ? ? ? count++;? ? ? ? ? ? ? ? demo =null;? ? ? ? ? ? }? ? ? ? ? ? System.out.println("jvm 空閑內(nèi)存"+ meme_free +" m");? ? ? ? ? ? System.out.println(objects.size());? ? ? ? }? ? }? ? @DatastaticclassDemo{privatebyte[] a =newbyte[_1M *10];privateString str;publicDemo(inti){this.str = String.valueOf(i);? ? ? ? }? ? }}
ThreadLocal
ThreadLocal 在我們實(shí)際開發(fā)中,用的還是比較多的烘嘱。那它到底是個(gè)什么東東呢(線程本地變量)昆禽,我們知道 局部變量 (方法內(nèi)定義的變量)和 成員變量 (類的屬性)蝗蛙。
有的時(shí)候呢,我們希望一個(gè)變量的生命周期可以貫穿整個(gè)線程的一個(gè)任務(wù)運(yùn)行周期(線程池中的線程可以分配執(zhí)行不同的任務(wù))为狸,在各個(gè)方法調(diào)用的時(shí)候我們可以拿到這個(gè)預(yù)先設(shè)置的變量歼郭,這就是 ThreadLocal 的作用。
比如我們想要拿到當(dāng)前請(qǐng)求的 HttpServletRequest辐棒,然后在當(dāng)前各個(gè)方法都可以獲取到,SpringBoot 已經(jīng)幫我們封裝好了牍蜂,RequestContextFilter 在每個(gè)請(qǐng)求過來之后漾根,都會(huì)通過 RequestContextHolder 設(shè)置線程本地變量,原理就是操作 ThreadLocal鲫竞。
ThreadLocal 只是針對(duì)當(dāng)前線程中的調(diào)用辐怕,跨線程調(diào)用是不行的,所以 Jdk 通過 InheritableThreadLocal 繼承 ThreadLocal 來實(shí)現(xiàn)从绘。
ThreadLocal 獲取當(dāng)前請(qǐng)求的用戶信息
看注釋大致就能明白 TheadLocal 怎么使用了
/** *@author張攀欽 *@date2018/12/21-22:59 */@RestControllerpublicclassUserInfoController{@RequestMapping("/user/info")publicUserInfoDTOgetUserInfoDTO(){returnUserInfoInterceptor.getCurrentRequestUserInfoDTO();? ? }}@Slf4jpublicclassUserInfoInterceptorimplementsHandlerInterceptor{privatestaticfinalThreadLocal THREAD_LOCAL =newThreadLocal();// 請(qǐng)求頭用戶名privatestaticfinalString USER_NAME ="userName";// 注意這個(gè)寄疏,只有注入到 ioc 中的 bean,才能注入進(jìn)來@AutowiredprivateIUserInfoService userInfoService;@OverridepublicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException{// 判斷是不是接口請(qǐng)求if(handlerinstanceofHandlerMethod) {? ? ? ? ? ? String userName = request.getHeader(USER_NAME);? ? ? ? ? ? UserInfoDTO userInfoByUserName = userInfoService.getUserInfoByUserName(userName);? ? ? ? ? ? THREAD_LOCAL.set(userInfoByUserName);returntrue;? ? ? ? }returnfalse;? ? }@OverridepublicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException{// 用完之后記得釋放掉內(nèi)存THREAD_LOCAL.remove();? ? }// 獲取當(dāng)前線程設(shè)置的用戶信息publicstaticUserInfoDTOgetCurrentRequestUserInfoDTO(){returnTHREAD_LOCAL.get();? ? }}@ConfigurationpublicclassWebMvcConfigimplementsWebMvcConfigurer{/**
? ? * 將 UserInfoInterceptor 注入到 ioc 容器中
? ? */@BeanpublicUserInfoInterceptorgetUserInfoInterceptor(){returnnewUserInfoInterceptor();? ? }@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){// 調(diào)用這個(gè)方法返回的就是 ioc 的 beanregistry.addInterceptor(getUserInfoInterceptor()).addPathPatterns("/**");? ? }}
InheritableThreadLocal
有的時(shí)候,我們希望當(dāng)前線程的局部變量的生命周期可以延伸到子線程 中僵井,父線程設(shè)置的變量陕截,在子線程拿到。 InheritableThreadLocal 就是提供了這個(gè)能力批什。
/**
* @author 張攀欽
* @date 2020-06-27-21:18
*/publicclassInheritableThreadLocalDemo{staticInheritableThreadLocal INHERITABLE_THREAD_LOCAL =newInheritableThreadLocal();staticThreadLocal THREAD_LOCAL =newThreadLocal<>();publicstaticvoidmain(String[] args) throws InterruptedException{? ? ? ? INHERITABLE_THREAD_LOCAL.set("父線程中使用 InheritableThreadLocal 設(shè)置變量");? ? ? ? THREAD_LOCAL.set("父線程中使用 ThreadLocal 設(shè)置變量");? ? ? ? Thread thread =newThread(? ? ? ? ? ? ? ? () -> {// 能拿到設(shè)置的變量System.out.println("從 InheritableThreadLocal 拿父線程設(shè)置的變量: "+ INHERITABLE_THREAD_LOCAL.get());// 打印為 nullSystem.out.println("從 ThreadLocal 拿父線程設(shè)置的變量: "+ THREAD_LOCAL.get());? ? ? ? ? ? ? ? }? ? ? ? );? ? ? ? thread.start();? ? ? ? thread.join();? ? }}
ThreadLocal get 方法源碼分析
你可以理解 Thead 對(duì)象有個(gè)屬性 Map农曲,它的 key 是 ThreadLoal 實(shí)例,獲取線程局部變量的源碼
publicclassThreadLocal{publicTget() {// 獲取運(yùn)行在那個(gè)線程中Thread t = Thread.currentThread();// 從 Thread 拿 Map ThreadLocalMap map = getMap(t);if(map !=null) {// 使用 ThreadLocal 實(shí)例從 Map 獲取值ThreadLocalMap.Entry e = map.getEntry(this);if(e !=null) {@SuppressWarnings("unchecked")T result = (T)e.value;returnresult;? ? ? ? ? ? }? ? ? ? }// 初始化 Map,并返回初始化值驻债,默認(rèn)為 null乳规,你可以定義方法,從這個(gè)方法加載初始化值returnsetInitialValue();? ? }}
InheritableThreadLocal 獲取父線程設(shè)置的數(shù)據(jù)分析
每個(gè) Thread 還有一個(gè) Map 屬性為 inheritableThreadLocals合呐,用于保存從父線程復(fù)制過來的 value 暮的。
當(dāng)初始化子線程的時(shí)候,它會(huì)將父線程的 Map (inheritableThreadLocals) 的值復(fù)制到自己的 Thead Map (inheritableThreadLocals)過來淌实,每個(gè)線程維護(hù)自己的 inheritableThreadLocals冻辩, 所以子線程改不了父線程維護(hù)的數(shù)據(jù),只是子線程可以獲得父線程設(shè)置的數(shù)據(jù)翩伪。
publicclassThread{// 維護(hù)線程本地變量ThreadLocal.ThreadLocalMap threadLocals =null;// 維護(hù)可以子線程可以繼承的父線程的數(shù)據(jù)ThreadLocal.ThreadLocalMap inheritableThreadLocals =null;// 線程初始化publicThread(ThreadGroupgroup, Runnable target, String name,longstackSize){? ? ? ? init(group, target, name, stackSize);? ? }privatevoidinit(ThreadGroup g, Runnable target, String name,longstackSize, AccessControlContext acc,? ? ? ? ? ? ? ? ? ? ? boolean inheritThreadLocals){if(inheritThreadLocals && parent.inheritableThreadLocals !=null){// 將父線程的 inheritableThreadLocals 數(shù)據(jù)復(fù)制到子線程中去this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);? ? ? ? }? ? }}publicclassTheadLocal{staticThreadLocalMapcreateInheritedMap(ThreadLocalMap parentMap){///創(chuàng)建自己線程的 Map,將父線程的值復(fù)制進(jìn)去returnnewThreadLocalMap(parentMap);? ? }staticclassThreadLocalMap{privateThreadLocalMap(ThreadLocalMap parentMap){? ? ? ? ? ? Entry[] parentTable = parentMap.table;intlen = parentTable.length;? ? ? ? ? ? setThreshold(len);? ? ? ? ? ? table =newEntry[len];// 遍歷父線程微猖,將數(shù)據(jù)復(fù)制過來for(intj =0; j < len; j++) {? ? ? ? ? ? ? ? Entry e = parentTable[j];if(e !=null) {? ? ? ? ? ? ? ? ? ? @SuppressWarnings("unchecked")? ? ? ? ? ? ? ? ? ? ThreadLocal key = (ThreadLocal) e.get();if(key !=null) {? ? ? ? ? ? ? ? ? ? ? ? Objectvalue= key.childValue(e.value);? ? ? ? ? ? ? ? ? ? ? ? Entry c =newEntry(key,value);inth = key.threadLocalHashCode & (len -1);while(table[h] !=null)? ? ? ? ? ? ? ? ? ? ? ? ? ? h = nextIndex(h, len);? ? ? ? ? ? ? ? ? ? ? ? table[h] = c;? ? ? ? ? ? ? ? ? ? ? ? size++;? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }}
demo 驗(yàn)證,以上分析
內(nèi)存泄漏原因
定義了一個(gè) 20 大小的線程池缘屹,執(zhí)行 50 次任務(wù),執(zhí)行完之后凛剥,將 threadLocal 置為 null,模擬內(nèi)存泄漏的場(chǎng)景 轻姿。為了排除干擾因素犁珠,我設(shè)置 jvm 參數(shù)為 -Xms8g -Xmx8g -XX:+PrintGCDetails
publicclassThreadLocalDemo{privatestaticExecutorService executorService = Executors.newFixedThreadPool(20);privatestaticThreadLocal threadLocal =newThreadLocal();publicstaticvoidmain(String[] args)throwsInterruptedException{for(inti =0; i <50; i++) {? ? ? ? ? ? executorService.submit(() -> {try{? ? ? ? ? ? ? ? ? ? threadLocal.set(newDemo());? ? ? ? ? ? ? ? ? ? Thread.sleep(50);? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }finally{if(Objects.nonNull(threadLocal)) {// 為防止內(nèi)存泄漏,當(dāng)前線程用完,清除掉 value//? ? ? ? ? ? ? ? ? ? ? ? threadLocal.remove();}? ? ? ? ? ? ? ? }? ? ? ? ? ? });? ? ? ? }? ? ? ? Thread.sleep(5000);? ? ? ? threadLocal =null;while(true) {? ? ? ? ? ? Thread.sleep(2000);? ? ? ? }? ? }@DatastaticclassDemo{//privateDemo[] demos =newDemo[1024*1024*5];? ? }}
運(yùn)行程序逻炊,沒有打印 gc 日志,說明沒有進(jìn)行垃圾回收
在 Java VisualVM 中我們 執(zhí)行垃圾回收犁享,回收之后的內(nèi)存分布余素,這個(gè) 20 個(gè) ThreadLocalDemo$Demo[] 是回收不了的,這就是內(nèi)存泄漏炊昆。
程序循環(huán) 50 次創(chuàng)建了 50 個(gè) Demo 桨吊,程序運(yùn)行期間是不會(huì)觸發(fā)垃圾回收(設(shè)置 jvm 參數(shù)保證的),所以 ThreadLocalDemo$Demo[] 存活的實(shí)例數(shù)為 50凤巨。
當(dāng)我手動(dòng)觸發(fā)了 GC视乐,實(shí)例數(shù)降為 20,并不是我們期望的 0敢茁,這就是程序發(fā)生了內(nèi)存泄漏問題
為什么發(fā)生了內(nèi)存泄漏呢佑淀?
因?yàn)槊總€(gè)線程對(duì)應(yīng)一個(gè)Thread,線程池大小為 20 個(gè)彰檬。Thread 中有?
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 中有 Entry[] tables伸刃,k 為弱引用。當(dāng)我們將 threadLocal 置為 null 的時(shí)候逢倍,GC ROOT 到 ThreadLocalDemo$Demo[] 引用鏈還是存在的捧颅,只是 k 回收掉了,value 依然存在的瓶堕,tables 長(zhǎng)度是不會(huì)變的隘道,是不會(huì)被回收的。
ThreadLocal 在set 和 get 的時(shí)候郎笆,針對(duì) k 為 null 的情況做了優(yōu)化谭梗,會(huì)將對(duì)應(yīng)的 tables[i] 設(shè)置為 null。這樣單個(gè) Entry 就可以被回收了宛蚓。但是我們將 ThreadLocal 置為 null 之后激捏,不能操作方法調(diào)用了。只能等到 Thread 再次調(diào)用別的 ThreadLocal 時(shí)操作 ThreadLocalMap 時(shí)根據(jù)條件判斷凄吏,進(jìn)行 Map 的 rehash,將 k 為 null 的 Entry 刪除掉远舅。
上述問題解決也比較方便,線程使用完 線程局部變量痕钢,調(diào)用 remove 主動(dòng)清除 Entry 就可以了图柏。
原文鏈接:https://www.toutiao.com/a6843309373326885379/?log_from=58b0fd46edda2_1640263749333