1. JVM中引用的介绍
在java中,除了基本数据类型的变量外,其他所有的变量都是引用类型,指向堆中各种不同的对象。
在jvm中,除了我们常用的强引用外,还有软引用、弱引用、虚引用,这四种引用类型的生命周期与jvm的垃圾回收过程息息相关。
2. 这四种引用类型有的区别
在Java中,引用类型是用来描述对象之间关联关系的一种方式。Java中的引用类型主要有四种:强引用、软引用、弱引用和虚引用。
- 强引用(Strong Reference):这是最常见的引用类型。如果一个对象具有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。在Java中,最常见的就是通过new关键字创建一个对象,这个对象就是强引用。
- 软引用(Soft Reference):如果一个对象只具有软引用,那么在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在Java中,我们可以通过java.lang.ref.SoftReference类来实现软引用。
- 弱引用(Weak Reference):如果一个对象只具有弱引用,那么在垃圾回收器线程扫描它所管辖的内存区域的过程中,只要发现弱引用关联的对象,不管当前内存空间足够与否,都会回收它的内存。在Java中,我们可以通过java.lang.ref.WeakReference类来实现弱引用。
- 虚引用(Phantom Reference):虚引用也称为”幽灵引用”或”幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在Java中,我们可以通过java.lang.ref.PhantomReference类来实现虚引用。
强引用
强引用(Strong references)就是直接new一个普通对象,表示一种比较强的引用关系,只要还有强引用对象指向一个对象,那么表示这个对象还活着,垃圾收集器宁可抛出OOM异常,也不会回收这个对象。
例如下面代码中的的引用 u 就是一个强引用。
// 定义一个名为StrongReferenceDemo的公共类
public class StrongReferenceDemo {
// 定义主函数
public static void main(String[] args) throws IOException {
// 创建一个User对象,这是一个强引用
User u = new User();
// 打印User对象的信息
System.out.println(u);
// 将User对象的引用设置为null,这样原来的User对象就没有任何强引用指向它了
u = null;
// 提示JVM的垃圾回收器运行,这将回收没有任何强引用指向的对象
System.gc();
// 使程序暂停,等待用户输入,这样可以看到垃圾回收器的工作情况
System.in.read();
}
}
// 定义一个名为User的公共类
public class User {
// 重写finalize方法,这个方法在对象被垃圾回收器回收时会被调用
@Override
protected void finalize() throws Throwable {
// 打印一条消息,表示finalize方法被调用了
System.out.println("call User finalize() method");
}
}
在这段代码中,当我们创建一个User对象并将其引用设置为null后,这个User对象就没有任何强引用指向它了,所以它可能会被垃圾回收器回收。当垃圾回收器准备回收一个对象时,会调用这个对象的finalize方法。在这个例子中,我们重写了User类的finalize方法,所以当User对象被垃圾回收器回收时,我们会看到”call User finalize() method”这条消息。
软引用
软引用用于存储一些可有可无的东西,例如缓存,当系统内存充足时,这些对象不会被回收,当系统内存不足时也是GC时才会回收这些对象,如果回收完这些对象后内存还是不足,就会抛出OOM异常。
下面代码主要演示了Java中的软引用和垃圾回收的基本概念。
// vm args: -Xmx36m -XX:+PrintGCDetails
// 定义一个名为SoftReferenceDemo的公共类
public class SoftReferenceDemo {
// 定义主函数
public static void main(String[] args) throws InterruptedException {
// 创建一个User对象,并用一个软引用来引用它
SoftReference<User> softReference = new SoftReference<>(new User());
// 打印软引用指向的对象
System.out.println(softReference.get());
// 提示JVM的垃圾回收器运行
System.gc();
// 等待3秒,让垃圾回收器有时间运行
TimeUnit.SECONDS.sleep(3);
// 再次打印软引用指向的对象,因为这个对象只被软引用指向,但是内存还没有不足,所以它还没有被回收
System.out.println(softReference.get());
// 创建一个大的字节数组,使得堆内存不足
byte[] bytes = new byte[1024 * 1024 * 10];
// 再次打印软引用指向的对象,因为内存不足,所以这个对象已经被垃圾回收器回收了
System.out.println(softReference.get());
}
}
在这段代码中,我们创建了一个User对象,并用一个软引用来引用它。当我们提示垃圾回收器运行时,这个User对象并没有被回收,因为内存还没有不足。但是当我们创建一个大的字节数组,使得堆内存不足时,这个User对象就被垃圾回收器回收了,因为它只被软引用指向,而软引用指向的对象在内存不足时会被垃圾回收器回收。
弱引用
弱引用(WeakReference)并不能使对象豁免垃圾回收,仅仅是提供一种访问在弱引用状态下对象的途径。只要发生gc,弱引用对象就会被回收。ThreadLocal中就使用了WeakReference来避免内存泄漏。
// 定义一个名为WeakReferenceDemo的公共类
public class WeakReferenceDemo {
// 定义主函数
public static void main(String[] args) throws InterruptedException {
// 创建一个User对象,并用一个弱引用来引用它
WeakReference<User> weakReference = new WeakReference<>(new User());
// 打印弱引用指向的对象
System.out.println(weakReference.get());
// 提示JVM的垃圾回收器运行
System.gc();
// 等待3秒,让垃圾回收器有时间运行
TimeUnit.SECONDS.sleep(3);
// 再次打印弱引用指向的对象,因为这个对象只被弱引用指向,所以它已经被垃圾回收器回收了
System.out.println(weakReference.get()); // null
}
}
在这段代码中,我们创建了一个User对象,并用一个弱引用来引用它。当我们提示垃圾回收器运行时,这个User对象就被垃圾回收器回收了,因为它只被弱引用指向,而弱引用指向的对象在垃圾回收器运行时会被回收。所以,当我们再次打印弱引用指向的对象时,结果是null,因为这个对象已经被回收了。
虚引用
虚引用必须和引用队列(ReferenceQueue)联合使用。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象之前,把这个虚引用加入到与之关联的引用队列中。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
下面代码演示了Java中的虚引用和垃圾回收的基本概念。
// vm args: -Xms4m -XX:+PrintGC
// 定义一个名为PhantomReferenceDemo的公共类
public class PhantomReferenceDemo {
// 定义主函数
public static void main(String[] args) throws IOException, InterruptedException {
// 创建一个引用队列
ReferenceQueue<User> referenceQueue = new ReferenceQueue<>();
// 创建一个User对象,并用一个虚引用来引用它,这个虚引用被关联到了引用队列
PhantomReference<User> phantomReference = new PhantomReference<>(new User(), referenceQueue);
// 打印虚引用指向的对象,结果是null,因为不能通过虚引用来获取一个对象实例
System.out.println(phantomReference.get());
// 创建一个新的线程,这个线程不断检查引用队列,看是否有被垃圾回收器回收的对象
new Thread(() -> {
while (true) {
Reference<? extends User> poll = referenceQueue.poll();
if (poll != null) {
// 如果引用队列中有被垃圾回收器回收的对象,打印一条消息
System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
// 打印被回收的对象,结果是null,因为不能通过虚引用来获取一个对象实例
System.out.println("--- 回收对象 ---- " + poll.get());
}
}
}).start();
// 等待1秒,让新的线程有时间运行
TimeUnit.SECONDS.sleep(1);
// 提示JVM的垃圾回收器运行
System.gc();
// 使程序暂停,等待用户输入,这样可以看到垃圾回收器的工作情况
System.in.read();
}
// 定义一个名为User的私有静态类
private static class User {
// User类有一个大的数组字段,使得User对象占用较多的内存
private int[] bytes = new int[1024 * 1024 * 5];
// 重写finalize方法,这个方法在对象被垃圾回收器回收时会被调用
@Override
protected void finalize() throws Throwable {
// 打印一条消息,表示finalize方法被调用了
System.out.println("call User finalize() method");
}
}
}
在这段代码中,我们创建了一个User对象,并用一个虚引用来引用它。这个虚引用被关联到了一个引用队列。当这个User对象被垃圾回收器回收时,这个虚引用就会被加入到引用队列中。我们创建了一个新的线程,这个线程不断检查引用队列,看是否有被垃圾回收器回收的对象。如果有,就打印一条消息。这样,我们就可以知道何时有对象被垃圾回收器回收了。
基于虚引用,有一个更加优雅的实现方式,那就是Cleaner,可以用来替代Object类的finalizer方法,在DirectByteBuffer中用来回收堆外内存。
使用场景
在Java中,根据对象的生命周期和内存管理需求,我们可以选择使用不同类型的引用。以下是这四种引用类型的常见应用场景:
- 强引用(Strong Reference):强引用是最常见的引用类型,我们在编程时经常使用的就是强引用。只要对象有强引用指向,垃圾回收器就不会回收这个对象。因此,强引用适用于程序运行期间始终需要的对象。
- 软引用(Soft Reference):软引用可以用来实现内存敏感的缓存。如果一个对象只有软引用,那么它会在系统内存不足时被垃圾回收器回收。因此,我们可以把一些非必需但重建成本高的对象通过软引用存储在缓存中,如图片、视频等大文件。
- 弱引用(Weak Reference):弱引用通常用于容器类中,用来解决循环引用的问题。如果一个对象只有弱引用,那么在垃圾回收器线程扫描时,无论内存是否足够,都会被回收。Java中的WeakHashMap就是使用弱引用来实现的。
- 虚引用(Phantom Reference):虚引用主要用于跟踪对象被垃圾回收的活动。虚引用无法获取到对象的实例,它的唯一目的就是在对象被收集器回收时收到一个系统通知。这通常用于实现比finalize机制更灵活的回收操作,例如,DirectByteBuffer就是通过虚引用来实现内存的回收的。
这四种引用类型提供了不同级别的对象生命周期管理,可以根据具体的应用场景和需求进行选择。