介绍了Java中的强引用、软引用、弱引用、虚引用等四种对象引用,并且提供了测试案例,对虚引用的坑还做出了额外说明。
1 对象与引用
每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。
甚至连“Reference”都是一个类,该类代表对象的引用,它的子类有softReference、WeakReference、PhantomReference。通过get方法用来获取与引用对象关联的对象的引用,如果该对象被回收了,则返回null。注意PhantomReference的get永远返回null。
在 JDK1.2 之前,Java中的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。
所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)4 种,这 4 种引用的强度依次减弱,后三种引用都在java.lang.ref包中。
实际上在java.lang.ref包中还有一个FinalReference(继承了Reference)、Finalizer(继承了FinalReference)和ReferenceQueue(引用队列),实际上它们都与垃圾回收有关,可以看Java中的Finalizer类以及GC二次标记过程中的Java源码解析。
2 四种引用
2.1 强引用
强引用就是指在程序代码之中普遍存在的,指创建一个对象并把这个对象赋给一个引用变量,并没有像其它三种引用一样有一个就具体的类来描述。对于强引用对象,即使内存不足,JVM宁愿抛出OutOfMemoryError (OOM)错误也不会回收这种对象。
例如:
Object object =new Object();
String str ="hello";
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。比如各种集合的移除方法。
2.2 软引用
SoftReference< People > aSoftRef=new SoftReference< People >(obj)
软引用是用来描述一些还有用但并非必需的对象。只使用SoftReference类修饰的对象就是一个软引用对象(软可到达对象),如果一个对象只具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
常常用于缓存操作,把某个对象标记为软引用时,当内存足够就不会回收,内存不足就会回收,比如网页缓存,图片缓存等。并且软引用都会保证在虚拟机抛出OutOfMemoryError之前已经被清除。
软引用对象可以和一个引用队列(ReferenceQueue)联合使用。当jvm对软引用对象引用的对象回收后,会将此软引用对象放入关联的队列中。
2.3 弱引用
WeakReference< People > reference=new WeakReference< People >(obj);
弱引用也是用来描述非必需对象的。只使用WeakReference类修饰的对象就是一个弱引用对象(弱可达对象)。弱引用的对象相比软引用拥有更短暂的生命周期。无论内存是否足够,一旦下次垃圾回收器运行后扫描到弱引用,便会回收。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
如果对象被回收WeakReference的get方法就返回null。
弱引用对象可以和一个引用队列(ReferenceQueue)联合使用。当jvm对弱引用对象引用的对象回收后,会将此弱引用对象放入关联的队列中。
Java中有WeakHashMap类,以及ThreadLocal中的ThreadLocalMap内部节点Entry,他们都是WeakReference的实现,他们的key都与被弱引用对象关联,key被回收之后就代表这一个无效的Entry,Entry也可以被回收了。
2.4 虚引用
PhantomReference< People > pr = new PhantomReference< People > (object, ReferenceQueue);
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。
无法通过get获取该引用,但其实虚引用其实是持有对象引用的,只是PhantomReference的get方法的实现永远返回null。
一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时会被加入到相关引用队列中,常常被用于管理堆外内存的清理,因为堆外内存是GC不会自动清理,所以DirectByteBuffer等能够申请堆外内存的对象在创建时均与一个虚引用对象Cleaner关联,同时Cleaner中保存着申请的堆外内存的地址、大小等信息,当DirectByteBuffer对象被回收时,Cleaner会被加入到一个引用队列,此时GC会判断队列中的对象,并且通过Cleaner对象的clean方法对申请的堆外内存进行清理。
一个对象被关联成虚引用,是完全有可能影响其生命周期的,虚引用有潜在的内存泄露风险,因为JVM不会自动帮助我们释放,我们必须要保证它指向的堆对象是不可达的。
3 引用测试案例
代码如下:
public class ReferenceTest {
/**
* 1Mb内存
*/
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
//测试强引用
// testStrongReference();
//测试软引用
// testSoftReference();
//测试弱引用
// testWeakReference();
//测试虚引用
// testPhantomReference();
}
/**
* 测试强引用
*
* @throws InterruptedException
*/
public static void testStrongReference() throws InterruptedException {
byte[] StrongReference1, StrongReference2, StrongReference3, StrongReference4;
StrongReference1 = new byte[1 * _1MB];
StrongReference2 = new byte[1 * _1MB];
//到这里由于内存不足,所以虚拟机会自动尝试一次自动GC,但是由于是强引用,无法清除对象,造成OutOfMemoryError异常
StrongReference3 = new byte[1 * _1MB];
}
/**
* 测试软引用
*
* @throws InterruptedException
*/
public static void testSoftReference() throws InterruptedException {
ReferenceQueue<byte[]> objectReferenceQueue = new ReferenceQueue<>();
SoftReference softReference1, softReference2, softReference3, softReference4;
softReference1 = new SoftReference(new byte[1 * _1MB], objectReferenceQueue);
softReference2 = new SoftReference(new byte[1 * _1MB], objectReferenceQueue);
//到这里由于内存不足,虚拟机会自动尝试一次自动GC
softReference3 = new SoftReference(new byte[1 * _1MB], objectReferenceQueue);
//执行到这里实际上又GC了一次
softReference4 = new SoftReference(new byte[1 * _1MB], objectReferenceQueue);
System.out.println("第一次GC之后的值");
System.out.println(softReference1.get());
System.out.println(softReference2.get());
System.out.println(softReference3.get());
System.out.println(softReference4.get());
System.out.println("===========>");
System.out.println(objectReferenceQueue.poll());
System.out.println(objectReferenceQueue.poll());
System.out.println(objectReferenceQueue.poll());
System.out.println(objectReferenceQueue.poll());
//到这里,尝试手动使虚拟机GC一次,对于软引用,如果内存足够,GC是并不会回收对象的
System.gc();
Thread.sleep(500);
System.out.println("第二次GC之后的值");
System.out.println(softReference4.get());
System.out.println("===========>");
System.out.println(objectReferenceQueue.poll());
}
/**
* 测试弱引用
*
* @throws InterruptedException
*/
public static void testWeakReference() throws InterruptedException {
ReferenceQueue<byte[]> objectReferenceQueue = new ReferenceQueue<>();
WeakReference weakReference1, weakReference2, weakReference3, weakReference4;
weakReference1 = new WeakReference(new byte[1 * _1MB], objectReferenceQueue);
weakReference2 = new WeakReference(new byte[1 * _1MB], objectReferenceQueue);
//到这里由于内存不足,虚拟机会自动尝试一次自动GC
weakReference3 = new WeakReference(new byte[1 * _1MB], objectReferenceQueue);
//执行到这里实际上又GC了一次
weakReference4 = new WeakReference(new byte[1 * _1MB], objectReferenceQueue);
System.out.println("第一次GC之后的值");
System.out.println(weakReference1.get());
System.out.println(weakReference2.get());
System.out.println(weakReference3.get());
System.out.println(weakReference4.get());
System.out.println("===========>");
System.out.println(objectReferenceQueue.poll());
System.out.println(objectReferenceQueue.poll());
System.out.println(objectReferenceQueue.poll());
System.out.println(objectReferenceQueue.poll());
//到这里,尝试手动使虚拟机GC一次,,对于弱引用,即使内存足够,GC还是会回收对象的
System.gc();
Thread.sleep(500);
System.out.println("第二次GC之后的值");
System.out.println(weakReference4.get());
System.out.println("===========>");
System.out.println(objectReferenceQueue.poll());
}
/**
* 测试虚引用
*
* @throws InterruptedException
*/
public static void testPhantomReference() throws InterruptedException {
ReferenceQueue<byte[]> objectReferenceQueue = new ReferenceQueue<>();
PhantomReference phantomReference1, phantomReference2, phantomReference3;
phantomReference1 = new PhantomReference(new byte[1 * _1MB], objectReferenceQueue);
phantomReference2 = new PhantomReference(new byte[1 * _1MB], objectReferenceQueue);
System.gc();
Thread.sleep(500);
System.out.println(objectReferenceQueue.poll());
System.out.println(objectReferenceQueue.poll());
/*按照我们的思维,这是比软引用和弱引用还弱的引用,调用一次GC,虚引用关联的匿名对象会被GC掉,这看起来确实没错,并且已经被加入到了ReferenceQueue中(可以poll得到数据),但实际上在初始化下一个phantomReference3时还是会会抛出OOM异常,就像强引用一样
问题 就在于这个虚引用.在GC启动时,会将虚引用对象传到它的引用队列中去,这没错.但是却不会将虚引用的referent字段设置成null,
这样一来,也就不会释放虚引用指向的匿名数组的堆内存空间,看起来这个匿名数组被回收了,但实际上phantomReference的内部的referent已经持有了这个数组,造成了内存泄漏
使用时一定要注意这个问题*/
/*我们可以采用下面的操作,手动将PhantomReference的referent置为null,然后再次GC时,这样就会真正的清理内存空间*/
/*phantomReference1.clear();
phantomReference2.clear();*/
phantomReference3 = new PhantomReference(new byte[1 * _1MB], objectReferenceQueue);
}
}
测试前,我们首先将VM参数设置为:-Xms2M -Xmx3M 表示虚拟机启动内存2M,最大内存3M
3.1 虚引用的坑
软引用、弱引用以及虚引用可以与一个引用队列一起配合使用,实际上是将对象和Reference内部的referent字段关联上。
当弱引用和软引用引用的对象需要进行回收的时候,JVM都是先将其内部的referent字段设置成null,之后将软引用或弱引用引用的对象,加入到关联的引用队列中。也就是说JVM先回收堆对象占用的内存,然后才将软引用或弱引用加入到引用队列。
而虚引用(PhantomReference) 不同,他必须和引用队列 (ReferenceQueue)联合使用,若GC启动时,则将引用对象传到它的引用队列中去,但是不会将虚引用的referent字段设置成null, 也不会释放虚引用指向对象的堆内存空间,因此使用虚引用一定要注意内存泄漏。我们可以通过手动调用PhantomReference.clear()方法来释放虚引用指向对象的堆内存空间。
在与引用队列相关联的类构造器中,都会调用父类Reference的构造器:
/**
* SoftReference的构造器
* @param referent
* @param q
*/
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
/**
* WeakReference的构造器
* @param referent
* @param q
*/
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
/**
* PhantomReference的构造器
* @param referent
* @param q
*/
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
/**
* 父类Reference的构造器
* @param referent
* @param queue
*/
Reference(T referent, ReferenceQueue<? super T> queue) {
//这里将关联对象赋值给referent字段引用
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
在GC之时,软引用、弱引用对象的referent会被置空,并加入引用队列,但是虚引用的referent不会置空,我们使用断点DEBUG测试。
软引用测试:
我们查看softReference1中的数据,可以看到referent字段已经被置为null
弱引用测试:
我们查看weakReference1中的数据,可以看到referent字段已经被置为null
虚引用测试:
我们查看phantomReference1中的数据,可以看到referent字段没有被置为null,造成内存泄漏。
下面来看看clear方法的源码:
public void clear() {
this.referent = null;
}
将referent字段置空,这样上面的匿名数组就能真正的被回收了。因此在追踪对象之后要调用clear方法清除引用,但是即使这样做,还是需要下一次GC才能真正清理这对象,这已经算是改变对象的生命周期了。
4 总结
即便是弱引用和软引用能够自动回收, 但如果虚拟机来不及回收弱引用或软引用指向的对象时也是会抛出 java.lang.OutOfMemoryError: Java heap space 异常的。而虚引用则会明确改变对象的生命周期,甚至造成内存泄漏。
对于四种引用我们现在认识了一些,但是java.lang.ref包中还有一个FinalReference(继承了Reference)、Finalizer(继承了FinalReference)等类,他们是与可达性分析算法的二次表及相关联的,可以看看这篇文章,具体了解这些类的运作可以看这篇文章:Java中的Finalizer类以及GC二次标记过程中的Java源码解析。
相关文章:
- 《深入理解Java虚拟机》
- 《Java虚拟机规范》
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!