本篇博客主要讲解了java的四种引用类型
1. 前言——垃圾回收如何判断对象已“死”?
在堆中基本存放了java中所有的对象实例,垃圾收集器在对堆进行回收的时候,首先要确认的就是哪些对象还“存活”着,哪些对象已经“死去”(即不再被任何途径使用)。而在判断对象存活的方法中,主要有两种。
- 引用计数法: 引用计数法主要是给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1,当引用失效的时候,计数减1;当计数器为0的时候,说明这个对象便不会再被使用。
- 可达性分析: 这个算法的思路就是通过一系列的称为“GC Roots”作为对象的起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),每当一个对象到 GC Roots 没有任何引用链相连,则证明此对象不可用。
在引用计数法这种方式中,如果两个对象没有其他的对象对其进行引用,但是这两个对象有相互引用的情况。则引用计数永远不会为0,导致不会被回收,即使他们已经不会再被使用了。具体示例如下:
1 | public class Test{ |
在示例中,即使test1和test2互相引用,但是方法执行完之后这两个对象已经不会再使用了,因此虽然引用计数不为 0,但也应该被GC所清理,在实际试验中也的确如此。说明java虚拟机采用的是 GC Roots的方式来清理对象。
在java语言中,可作为GC Roots的对象有以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。(即 static 变量)
- 方法区中常量引用的对象。(即 static final 变量)
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
2. 什么是四大引用类型?
为了能够描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还非常紧张,则可以抛弃这些对象。在JDK1.2之后,java对引用进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4中,这四种引用强度依次逐渐减弱。
- 强引用: 强引用就是指在程序代码之中普遍存在的,如通过 new 关键字创建、反射、clone、序列化等方式创建的对象,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用: 软引用是用来描述一些还有用但并非必须的对象,对于软引用关联的对象,一般状态下是不会被垃圾收集器回收,只有在系统将要发生内存溢出异常之前,才会将该引用的对象列入垃圾回收目标进行回收。在JDK1.2 之后,提供了 SoftReference 类来实现软引用。
- 弱引用: 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在JDK1.2 之后,提供了 WeakReference 类来实现弱引用。
- 虚引用: 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。为一个对象设置虚引用关联的唯一目的就是在这个对象被回收时收到一个系统通知。在JDK1.2 之后,提供了 PhantomReference类表示 类来实现虚引用。
强引用我们应该不用去过多详解,大家在开发中随处可见的就是强引用的运用。而软引用的使用场景之一便是作为缓存存在,将一些可能被频繁查询的数据保存在内存中,在内存紧张的时候也能被回收不至于出现 OOM。虚引用的话可以用来监控一些重要对象的GC回收状态,做一些日志记录(其实我也不知道它有哪些有用的使用场景)。至于弱引用,JAVA中我们常见的ThreadLocal就使用了它,他能用于保障一些频繁使用但生命周期短的对象处于一个线程隔离的状态,通过每个线程自带的ThreadLocalMap来存储这些对象,以此解决对象不能被多线程共享的问题。
3. 引用是否能持有多种?
开始讲解之前,我们看一下下面的一段代码:
1 | public static void main(String[] args) { |
当我们执行这段代码的时候,输出结果是 null,原理大家想必都知道,当我们调用 System.gc() 时,因为弱引用的关系,我们的Date()对象被回收,因此弱引用中所持有的对象被清理导致为null。而当我们把代码中的注释放开,我们会发现输出的结果不是 null,而是 “Fri Jun 12 18:27:33 CST 2020”。想必大家就开始很迷惑了,Date()不是个弱引用吗,为什么没有被垃圾回收,为什么还能输出?
答案就是:一个对象可以持有多种引用,在垃圾回收的时候,以其最强的引用对待此对象。在注释存在的时候,只有一条弱引用 weakReference —> new Date() 存在。因此,在GC的时候,new Date() 遵循弱引用的回收机制将其回收。而在注释放开的时候,则存在两条引用链,一条为强引用链 date —> new Date(),一条弱引用链 weakReference —> new Date()。在拥有多条引用的时候,GC回收则是按强引用回收对待 new Date(),因此new Date()并不会被回收。引用关系如图:
4. ThreadLocal内存泄漏的原因
首先我们大致了解一下ThreadLocal的原理。每个 Thread(线程)为了保证数据隔离,其内部存储了一个 ThreadLocal 的内部类 ThreadLocalMap,在使用的时候我们可以创建一个ThreadLocal,通过其 set() 方法,将对象保存在ThreadLocalMap中,其中 key 为创建的 ThreadLocal,value 为我们 set 的对象。
我们来看一个示例:
1 | public static void test() { |
在上述代码的使用中,会产生以下几条引用链:
上图中,实线代表强引用,虚线代表的是弱引用。我们发现ThreadLocal对象实际持有两个引用,一个我们代码第一行中显式 new 出来的 threadLocal 强引用,另一个则是通过 set() 方法在 Entry 中作为一个虚引用的 key。同时,也会有一条当前线程到 entry 的强引用,即 currentThread(当前线程)-> threadLocalMap -> entry。如果我们将外部强引用置为 null,那么 ThreadLocal 对象只有一条虚引用,在 GC 的时候势必会被回收,则此时的 key 为null。而此时仍然存在当前线程到 value 的一条强引用,虽然此时 map 的 key 为 null,该 value 永远不会被访问到,但强引用的存在导致其不能被回收,只有线程被销毁的时候,这条强引用才会断开。然而我们实际应用中都会使用线程池去维护线程,就导致其无法被回收,造成内存泄漏。
当然,实际上 ThreadLocal 在 set 和 get 的过程中也会去清除一些临近的过期的 value。但是这也不能完全避免内存泄漏的情况,因此我们在使用完对象的时候,最好调用 remove 方法清除 value。
5. 夹带私货
周杰伦出新歌了,歌名是《Mojito》,大家看完别忘了支持一波。虽说不及巅峰之作,但是异域的风格也是周董的一次突破了,中间的 RAP 部分也十分抓耳,保证听了不亏好伐。