浅谈Java引用和Threadlocal的那些事
1 背景
某一天在某一个群里面的某个群友突然提出了一个问题:"threadlocal的key是虚引用,那么在threadlocal.get()的时候,发生gc之后,key是否是null?"屏幕前的你可以好好的想想这个问题,在这里我先卖个关子,先讲讲java中引用和threadlocal的那些事。
2 java中的引用
对于很多java初学者来说,会把引用和对象给搞混淆。下面有一段代码,
user zhangsan = new user("zhangsan", 24);
这里先提个问题zhangsan到底是引用还是对象呢?很多人会认为zhangsan是个对象,如果你也是这样认为的话那么再看一下下面一段代码
user zhangsan; zhangsan = new user("zhangsan", 24);
这段代码和开始的代码其实执行效果是一致的,这段代码的第一行user zhangsan,定义了zhangsan,那你认为zhangsan还是对象吗?如果你还认为的话,那么这个对象应该是什么呢?的确,zhangsan其实只是一个引用,对jvm内存划分熟悉的同学应该熟悉下面的图片:
其实zhangsan是栈中分配的一个引用,而new user("zhangsan", 24)是在堆中分配的一个对象。而'='的作用是用来将引用指向堆中的对象的。就像你叫张三但张三是个名字而已并不是一个实际的人,他只是指向的你。
我们一般所说的引用其实都是代指的强引用,在jdk1.2之后引用不止这一种,一般来说分为四种:强引用,软引用,弱引用,虚引用。而接下来我会一一介绍这四种引用。
2.1 强引用
上面我们说过了 user zhangsan = new user("zhangsan", 24);这种就是强引用,有点类似c的指针。对强引用他的特点有下面几个:
强引用可以直接访问目标对象。
只要这个对象被强引用所关联,那么垃圾回收器都不会回收,那怕是抛出oom异常。
容易导致内存泄漏。
2.2 软引用
在java中使用softreference帮助我们定义软引用。其构造方法有两个:
public softreference(t referent); public softreference(t referent, referencequeue<? super t> q);
两个构造方法相似,第二个比第一个多了一个引用队列,在构造方法中的第一个参数就是我们的实际被指向的对象,这里用新建一个softreference来替代我们上面强引用的等号。 下面是构造软引用的例子:
softzhangsan = new softreference(new user("zhangsan", 24));
2.2.1软引用有什么用?
如果某个对象他只被软引用所指向,那么他将会在内存要溢出的时候被回收,也就是当我们要出现oom的时候,如果回收了一波内存还不够,这才抛出oom,弱引用回收的时候如果设置了引用队列,那么这个软引用还会进一次引用队列,但是引用所指向的对象已经被回收。这里要和下面的弱引用区分开来,弱引用是只要有垃圾回收,那么他所指向的对象就会被回收。下面是一个代码例子:
public static void main(string[] args) { referencequeue<user> referencequeue = new referencequeue(); softreference softreference = new softreference(new user("zhangsan",24), referencequeue); //手动触发gc system.gc(); thread.sleep(1000); system.out.println("手动触发gc:" + softreference.get()); system.out.println("手动触发的队列:" + referencequeue.poll()); //通过堆内存不足触发gc makeheapnotenough(); system.out.println("通过堆内存不足触发gc:" + softreference.get()); system.out.println("通过堆内存不足触发gc:" + referencequeue.poll()); } private static void makeheapnotenough() { softreference softreference = new softreference(new byte[1024*1024*5]); byte[] bytes = new byte[1024*1024*5]; } 输出: 手动触发gc:user{name='zhangsan', age=24} 手动触发的队列:null 通过堆内存不足触发gc:null 通过堆内存不足触发gc:java.lang.ref.softreference@4b85612c
通过-xmx10m设置我们堆内存大小为10,方便构造堆内存不足的情况。可以看见我们输出的情况我们手动调用system.gc并没有回收我们的软引用所指向的对象,只有在内存不足的情况下才能触发。
2.2.2软应用的应用
在softreference的doc中有这么一句话:
soft references are most often used to implement memory-sensitive caches
也就是说软引用经常用来实现内存敏感的高速缓存。怎么理解这句话呢?我们知道软引用他只会在内存不足的时候才触发,不会像强引用那用容易内存溢出,我们可以用其实现高速缓存,一方面内存不足的时候可以回收,一方面也不会频繁回收。在高速本地缓存caffeine中实现了软引用的缓存,当需要缓存淘汰的时候,如果是只有软引用指向那么久会被回收。不熟悉caffeine的同学可以阅读深入理解caffeine
2.3 弱引用
弱引用在java中使用weakreference来定义一个弱引用,上面我们说过他比软引用更加弱,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收。这里我们就不多废话了,直接上例子:
public static void main(string[] args) { weakreference weakreference = new weakreference(new user("zhangsan",24)); system.gc(); system.out.println("手动触发gc:" + weakreference.get()); } 输出结果: 手动触发gc:null
可以看见上面的例子只要垃圾回收一触发,该对象就被回收了。
2.3.1 弱引用的作用
在weakreference的注释中写到:
weak references are most often used to implement canonicalizing mappings.
从中可以知道虚引用更多的是用来实现canonicalizing mappings(规范化映射)。在jdk中weakhashmap很好的体现了这个例子:
public static void main(string[] args) throws exception { weakhashmap<user, string> weakhashmap = new weakhashmap(); //强引用 user zhangsan = new user("zhangsan", 24); weakhashmap.put(zhangsan, "zhangsan"); system.out.println("有强引用的时候:map大小" + weakhashmap.size()); //去掉强引用 zhangsan = null; system.gc(); thread.sleep(1000); system.out.println("无强引用的时候:map大小"+weakhashmap.size()); } 输出结果为: 有强引用的时候:map大小1 无强引用的时候:map大小0
可以看出在gc之后我们在map中的键值对就被回收了,在weakhashmap中其实只有key是虚引用做关联的,然后通过引用队列再去对我们的map进行回收处理。
2.4 虚引用
虚引用是最弱的引用,在java中使用phantomreference进行定义。弱到什么地步呢?也就是你定义了虚引用根本无法通过虚引用获取到这个对象,更别谈影响这个对象的生命周期了。在虚引用中唯一的作用就是用队列接收对象即将死亡的通知。
public static void main(string[] args) throws exception { referencequeue referencequeue = new referencequeue(); phantomreference phantomreference = new phantomreference(new user("zhangsan", 24), referencequeue); system.out.println("什么也不做,获取:" + phantomreference.get()); } 输出结果: 什么也不做,获取:null
在phantomreference的注释中写到:
phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the java finalization mechanism.
虚引用得最多的就是在对象死前所做的清理操作,这是一个比java的finalization梗灵活的机制。 在directbytebuffer中使用cleaner用来回收对外内存,cleaner是phantomreference的子类,当directbytebuffer被回收的时候未防止内存泄漏所以通过这种方式进行回收,有点类似于下面的代码:
public static void main(string[] args) throws exception { cleaner.create(new user("zhangsan", 24), () -> {system.out.println("我被回收了,当前线程:{}"+ thread.currentthread().getname());}); system.gc(); thread.sleep(1000); } 输出: 我被回收了,当前线程:reference handler
3 threadlocal
threadlocal是一个本地线程副本变量工具类,基本在我们的代码中随处可见。这里就不过多的介绍他了。
3.1 threadlocal和弱引用的那些事
上面说了这么多关于引用的事,这里终于回到了主题了我们的threadlocal和弱引用有什么关系呢?
在我们的thread类中有下面这个变量:
threadlocal.threadlocalmap threadlocals
threadlocalmap本质上也是个map,其中key是我们的threadlocal这个对象,value就是我们在threadlocal中保存的值。也就是说我们的threadlocal保存和取对象都是通过thread中的threadlocalmap来操作的,而key就是本身。在threadlocalmap中entry有如下定义:
static class entry extends weakreference<threadlocal<?>> { /** the value associated with this threadlocal. */ object value; entry(threadlocal<?> k, object v) { super(k); value = v; } }
可以看见entry是weakreference的子类,而这个虚引用所关联的对象正是我们的threadlocal这个对象。我们又回到上面的问题:
"threadlocal的key是虚引用,那么在threadlocal.get()的时候,发生gc之后,key是否是null?"
这个问题晃眼一看,虚引用嘛,还有垃圾回收那肯定是为null,这其实是不对的,因为题目说的是在做threadlocal.get()操作,证明其实还是有强引用存在的。所以key并不为null。如果我们的强引用不存在的话,那么key就会被回收,也就是会出现我们value没被回收,key被回收,导致value永远存在,出现内存泄漏。这也是threadlocal经常会被很多书籍提醒到需要remove()的原因。
你也许会问看到很多源码的threadlocal并没有写remove依然再用得很好呢?那其实是因为很多源码经常是作为静态变量存在的生命周期和class是一样的,而remove需要再那些方法或者对象里面使用threadlocal,因为方法栈或者对象的销毁从而强引用丢失,导致内存泄漏。
3.2 fastthreadlocal
fastthreadlocal是netty中提供的高性能本地线程副本变量工具。在netty的io.netty.util中提供了很多牛逼的工具,后续会一一给大家介绍,这里就先说下fastthreadlocal。
fastthreadlocal有下面几个特点:
使用数组代替threadlocalmap存储数据,从而获取更快的性能。(缓存行和一次定位,不会有hash冲突)
由于使用数组,不会出现key回收,value没被回收的尴尬局面,所以避免了内存泄漏。
总结
文章开头的问题,为什么会被问出来,其实是对虚引用和threadlocal理解不深导致,很多时候只记着一个如果是虚引用,在垃圾回收时就会被回收,就会导致把这个观念先入为主,没有做更多的分析思考。所以大家再分析一个问题的时候还是需要更多的站在不同的场景上做更多的思考。
以上所述是小编给大家介绍的java引用和threadlocal的那些事,希望对大家有所帮助