大家对Java中的四种引用类型相信都不陌生,都知道这四种应用类型分别是强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Refrence)和虚引用(Phantom Reference)。但也好像再具体一点就有点模糊了,比如jdk为什么要设计这四种引用类型?既然设计了它肯定就会有对应的引用场景等等。我本人之前对这些的了解也是知道有这么个东西,再具体点的就不知道了,后来看了马士兵老师的一节相关公开课才有了个大体的了解(你用大腿想一想,我这肯定不是做广告),现在对相关的内容做个总结,希望可以帮到有相关困扰的同学。
一、强引用
概念:关于强引用我想就不必太多的介绍了,强引用是java中最传统的“引用”定义,我们平常做“Object obj = new Object()"的操作就是强引用。
特点:强引用无论在任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。
代码演示:
public class Person {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize...");
}
}
上面我们新建了一个Person类,并且重写了它其中的finalize()方法(一个对象在被回收的时候一般都会调用此对象的finalize方法,jdk1.8后此方法被废弃了),为了清楚的看到这个对象是否被回收了,所以我们重写了其finalize()方法,并在其中打印了一段话。
public class StronglyReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Person person = new Person();
person = null;
//手动调用gc,进行回收
System.gc();
}
}
输出结果:
finalize...
从上面的代码可以看到 ,我们把person = null;这个时候就没有引用指向Person对象了,这时我们手动调用gc,就会把其回收掉,这时对象的finalize()方法就会被调用。
二、软引用
在正式讲软引用之前,首先需要说明的一点就是,除了强应用意外,其他的三个引用都有其对应的类。比如软引用对应的类就是SoftReference。
概念:如何创建软应用呢?
SoftReference<Person> m = new SoftReference<>(new Person());
像上面这样,我们就创建了一个Person对象,并且让m指向这个Person对象,而这个指向Person对象的m就是软引用。我们通过SoftReference类中的方法get()就可以获取到软引用指向的person对象。
特点:当内存比较紧缺的时候,由软引用指向的对象在碰到gc线程后,就会被回收,如果内存不紧缺,则不会回收。
代码演示:
public class SoftReferenceDemo {
public static void main(String[] args) {
SoftReference<byte[]> m = new SoftReference<>(new byte[1024 * 1024 * 10]);//大小为10M
System.out.println(m.get());
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
//再分配一个数组,heap将装不下,这时候系统会进行垃圾回收,先回收一次,如果不够,会把
//软引用指向的对象进行回收
byte[] b = new byte[1024*1024*15];
System.out.println(m.get());
}
}
代码写好后,我们为这个程序设置一下运行时的最大堆内存(-Xmx25M),设置方式如下图所示:
运行程序后输出结果:
[B@470d1f30
[B@470d1f30
null
内存示意图
上面的代码首先我们在堆内存中申请了一个大小为10M的byte数组,之后通过软引用的get方法去得到这个数组在堆中的地址,从第一行的输出结果来看,我们是正确的拿到了这个地址的;之后主动调用gc线程去进行垃圾回收,让线程睡500ms后,我们继续通过软引用的get方法去拿这个数值的地址,通过第二行的输出结果得知,我们依然拿到了正确的地址。由此可知,虽然我们在上面通过gc调用了垃圾回收线程进行垃圾处理,但gc线程并没有回收这个byte数组对象,这是因为我们在运行程序的时候给其设置的最大堆内存为25M,而我们通过new byte[1024 * 1024 * 10]只是在堆中创建了大小为10M的对象,10M < 25M,堆内存目前还够用,所以gc不会去处理由“软引用”指向的这个数组对象。之后我们继续通过new byte[1024 * 1024 * 15]继续向堆内存中申请了大小为15M的空间来存放byte数组对象,这个时候因为我们初始设置的堆大小为25M,所以在jvm在申请第二次大小为15M的堆空间时,发现队中剩余的空间不够了(有的同学可能会问10M+15M=25M不是刚刚好吗?其实内存中除了我们刚开始申请的那10M,还会有一些其他的东西占用了很少的一部分空间,所以整体加起来10M+15M+其他是大于25M的,如果觉得不行,你可以在开始时设置堆得大小为-Xmx20M),这个时候gc线程就会回收由软引用m执行指向得那10M的空间。所以我们在程序最后通过软引用m的get()方法去拿的时候就已经拿不到了,因为它已经被回收掉了,所以输出结果为null。
应用场景:软引用的应用场景主要是用来做缓存。通过上面的分析我们可以知道,当内存空间足够的时候,我们可以把由软引用指向的对象放到内存中,当内存不足的时候,我们就对其进行回收,这种特性特别适合用来做缓存功能。
三、弱引用
概念:如何创建弱应用呢?同软引用类似,如下:
WeakReference<Person> person = new WeakReference<>(new Person());
像上面这样,我们创建了一个Person对象,并且让person指向这个Person对象,而这个指向Person对象的person就是弱引用。我们通过WeakReference类中的方法get()就可以获取到弱引用指向的person对象。
特征:弱引用不像软引用那样,当内存不足的时候才会进行回收,弱引用是“只要碰到gc,就会进行回收,不管当前内存够不够用”。
代码演示:
public class WeakReferenceDemo {
public static void main(String[] args) {
WeakReference<Person> person = new WeakReference<>(new Person());
System.out.println(person.get());
System.gc();
System.out.println(person.get());
}
}
输出结果:
com.zhangxudong.Person@53ffb7d4
null
finalize...
通过上面的输出结果我们可以得知,在第一次通过弱引person的get()方法去拿对象时我们正确的得到了结果,之后手动调用gc,然后再通过弱引person的get()方法去拿对象时,发现已经为空了并且Person对象中的finalized方法也被调用了。这就表明弱引用只要碰到gc就会被回收。
引用场景:弱引用的应用场景在java中最主要的其实就是ThreadLocal,这个一块因为涉及的内容比较多,我会另外写一篇文章来做进一步的说明,详情参见文章(待补充)。
四、虚引用
概念:虚引用是最弱的一种引用关系,它是一种比弱引用还弱n倍的引用,如果一个对象被一个虚引用指着的时候,其实这个指向和没有一样,不像软引用和弱引用那样,我们可以通过对应引用的get()方法拿到引用指向的对象。但虚引用不同,即使由虚引用指向某个对象,我们通过虚引用的get()方法也是拿不到它指向的对象的。
那我们怎么创建一个虚引用呢?如下:
//先创建一个引用队列
ReferenceQueue<Person> QUEUE = new ReferenceQueue<Person>();
//在new虚引用对象的时候传递两个参数,一个就是虚引用要指向的队形person,一个就是上面的队列
PhantomReference<Person> personPhantomReference = new PhantomReference<>(new Person(), QUEUE);
可以看到,我们创建虚引用的时候,它和软引用与弱引用有所不同,它除了要传递一个需要指向的对象外,还要传递一个队列。这个队列是做什么用的呢?当一个虚引用指向的对象被回收的时候,它就会把相关的信息存入到这个队列里面去。所以为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。这有什么用呢?我们一会儿在“应用场景”中做进一步说明。
特征:关于虚引用的特点我们在上面概念部分已经大体介绍过来,就是如果一个对象被一个虚引用指着的时候,其实这个指向和没有一样,我们通过虚引用的get()方法也拿不到它指向的这个对象。
下面我们通过一段代码来演示一下虚引用的相关特征。
代码演示:
public class PhantomReferenceDemo {
private static final List<Object> LIST = new LinkedList<Object>();
private static final ReferenceQueue<Person> QUEUE = new ReferenceQueue<Person>();
public static void main(String[] args) {
final PhantomReference<Person> personPhantomReference = new PhantomReference<>(new Person(), QUEUE);
new Thread(new Runnable() {
@Override
public void run() {
while(true){
LIST.add(new byte[1024 * 1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
//不管有没有被回收,通过虚引用的get方法都是永远也拿不到指定对象的值的
System.out.println(personPhantomReference.get());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
Reference<? extends Person> poll = QUEUE.poll();
if(poll != null){
System.out.println("-----虚引用对象被jvm回收了-----" + poll);
}
}
}
}).start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面的代码中,我们开启了两个线程,一个线程不断的向List中添加数据,等添加到一定时间的时候,堆内存就会满了,这个时候就会触发gc进行垃圾回收,当gc碰到虚引用的时候,它会二话不说立马对其进行回收。 当gc回收虚引用指向的对象时,就会向队列QUEUE中存放相关的信息。而我们开启的第二个线程,它会不断的去检测这个队列,如果发现队列中存进去了东西 它就会打印出“虚引用对象被jvm回收了” 这段话。 其实这第二个线程所做的工作在Java中就是垃圾回收线程 要干的活,只不过我们为了清楚的展示虚引用被回收的过程,就用第二个线程模拟一下gc线程,当虚引用被回收时,只是简单的打印一段话出来 。而gc线程在检测到虚引用被回收时,会做其他一些相对复杂的操作。
输出结果:
null
null
null
finalize...
null
null
null
null
null
-----虚引用对象被jvm回收了-----java.lang.ref.PhantomReference@17990d96
null
null
null
null
null
null
null
null
null
通过上面的输出结果我们可以看到,即使在没有被回收之前,我们通过虚引用的get()方法也是拿不到它所指向的对象的。等到程序进行gc垃圾回收后,它把相应的信息放到了队列中,这是我们一直处于监控状态的第二个线程就会检测到,并打印出了“虚引用对象被jvm回收了”这句话。
引用场景:通过上面的一顿描述,大家可能觉得虚引用挺鸡肋的,要这玩意儿有啥用?其实既然jdk中设计了它,就一定有他的用处。虚引用最大的应用场景就是“堆外内存的管理”。像NIO、Netty中其实就用到了虚引用。
先解释一下什么是堆外内存:我们知道整个操作系统会有属于它的一块大内存,而我们Java中的JVM只是分配和管理了属于操作系统内存中的一小部分内存而已,而JVM内存之外的那部分内存就属于堆外内存。
设想这样一个场景,在早期,当我的Java程序需要从网络上获取数据的时候,首先是由操作系统将相应的数据从网络上获取然后放到堆外内存,之后java再把这些数据从堆外内存拷贝到自己JVM管理的内存,这样就是出现了两次拷贝,效率比较低下。因此java就提供了一个可以直接管理堆外内存的DirectByteBuffer类,DirectByteBuffer指向的就是堆外内存,我们可以使用它进行堆外内存的分配/使用/回收。DirectByteBuffer这个类的对象本身是存储在JVM管理的堆内存中的,而由DirectByteBuffer指向的堆外内存是JVM管理不了的。所以当出现DirectByteBuffer对象本身被垃圾收集器回收掉的时候,它所指向的堆外内存就与它失去了关联,这样它指向的那块堆外内存就永远的停留在了哪里,不会被回收掉,这就造成了内存泄漏。所以为了避免由DirectByteBuffer引起的内存泄漏,jdk的设计者们就把指向“DirectByteBuffer对象”的引用(注意这里是指向DirectByteBuffer对象本身的引用,而不是DirectByteBuffer指向的堆外内存引用)设计为虚引用,这样,当JVM堆内存中的DirectByteBuffer对象被垃圾回收器干掉的时候,它就会向队列中存放相关的信息,这些信息就包含DirectByteBuffer指向堆外内存的地址,这个时候gc线程就会根据这个信息释放掉DirectByteBuffer所指向的堆外内存(注意gc线程是C++写的,它本身是可以去释放堆外内存的),从而避免了内存泄漏。
关于Java的四种引用类型就讲这么多吧,大家有什么疑问或异议,欢迎给我留言讨论。
来源:CSDN
作者:张旭东0101
链接:https://blog.csdn.net/Hi_Red_Beetle/article/details/104552676