一、Java中的四种引用
Java中有四种引用类型:强引用、软引用、弱引用、虚引用。
二、Java为什么要设计这四种引用
Java中的内存分配和内存回收,都是由JVM去负责的,而不需要程序员进行考虑,一个对象是否可以被回收,主要看是否有引用指向此对象,说的专业点,叫可达性分析。
Java设计这四种引用的主要目的有两个:
1、可以让程序员通过代码的方式来决定某个对象的生命周期,不同的引用方式在回收策略不同。
2、有利用垃圾回收。
三、强引用
强引用是最普遍的一种引用,我们写的代码,99.9999%都是强引用,如下这种就是强引用了,是不是在代码中随处可见,最亲切。
Object o = new Object();
只要某个对象有强引用与之关联,这个对象永远不会被回收,即使内存不足,JVM宁愿抛出OOM,也不会去回收。
1、如何回收强引用?
当强引用和对象之间的关联被中断了,就可以被回收了。我们可以手动把关联给中断了,方法也特别简单:
o = null;
我们可以手动调用GC,看看如果强引用和对象之间的关联被中断了,资源会不会被回收,为了更方便、更清楚的观察到回收的情况,我们需要新写一个类,然后重写finalize方法,下面我们来进行这个实验:
/**
* 只是为了测试对象最后的操作才重写finalize,现实不会重写finalize()
*
**/
public class Student {
@Override
protected void finalize() throws Throwable {
System.out.println("Student 被回收了");
}
}
public static void main(String[] args) {
Student student = new Student();
// 不加这个观察一下,会发下永远不会打印
student = null;
System.gc();
}
运行结果:
Student 被回收了
在实际的开发中,看到有一些对象被手动赋值为NULL,很大可能就是为了“特意提醒”JVM这块资源可以进行垃圾回收了。
四、软引用
下面先来看看如何创建一个软引用,会见把总代码贴出,之后在分开讲解,方便看完分解之后返回重新理解:
/**
* 软引用
* 软引用是用来描述一些还有用但并非必须的对象。
* 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。
* 如果这次回收还没有足够的内存,才会抛出内存溢出异常。
* 需要设置堆内存,不然难以观察
* 设置堆内存20M
* -Xmx20M
*
* 可以做缓存,比如:内存够的话存,不够直接回收,之后去库查找,但是很少,因为有redis
*/
package com.zcm.juc;
import java.lang.ref.SoftReference;
public class T02_SoftReference {
public static void main(String[] args) {
// 创建软引用,分配10M内存
SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
// m = null;
// 第一次有值,为一个hashCode的值
System.out.println(m.get());
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 没有内存操作,内存还是够,不回收
System.out.println(m.get());
//再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用干掉
//已经不够15M,回收,出现null
byte[] b = new byte[1024*1024*15];
System.out.println(m.get());
}
}
软引用就是把对象用SoftReference包裹一下,当我们需要从软引用对象获得包裹的对象,只要get一下就可以了:
//上边的对象是new byte[1024*1024*10],为了方便理解,将对象换为Student对象,可以发现get得到的是包装对象
SoftReference<Student>studentSoftReference=new SoftReference<Student>(new Student());
Student student = studentSoftReference.get();
System.out.println(student);
1、软引用有什么特点呢?
当内存不足,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用的包裹的对象给干掉,也就是只有在内存不足,JVM才会回收该对象。
对上述代码进行分开解析:
SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024*1024*10]);
System.out.println(softReference.get());
System.gc();
System.out.println(softReference.get());
byte[] bytes = new byte[1024 * 1024 * 10];
System.out.println(softReference.get());
我定义了一个软引用对象,里面包裹了byte[],byte[]占用了10M,然后又创建了10Mbyte[]。
运行程序,需要带上一个参数:代表最大堆内存是20M。
-Xmx20M
运行结果:
[B@11d7fff
[B@11d7fff
null
可以很清楚的看到手动完成GC后,软引用对象包裹的byte[]还活的好好的,但是当我们创建了一个10M的byte[]后,最大堆内存不够了,所以把软引用对象包裹的byte[]给干掉了,如果不干掉,就会抛出OOM。
软引用到底有什么用呢?比较适合用作缓存,当内存足够,可以正常的拿到缓存,当内存不够,就会先干掉缓存,不至于马上抛出OOM。但是实际工作中缓存我们完全可以使用redis。
五、弱引用
弱引用的使用和软引用类似,只是关键字变成了WeakReference:
/**
* 弱引用遭到gc就会回收
*
* 如果有一个强引用执行一个弱引用,强引用回收,弱就回收
* 一般用在容器中
* 看一下WeakReference的实现(前面作业,unlock源码)
*/
package com.zcm.juc;
import java.lang.ref.WeakReference;
public class T03_WeakReference {
public static void main(String[] args) {
WeakReference<byte[]> m = new WeakReference<>(new byte[1]);
System.out.println(m.get());
System.gc();
System.out.println(m.get());
// 用完一定的remove,防止内存泄漏
// 看一下为啥
ThreadLocal<M> tl = new ThreadLocal<>();
tl.set(new M());
tl.remove();
}
}
弱引用的特点是不管内存是否足够,只要发生GC,都会被回收:运行结果:
[B@11d7fff
null
可以很清楚的看到明明内存还很充足,但是触发了GC,资源还是被回收了。 弱引用在很多地方都有用到,比如ThreadLocal、WeakHashMap。
六、虚引用
虚引用又被称为幻影引用,我们来看看它的使用:
/**
* 主要是管理堆外内存
* 虚引用的值没法获取
*
* 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,
* 也无法通过虚引用来获取一个对象的实例。
* 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
* 虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象,
* 那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null,
* 弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。
*
* jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,
* 而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),
* 所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,
* 会在堆内存分配一个对象保存这个堆外内存的引用,
* 这个对象被垃圾收集器管理,一旦这个对象被回收,
* 相应的用户线程会收到通知并对直接内存进行清理工作。
*
* 事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,
* DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。
*
*/
package com.zcm.juc;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;
public class T04_PhantomReference {
private static final List<Object> LIST = new LinkedList<>();
private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();
public static void main(String[] args) {
PhantomReference<byte[]> phantomReference = new PhantomReference<>(new byte[1], QUEUE);
new Thread(() -> {
while (true) {
LIST.add(new byte[1024 * 1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
System.out.println(phantomReference.get());
}
}).start();
new Thread(() -> {
while (true) {
Reference<? extends M> poll = QUEUE.poll();
if (poll != null) {
System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
}
}
}).start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1、虚引用的特点
创建虚引用的关键代码:
ReferenceQueue queue = new ReferenceQueue();
PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], queue);
System.out.println(reference.get());
虚引用的使用和上面说的软引用、弱引用的区别还是挺大的,我们先不管ReferenceQueue 是什么,直接运行:
null
竟然打印出了null,我们来看看get方法的源码:
public T get() {
return null;
}
特点一:无法通过虚引用来获取对一个对象的真实引用。
那虚引用存在的意义是什么呢?这就要回到我们上面的代码了,我们把代码复制下,以免大家再次往上翻:
ReferenceQueue queue = new ReferenceQueue();
PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], queue);
System.out.println(reference.get());
创建虚引用对象,我们除了把包裹的对象传了进去,还传了一个ReferenceQueue,从名字就可以看出它是一个队列。
特点二:虚引用必须与ReferenceQueue一起使用,当GC准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中。
将总的代码执行一下,运行结果如下:
null
null
null
null
null
null
null
--- 虚引用对象被jvm回收了 ---- java.lang.ref.PhantomReference@4bf69a32
null
null
null
null
我们简单的分析下代码:
- 第一个线程往集合里面塞数据,随着数据越来越多,肯定会发生GC。
- 第二个线程死循环,从queue里面拿数据,如果拿出来的数据不是null,就打印出来。
- 从运行结果可以看到:当发生GC,虚引用就会被回收,并且会把回收的通知放到ReferenceQueue中。