实在是妙啊!Java中强软虚弱引用,居然还能这样去操作

前言

ThreadLocal 在什么情况下可能发生内存泄漏?如果你想清楚这个问题的来龙去脉,看源码是必不可少的,看了源码之后你发现, ThreadLocal 中用到 static class Entry extends WeakReference<ThreadLocal<?>> {} ,谜底实际就是使用了弱引用 WeakReference。

本文内容概要

强引用:Object o = new Object()

软引用:new SoftReference(o);

弱引用:new WeakReference(o);

虚引用:new PhantomReference(o);

ThreadLocal 的使用,及使用不当发生内存泄漏的原因

Jdk 1.2 增加了抽象类 Reference 和 SoftReference、WeakReference、PhantomReference,扩展了引用类型分类,达到对内存更细粒度的控制。

比如我们的缓存数据,当内存不够用的时候,我希望缓存可以释放内存,或者将缓存存入到堆外等。

但我们怎么区分哪些对象需要回收(垃圾回收算法,可达性分析),回收的时机,回收的时候可以让我们拿到回收的通知,所以 JDK 1.2 带来这几个引用类型。


强引用

强引用就是我们经常用到的方式:Object o = new Object()。垃圾回收时,强引用的变量是不会被回收,只有设置 o=null,jvm 通过可达性分析,没有 GC root 到达对象,垃圾回收器才会清理堆中的对象,释放内存。 当继续申请内存分配,就会 oom。

定义一个类 Demo,Demo 实例占用内存大小为 10m,不停往 list 添加 Demo 的示例,由于不能申请到内存分配,程序抛出 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 空闲的内存为多少 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 空闲内存"+ meme_free +" m");            System.out.println(objects.size());        }    }    @DatastaticclassDemo{privatebyte[] a =newbyte[_1M *10];privateString str;publicDemo(inti){this.str = String.valueOf(i);        }    }

以上代码运行结果,抛出 oom 程序停止

jvm空闲内存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)

但是有的业务场景,需要我们在内存不够用,可以释放掉一些不必要的数据。比如我们在缓存中存的用户信息。

软引用

jdk 从 1.2 开始加入了 Reference ,SoftReference 是其中一个分类,它的作用是,通过 GC root 到达对象 a,仅有 SoftReference ,对象 a 将会在jvm oom 之前,被 jvm gc 释放掉。

