0%

Java Reference

简介

众所周知,Java不需要像C一样在创建对象之后,通过free来销毁对象,因为Java会有专门的gc程序通过可达性分析来回收不用的对象,而可达性分析使用的是引用,Java中引用分为强引用、弱引用(WeakReference)、软引用(SoftReference)、虚引用(PhantomReference),不同的引用类型在gc的时候会有不同的处理策略,下面做简要分析。

强引用

最常见的引用,new出来的对象就是强引用,只要有强引用的对象就不会被gc程序回收

弱引用(WeakReference)

在gc扫到弱引用的时候,就会回收弱引用所指向的对象(前提是它没有别的强引用指向了),可以通过下面的代码佐证

1
2
3
4
WeakReference<Object> weak = new WeakReference<>(new Object());
System.out.println(weak.get());// 返回object
System.gc();
System.out.println(weak.get());// 返回null

软引用(SoftReference)

软引用是在内存不够的时候,gc程序会去回收,我们先将最大堆内存设置成6M(-Xmx6m),运行下面的代码验证

1
2
3
4
5
6
// -Xmx6m
SoftReference<int[]> soft = new SoftReference<>(new int[512 * 1024]);
System.out.println(soft.get());// 返回数组,因为内存还够
int[] arr2 = new int[512 * 1024]; // 内存不够了,会gc
arr2[0] = 1;
System.out.println(soft.get());// 返回null,因为内存不够了

虚引用(PhantomReference)

虚引用,创建之后,get出来的值一直都是null,创建虚引用的时候必须要传一个引用队列进去,会在被引用对象gc的时候将引用放到引用队列中,然后我们可以通过探测这个引用队列来检测对象的释放过程,在对象释放的时候做一些事情,运行下面的代码验证

1
2
3
4
5
6
7
8
9
10
11
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference<Object> soft = new PhantomReference<>(new Object(), referenceQueue);
System.out.println(soft.get());// 返回null
System.out.println(referenceQueue.poll());// poll不出来,因为没有gc
System.gc();
Thread.sleep(5000);
Reference poll = referenceQueue.poll();// 可以poll出来,因为gc了
System.out.println(poll);
if (poll != null) {
System.out.println(poll.get());
}

虚引用的一个典型的应用,是在Java的DirectBuffer中,它控制了直接内存可以由gc程序去回收,而不用手动回收,
我们默认的new对象都是在堆内创建的,会被jvm管理,如果需要创建不受jvm的gc管理的内存(称为堆外内存),则需要通过Unsafe调用native方法进行分配

1
2
3
4
5
6
7
8
9
10
11
private static void allocateDirect() {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
byteBuffer.put(0, (byte) 0x55);
System.out.println(byteBuffer.get(0));
}
public static void main(String[] args) throws InterruptedException {
allocateDirect();
System.gc();
Thread.sleep(5000);
System.exit(0);
}

上面的代码需要点进去才能看到,在创建DirectBuffer的时候,会创建一个Cleaner(继承了PhantomReference),里面带一个Deallocator专门负责调用Unsafe去释放堆外内存,

1
2
// Cleaner是PhantomReference的实现类,虚引用在引用队列中被poll出来之后,也是get不出来东西的,需要根据需要把一些数据放到PhantomReference中或者存储一个Reference到这些数据的一个对应关系map,才能在poll出来之后做一些实际的操作
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

Cleaner的clean方法中会调用Deallocator,而clean方法会被Reference中的静态变量ReferenceHandler线程池调用,线程池会不断获取最近被回收的引用(pending),然后调用tryHandlePending方法去尝试执行Cleaner的clean方法。
注意到这个最近被回收的引用(pending)是垃圾回收器给设置值进去的,这种交互方式会感觉挺奇怪的,但是没有办法,gc程序不存在于java代码库中

1
2
3
4
5
6
/* List of References waiting to be enqueued.  The collector adds
* References to this list, while the Reference-handler thread removes
* them. This list is protected by the above lock object. The
* list uses the discovered field to link its elements.
*/
private static Reference<Object> pending = null;

总结

引用是很老旧的概念,实际应用比较狭窄,在Guava Cache中有WeakKey、WeakValue、SoftValue的设置来将key、value设置成不同的引用,但是实际使用的时候基本都是用缓存时间或者是缓存的大小来控制缓存,很少有用引用。而虚引用也只在一些特殊的情况下使用,总的来说这些都是有用的概念,但是只会在很关键的时候使用,平时使用很少。

Reference

  1. Netty源码—七、内存释放
如果您觉得这些内容对您有帮助,你可以赞助我以提高站点的文章质量