Java从1.2版本开始引入了4种引用,这4种引用分别是强引用、软引用、弱引用、虚引用,根据他们的引用级别依次为 强引用 > 软引用 > 弱引用 > 虚引用。
下面我就用几个例子分别说说他们几个的实现方式及其特点。
一、测试前的准备
1.1、建造测试对象
首先在测试之前,我们先创建一个对象,去继承Object类,重写finalize方法,以便后续更加清楚明了的查看gc回收的情况,代码如下:
public class Reference extends Object {
/**
* 创建一个大的属性去占用内存
*/
private byte[] jvmMemory = new byte[1024 * 1024];
@Override
protected void finalize() throws Throwable {
/**
* 重写finalize方法的目的是在gc的时候能打印下面这句话让我们知道对象什么时候被回收了
*/
super.finalize();
System.out.println("对象=" + this + "被gc回收");
}
}
1.2、测试对象大小设置jvm参数
其次我们可以借助工具openjdk测试一下这个对象的大小,以便于我们设置jvm的参数,控制jvm抛出oom,相关测试如下:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
package com.sxx.controller;
import com.sxx.bean.Reference;
import org.openjdk.jol.info.GraphLayout;
public class ObjectTest {
public static void main(String[] args) {
Reference reference = new Reference();
GraphLayout graphLayout = GraphLayout.parseInstance(reference);
//打印reference对象的在内存中占用的地址
System.out.println(graphLayout.toPrintable());
//打印reference的所有内存大小
System.out.println(graphLayout.toFootprint());
}
}
测试的结果如下:
相关解释:
第一个toPrintable方法打印的是reference这个实例对象在内存中的实际地址;
第二个toFootprint方法打印的是reference这个对象占用的实际内存大小,其中包含两个部分,count1表示jvmMemory数组占用了1048592,而count2表示reference对象占用了16,因此,最后一行tocal总共占用了1048592+16=1048608。
最终一个reference实例对象占用了1048608bytes,计算下来一个实例大概占用1M多一点的内存,
因此后续的测试类我们设置jvm都为-Xms6m -Xmx6m,方便触发GC以及抛出OOM。
二、测试
2.1、强引用
强引用就是程序中一般使用的引用类型,即我们直接new出来的对象。强引用的对象是可触及的,不会被回收,只要强引用关系存在,jvm宁肯抛出OutOfMemoryError也不会对具有与强引用的对象做回收。
具体可参考如下代码:
import com.sxx.bean.Reference;
/**
* 强引用测试
*/
public class StrongReferenceTest {
public static void main(String[] args) {
Reference r1 = new Reference();
System.out.println("r1=" + r1);
Reference r2 = new Reference();
System.out.println("r2=" + r2);
try {
Reference r3 = new Reference();
System.out.println("r3=" + r3);
Reference r4 = new Reference();
System.out.println("r4=" + r4);
}catch (Error e){
e.printStackTrace();
}
System.out.println("=======================");
System.out.println("r1=" + r1);
System.out.println("r2=" + r2);
}
}
打印结果如下:
从上面结果可以看出,只要强引用关系存在,jvm宁肯抛出OutOfMemoryError也不会对具有与强引用的对象做回收。
2.2、软引用
软引用主要是用来描述一些还有用但非必须的对象。被软引用关联的对象,只有在系统发生内存溢出之前对其进行垃圾回收,如果本次垃圾回收仍然没有回收到足够的内存才会抛出内存溢出异常。
import com.sxx.bean.Reference;
import java.lang.ref.SoftReference;
/**
* 软引用
*/
public class SoftReferenceTest {
public static void main(String[] args) {
SoftReference<Reference> s1 = new SoftReference<>(new Reference());
System.out.println("s1=" + s1.get());
SoftReference<Reference> s2 = new SoftReference<>(new Reference());
System.out.println("s2" + s2.get());
System.out.println("内存不足,开始触发垃圾回收");
try{
Reference reference3 = new Reference();
System.out.println("reference3=" + reference3);
Reference reference4 = new Reference();
System.out.println("reference4=" + reference4);
}catch (Error e){
e.printStackTrace();
}
System.out.println("垃圾回收结束");
System.out.println("s1=" + s1.get());
System.out.println("s2=" + s2.get());
}
}
打印结果如下:
从上面可以看出,在第20行准备创建reference4时内存不足,触发了一次gc对s1、s2进行回收,但是回收之后的内存仍然不够创建reference4,因此抛出OOM。
2.3、弱引用
弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能存活到下一次垃圾收集发生为止,也就是说当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
import com.sxx.bean.Reference;
import java.lang.ref.WeakReference;
/**
* 弱引用
*/
public class WeakReferenceTest {
public static void main(String[] args) {
WeakReference<Reference> w1 = new WeakReference<>(new Reference());
System.out.println("w1=" + w1.get());
WeakReference<Reference> w2 = new WeakReference<>(new Reference());
System.out.println("w2=" + w2.get());
System.out.println("开始进行垃圾回收");
System.out.println("=======================");
System.gc();
System.out.println("垃圾回收结束");
System.out.println("=======================");
System.out.println("w1=" + w1.get());
System.out.println("w2=" + w2.get());
}
}
打印结果如下:
从上面打印结果可以看出,不管内存是否足够,当gc被触发时,不管内存是否充足,弱引用对象都会被回收。
2.4、虚引用
虚引用顾名思义,就是形同虚设,是最弱的一种引用关系。与其他几种引用都不同,虚引用并不会决定对象的生命周期,也无法通过虚引用来取得一个对象实例。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它的唯一作用就是在这个对象被收集器回收时收到一个系统通知。
import com.sxx.bean.Reference;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
/**
* 虚引用
*/
public class VirtualReferenceTest {
public static void main(String[] args) {
WeakReference<Reference> w1 = new WeakReference<>(new Reference());
System.out.println("w1=" + w1.get());
ReferenceQueue queue = new ReferenceQueue();
PhantomReference<Reference> p1 = new PhantomReference<Reference>(w1.get(), queue);
System.out.println("p1=" + p1.get());
System.out.println("开始触发垃圾回收");
System.gc();
System.out.println("发垃圾回收结束");
System.out.println("p1=" + p1.get());
}
}
打印结果如下:
从上面可以看到,其实虚引用并没有什么东西,它不能指向持有它的实例对象也不能决定持有实例的生命周期,那么为什么还要创建它呢,如果说仅仅为了收到一个持有虚引用对象被gc的通知,那么我们重写finalize方法不是更好?
其实我们也可以理解虚引用和finalize方法有些类似都是为了接受持有对象被gc的通知,但是显然虚引用更加灵活些,毕竟当你真正用的时候不能把一堆的功能都写在finalize方法里啊。
虚引用的使用场景:
我们知道java的NIO中可以使用堆外的内存,也叫直接内存,直接内存的回收就是使用的虚引用通知机制。直接内存的使用可以通过ByteBuffer的allocateDirect调用,在清理的时候调Cleaner对象,这个Cleaner就是继承了PhantomReference。