无限循环往 List 添加 10m 左右大小的数据(SoftReference),发现没有出现 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 空闲的内存为多少 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 一条引用到达 Demo 的实例,GC 将会在 oom 之前回收 Demo 的实例demo =null;            }            System.out.println("jvm 空闲内存"+ 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 堆的使用,可以看到堆在要溢出的时候就会回收掉,空闲的内存很大的时候,你主动执行 执行垃圾回收,内存是不会回收的。

弱引用

对象 demo 的引用只有 WeakReference 可达时,会在 gc 之后回收 demo 释放掉内存。

以下程序也会一直不停的运行,只是内存释放的时机不同而已

// -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 空闲的内存为多少 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 空闲内存"+ meme_free +" m");            System.out.println(objects.size());        }    }    @DatastaticclassDemo{privatebyte[] a =newbyte[_1M *10];privateString str;publicDemo(inti){this.str = String.valueOf(i);        }    }}

运行结果,SoftReference 可用内存在快用尽的时候就会释放掉内存,而 WeakReference 每次可用内存达到 360m 左右会进行垃圾,而释放掉内存

[GC (Allocation Failure) [PSYoungGen: 129159K->1088K(153088K)]129175K->1104K(502784K), 0.0007990secs][Times: user=0.00 sys=0.00, real=0.00 secs]jvm空闲内存364m36jvm空闲内存477m

虚引用

也有称呼为 幻灵引用,因为你不知道什么时候被回收,所需必须配合 ReferenceQueue,当对象回收时,可以从这个队列拿到 PhantomReference 的实例。

// -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 可达性分析,可以到达 PhantomReference<Demo>,内存是不能及时释放的,我们需要在队里中拿到那个 Demo 被回收了,然后// 从 objects 移除这个对象if(objects.remove(remove)) {                        System.out.println("移除元素");                    }                }catch(InterruptedException e) {                    e.printStackTrace();                }            }        }).start();while(true) {            Thread.sleep(500);// 获取 jvm 空闲的内存为多少 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 空闲内存"+ 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 在我们实际开发中,用的还是比较多的。那它到底是个什么东东呢(线程本地变量),我们知道 局部变量 (方法内定义的变量)和 成员变量 (类的属性)。

有的时候呢,我们希望一个变量的生命周期可以贯穿整个线程的一个任务运行周期(线程池中的线程可以分配执行不同的任务),在各个方法调用的时候我们可以拿到这个预先设置的变量,这就是 ThreadLocal 的作用。

比如我们想要拿到当前请求的 HttpServletRequest,然后在当前各个方法都可以获取到,SpringBoot 已经帮我们封装好了,RequestContextFilter 在每个请求过来之后,都会通过 RequestContextHolder 设置线程本地变量,原理就是操作 ThreadLocal。

ThreadLocal 只是针对当前线程中的调用,跨线程调用是不行的,所以 Jdk 通过 InheritableThreadLocal 继承 ThreadLocal 来实现。

ThreadLocal 获取当前请求的用户信息

看注释大致就能明白 TheadLocal 怎么使用了

/** *@author张攀钦 *@date2018/12/21-22:59 */@RestControllerpublicclassUserInfoController{@RequestMapping("/user/info")publicUserInfoDTOgetUserInfoDTO(){returnUserInfoInterceptor.getCurrentRequestUserInfoDTO();    }}@Slf4jpublicclassUserInfoInterceptorimplementsHandlerInterceptor{privatestaticfinalThreadLocal THREAD_LOCAL =newThreadLocal();// 请求头用户名privatestaticfinalString USER_NAME ="userName";// 注意这个,只有注入到 ioc 中的 bean,才能注入进来@AutowiredprivateIUserInfoService userInfoService;@OverridepublicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException{// 判断是不是接口请求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{// 用完之后记得释放掉内存THREAD_LOCAL.remove();    }// 获取当前线程设置的用户信息publicstaticUserInfoDTOgetCurrentRequestUserInfoDTO(){returnTHREAD_LOCAL.get();    }}@ConfigurationpublicclassWebMvcConfigimplementsWebMvcConfigurer{/**

    * 将 UserInfoInterceptor 注入到 ioc 容器中

    */@BeanpublicUserInfoInterceptorgetUserInfoInterceptor(){returnnewUserInfoInterceptor();    }@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){// 调用这个方法返回的就是 ioc 的 beanregistry.addInterceptor(getUserInfoInterceptor()).addPathPatterns("/**");    }}

InheritableThreadLocal

有的时候,我们希望当前线程的局部变量的生命周期可以延伸到子线程 中,父线程设置的变量,在子线程拿到。 InheritableThreadLocal 就是提供了这个能力。

/**

* @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 设置变量");        THREAD_LOCAL.set("父线程中使用 ThreadLocal 设置变量");        Thread thread =newThread(                () -> {// 能拿到设置的变量System.out.println("从 InheritableThreadLocal 拿父线程设置的变量: "+ INHERITABLE_THREAD_LOCAL.get());// 打印为 nullSystem.out.println("从 ThreadLocal 拿父线程设置的变量: "+ THREAD_LOCAL.get());                }        );        thread.start();        thread.join();    }}

ThreadLocal get 方法源码分析

你可以理解 Thead 对象有个属性 Map,它的 key 是 ThreadLoal 实例,获取线程局部变量的源码

publicclassThreadLocal{publicTget() {// 获取运行在那个线程中Thread t = Thread.currentThread();// 从 Thread 拿 Map ThreadLocalMap map = getMap(t);if(map !=null) {// 使用 ThreadLocal 实例从 Map 获取值ThreadLocalMap.Entry e = map.getEntry(this);if(e !=null) {@SuppressWarnings("unchecked")T result = (T)e.value;returnresult;            }        }// 初始化 Map,并返回初始化值,默认为 null,你可以定义方法,从这个方法加载初始化值returnsetInitialValue();    }}

InheritableThreadLocal 获取父线程设置的数据分析

每个 Thread 还有一个 Map 属性为 inheritableThreadLocals,用于保存从父线程复制过来的 value 。

当初始化子线程的时候,它会将父线程的 Map (inheritableThreadLocals) 的值复制到自己的 Thead Map (inheritableThreadLocals)过来,每个线程维护自己的 inheritableThreadLocals, 所以子线程改不了父线程维护的数据,只是子线程可以获得父线程设置的数据。

publicclassThread{// 维护线程本地变量ThreadLocal.ThreadLocalMap threadLocals =null;// 维护可以子线程可以继承的父线程的数据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 数据复制到子线程中去this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);        }    }}publicclassTheadLocal{staticThreadLocalMapcreateInheritedMap(ThreadLocalMap parentMap){///创建自己线程的 Map,将父线程的值复制进去returnnewThreadLocalMap(parentMap);    }staticclassThreadLocalMap{privateThreadLocalMap(ThreadLocalMap parentMap){            Entry[] parentTable = parentMap.table;intlen = parentTable.length;            setThreshold(len);            table =newEntry[len];// 遍历父线程,将数据复制过来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 验证,以上分析




内存泄漏原因

定义了一个 20 大小的线程池,执行 50 次任务,执行完之后,将 threadLocal 置为 null,模拟内存泄漏的场景 。为了排除干扰因素,我设置 jvm 参数为 -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)) {// 为防止内存泄漏,当前线程用完,清除掉 value//                        threadLocal.remove();}                }            });        }        Thread.sleep(5000);        threadLocal =null;while(true) {            Thread.sleep(2000);        }    }@DatastaticclassDemo{//privateDemo[] demos =newDemo[1024*1024*5];    }}

运行程序,没有打印 gc 日志,说明没有进行垃圾回收

在 Java VisualVM 中我们 执行垃圾回收,回收之后的内存分布,这个 20 个 ThreadLocalDemo$Demo[] 是回收不了的,这就是内存泄漏。


程序循环 50 次创建了 50 个 Demo ,程序运行期间是不会触发垃圾回收(设置 jvm 参数保证的),所以 ThreadLocalDemo$Demo[] 存活的实例数为 50。

当我手动触发了 GC,实例数降为 20,并不是我们期望的 0,这就是程序发生了内存泄漏问题

为什么发生了内存泄漏呢?

因为每个线程对应一个Thread,线程池大小为 20 个。Thread 中有 

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 中有 Entry[] tables,k 为弱引用。当我们将 threadLocal 置为 null 的时候,GC ROOT 到 ThreadLocalDemo$Demo[] 引用链还是存在的,只是 k 回收掉了,value 依然存在的,tables 长度是不会变的,是不会被回收的。


ThreadLocal 在set 和 get 的时候,针对 k 为 null 的情况做了优化,会将对应的 tables[i] 设置为 null。这样单个 Entry 就可以被回收了。但是我们将 ThreadLocal 置为 null 之后,不能操作方法调用了。只能等到 Thread 再次调用别的 ThreadLocal 时操作 ThreadLocalMap 时根据条件判断,进行 Map 的 rehash,将 k 为 null 的 Entry 删除掉。

上述问题解决也比较方便,线程使用完 线程局部变量,调用 remove 主动清除 Entry 就可以了。

原文链接:https://www.toutiao.com/a6843309373326885379/?log_from=58b0fd46edda2_1640263749333

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容