一、Java 引用概述
Java 中出现四种引用是为了更加灵活地管理对象的生命周期,以便在不同场景下灵活地处理对象的回收问题。不同类型的引用在垃圾回收时的处理方式不同,可以用来实现不同的垃圾回收策略。Java 目前将其分成四类,类图如下:
Java 技术允许使用 finalize()
方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
@Deprecated(since="9")
protected void finalize() throws Throwable { }
}
传统的 finalize() 方法在 Java 9 中已经被标记为废弃(deprecated),而且在 Java 11 中已经被删除(推荐使用 Cleaner
是 Java 新一代的内存回收处理机制)。这是因为 finalize() 方法存在许多问题,包括但不限于以下几点:
- finalize() 方法并不能保证被及时地调用,这可能会导致内存泄漏等问题。
- finalize() 方法的执行时机是不确定的,可能会导致程序的不可预测行为。
- finalize() 方法是被 JVM 调用,手动调用容易出错,不够灵活。
=>Cleaner 的执行过程,如下:
在 JDK9 中,Java 提供了一种新的机制来代替传统的 finalize() 方法来清理对象,就是 Cleaner。Cleaner 是一个 Java 类,用于注册需要进行清理的对象以及相关的清理方法。它使用了本地内存技术
,可以在 Java 对象释放后及时进行清理。
当一个 Java 对象不再被引用时,其所占用的内存并不会立即释放,而是会被标记为“可回收”状态。JVM 会在后台开启一个线程,定期扫描这些“可回收”对象,清理其中已经没有引用的对象。Cleaner 就是在这个扫描过程中对于对象的清理操作。
使用 Cleaner 机制时,开发人员需要创建一个 Cleaner 对象,并使用该对象对需要清理的对象进行注册,同时定义一个清理方法。当被注册的对象被回收时,JVM 会在后台的扫描过程中调用注册的清理方法,对对象进行清理操作。相较于传统的 finalize() 方法,Cleaner 机制的清理操作更加灵活,可以在多种情况下触发,并且不会受到 finalize() 方法的不稳定性的影响。
下面是一个使用 Cleaner 的例子:
public class Resource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private ByteBuffer buffer;
public Resource(int size) {
buffer = ByteBuffer.allocate(size);
cleanable = cleaner.register(this, new ResourceCleaner(buffer));
}
@Override
public void close() throws Exception {
cleanable.clean();
}
private static class ResourceCleaner implements Runnable {
private ByteBuffer buffer;
public ResourceCleaner(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
System.out.println("Cleaner is cleaning the resource");
buffer = null;
}
}
}
在上面的例子中,Resource 类使用了 Cleaner 来释放资源。Resource 类中的 buffer 对象在实例化时会被分配内存。当使用完 Resource 实例时,需要调用 close() 方法来释放资源。close() 方法调用了 Cleaner.Cleanable 的 clean() 方法来触发资源的释放。同时,ResourceCleaner 类实现了 Runnable 接口,run() 方法被用来进行资源的清理操作。在这个例子中,资源的清理操作是将 buffer 设置为 null。这样,一旦实例被垃圾收集器回收,clean() 方法就会自动被调用,从而释放资源。
- 一般来说,实现了 Closeable 或 AutoCloseable 接口的类,在使用完后都需要手动调用 close() 方法来释放资源。但是有些情况下,JVM 会自动调用 close() 方法,比如在 try-with-resources 语句中,当 try 代码块执行完后,JVM会自动调用相应资源的 close() 方法来释放资源,而无需手动调用。但是,如果在 try-with-resources 语句中使用了多个资源,需要注意它们的释放顺序,确保后打开的资源先关闭,以避免可能出现的资源泄露问题。
- ResourceCleaner 是由 JVM 的垃圾回收器自动触发的,当一个对象不再被引用时,它的 clean() 方法就会被自动调用。具体来说,当垃圾回收器扫描到一个对象时,如果这个对象实现了 Cleaner.Cleanable 接口,那么垃圾回收器会把它注册到一个全局的
ReferenceHandler 队列
中,这个队列
中的线程会定期触发
这些对象的 clean() 方法。这个过程是由 JVM 自动进行的,程序员无需手动触发。
二、强、软、弱、虚引用简单介绍
1、强引用(Reference 默认)
是最常见的引用类型,也是默认的引用类型。当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算是出现 OOM 也不会对该对象进行回收,死都不放。
强引用只要还指向一个对象,就表示对象还活着,垃圾收集器不会碰这些对象。在 Java 最常见的就是把一个对象赋值给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用引用时,它处理可达状态
,它不可能被垃圾回收器回收,即使该对象后面永远都不会被使用,JVM 也不会进行回收。因此强引用是造成 Java 内存泄露的主要原因之一。
比如下面这个例子:
class MyObject {
public byte[] buff = new byte[1000 * 1000 * 3];
@Override
protected void finalize() throws Throwable {
System.out.println(">>>>>>调用 finalize 清理资源...");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
MyObject myObject = new MyObject();
System.out.println("gc before myObject = " + myObject);
try {
byte[] bytes = new byte[1000 * 1000 * 8];
System.gc();
} finally {
System.out.println("gc after myObject = " + myObject);
}
}
首先通过 -Xms10m -Xmx10m
命令把 JVM 堆内存修改成 10M 方便测试,然后定义一个类 MyObject 里面分配一个数组占用堆内存 3M 空间,然后再 main() 方法中又分配一个 8M 大小数组,这样加起来肯定超过 10M 堆内存,会 OOM,但是可以手动调用 System.gc 触发垃圾回收,但是这里并不是回收(finalize() 方法肯定不会被调用),因为这里是属于强引用
,就算 OOM 也不会被回收。
在 JVM 准备垃圾回收之前会先去调用 finalize() 方法做一些清理工作,所以只需要覆写该方法演示效果(工作中不会这样干),然后手动调用 System.gc() 让他触发 finalize() 方法调用。
垃圾回收线程
是一个后台守护线程,定时在回收垃圾,这里提供 System.gc() 方便手动触发 GC 垃圾回收
效果如下:
gc before myObject = main.juc.MyObject@5ebec15
gc after myObject = main.juc.MyObject@5ebec15
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at main.juc.ReferenceDemo.main(ReferenceDemo.java:27)
那么怎么可以让他回收呢?可以用最简单的方式,将强引用直接赋值 null。修改之后的代码如下:
class MyObject {
public byte[] buff = new byte[1000 * 1000 * 1];
@Override
protected void finalize() throws Throwable {
System.out.println(">>>>>>调用 finalize 清理资源...");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
MyObject myObject = new MyObject();
System.out.println("gc before myObject = " + myObject);
try {
byte[] bytes = new byte[1000 * 1000 * 7];
myObject = null;
System.gc();
System.out.println(">>>>>>垃圾回收..."+myObject);
} finally {
System.out.println("gc after myObject = " + myObject);
}
}
}
输出结果如下:
gc before myObject = main.juc.MyObject@5ebec15
>>>>>>调用 finalize 清理资源...
>>>>>>垃圾回收...null
gc after myObject = null
Process finished with exit code 0
myObject = null 可以帮助 JVM 进行垃圾回收。
2、软引用(SoftReference 装饰对象)
如果一个对象只具有软引用,则在系统内存不足时,垃圾回收器会尝试回收该对象。这种引用类型通常用于缓存中,以便在内存不足时自动释放缓存,可以通过 SoftReference
类来创建软引用。例子如下:
class MyObject {
public byte[] buff = new byte[1000 * 1000 * 3];
@Override
protected void finalize() throws Throwable {
System.out.println(">>>>>>调用 finalize 清理资源...");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
try {
System.out.println("gc before myObject = " + softReference.get());
byte[] buf = new byte[1000*1000*3];
System.gc();
Thread.sleep(3000);
System.out.println("gc after myObject = " + softReference.get());
} finally {
System.out.println(">>>>>>内存不够 myObject="+softReference.get());
}
}
}
首先通过 -Xms10m -Xmx10m
命令把 JVM 堆内存修改成 10M 方便测试,然后把需要标识为软引用的对象通过 SoftReference
关键字进行包装。MyObject 类中首先分配一个数组,数组占堆内存大约 3M 空间,mian() 方法 3M 空间,远远还没有超过 10M 分配内存大小,然后手动调用 gc,效果如下:
gc before myObject = main.juc.MyObject@5ebec15
gc after myObject = main.juc.MyObject@5ebec15
>>>>>>内存不够 myObject=main.juc.MyObject@5ebec15
明显在内存足够的情况下,不会触发 GC 垃圾回收。现在将代码修改成下面这样,代码如下:
class MyObject {
public byte[] buff = new byte[1000 * 1000 * 3];
@Override
protected void finalize() throws Throwable {
System.out.println(">>>>>>调用 finalize 清理资源...");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
try {
System.out.println("gc before myObject = " + softReference.get());
byte[] buf = new byte[1000*1000*7];
System.gc();
Thread.sleep(3000);
System.out.println("gc after myObject = " + softReference.get());
} finally {
System.out.println(">>>>>>内存不够 myObject="+softReference.get());
}
}
}
效果如下:
gc before myObject = main.juc.MyObject@5ebec15
gc after myObject = null
>>>>>>内存不够 myObject=null
明显在内存不足时,会触发 GC 垃圾回收,将 SoftReference 引用的内存空间释放,这个就是 SoftReference 引用的好处。内存足,不回收,反之,回收。
3、弱引用( WeakReference 装饰对象)
如果一个对象有弱引用,只要当垃圾回收器扫描到这个对象时,无论内存是否充足,都会回收该对象。弱引用通常用于实现缓存
、内存敏感的高速缓存、监视器等功能。可以通过 WeakReference 类来创建弱引用。通过 -Xms10m -Xmx10m
命令把 JVM 堆内存修改成 10M 方便测试。代码如下:
通过 -Xms10m -Xmx10m
命令把 JVM 堆内存修改成 10M 方便测试。
class MyObject {
public byte[] buff = new byte[1000 * 1000 * 3];
@Override
protected void finalize() throws Throwable {
System.out.println(">>>>>>调用 finalize 清理资源...");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
WeakReference<MyObject> softReference = new WeakReference<>(new MyObject());
try {
System.out.println("gc before myObject = " + softReference.get());
System.gc();
Thread.sleep(3000);
System.out.println("gc after myObject = " + softReference.get());
} catch (Exception e) {
System.out.println(">>>>");
} finally {
System.out.println(">>>>>>内存不够 myObject="+softReference.get());
}
}
}
输出结果如下:
gc before myObject = main.juc.MyObject@5ebec15
>>>>>>调用 finalize 清理资源...
gc after myObject = null
>>>>>>内存不够 myObject=null
看到不管内存是否还有,只要 GC 扫描(System.gc() 可以出发 GC 扫描)到就会直接被回收。
补充:通过弱引用
举个实际案例如下:
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
public class ImageLoader {
private Map<String, WeakReference<Image>> cache = new HashMap<>();
public Image loadImage(String filename) {
Image image = null;
WeakReference<Image> ref = cache.get(filename);
if (ref != null) {
image = ref.get();
}
if (image == null) {
image = loadImageFromDisk(filename);
cache.put(filename, new WeakReference<>(image));
}
return image;
}
private Image loadImageFromDisk(String filename) {
// load image from disk
return null;
}
}
在这个示例中,ImageLoader 类使用一个 HashMap 来缓存已加载到内存中的图片,每个图片对应一个弱引用。当需要加载图片时,首先从缓存中查找图片是否已经加载到内存中,如果是,则返回弱引用所引用的图片对象;否则,从磁盘中加载图片,并将其添加到缓存中,同时返回图片对象。
由于缓存中的每个图片对象都是弱引用,因此在内存不足时,垃圾回收器会自动回收这些图片对象所占用的内存空间,从而实现内存管理。
补充:ThreadLocal 中为什么需要使用弱引用?
ThreadLocal 使用弱引用是为了防止内存泄漏。由于 ThreadLocalMap 是存在 Thread 中的,如果使用强引用,一旦ThreadLocal 对象被回收,它在 ThreadLocalMap 中对应的 Entry 对象也不会被回收,这样就会导致 Entry 对象中的value对象长时间得不到回收,最终导致内存泄漏。
使用弱引用可以让 ThreadLocal 对象被回收后,Entry 对象中的 value 对象在下一次 ThreadLocalMap 的操作时被顺便回收,从而避免了内存泄漏的问题。
4、虚引用(PhantomReference 装饰对象)
也称为幽灵引用,如果一个对象只具有虚引用,那么这个对象就和没有引用一样,在任何时候都可能被垃圾回收器回收。虚引用通常用于跟踪对象被垃圾回收器回收的状态,可以通过 PhantomReference 类来创建虚引用。
顾明思义,就是形同虚设,与其他几种引用都不同,虚拟引用并不会决定对象的声明周期。
如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独实用也不能通过虚引用访问对象,虚引用必须和引用队列 ReferenceQueue 一起使用。
虚引用的主要作用就是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize() 以后,可以做某些操作。PhantomReference.get() 方法总是会返回 null,因此无法访问对应的引用对象。比如:在这个对象被垃圾收集器回收的时候收一个系统通知或者做进一步的处理。举个例子:
通过 -Xms10m -Xmx10m
命令把 JVM 堆内存修改成 10M 方便测试。
class MyObject {
@Override
protected void finalize() throws Throwable {
System.out.println(">>>>>>调用 finalize 清理资源...");
}
}
public class ReferenceDemo {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
PhantomReference<MyObject> softReference = new PhantomReference<>(new MyObject(), queue);
List<byte[]> list = new ArrayList<>();
new Thread(() -> {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
list.add(new byte[1000 * 1000 * 1]);
System.out.println("softReference.get() = " + softReference.get());
}
}).start();
new Thread(() -> {
while (true) {
if (queue.poll() != null) {
System.out.println(">>>>>>引用队列有值啦...");
}
}
}).start();
Thread.sleep(8000);
}
}
输出结果如下:
softReference.get() = null
softReference.get() = null
softReference.get() = null
softReference.get() = null
softReference.get() = null
softReference.get() = null
>>>>>>调用 finalize 清理资源...
softReference.get() = null
softReference.get() = null
>>>>>>引用队列有值啦...java.lang.ref.PhantomReference@5c4efc23>>>type=class java.lang.ref.PhantomReference
softReference.get() = null
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Thread-0"
Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated
上述代码中,通过 PhantomReference 创建出虚引用,并且传入 ReferenceQueue 队列。线程1不断地在堆内存中申请空间,每次 1M 大小,因为虚引用包裹的对象,通过 softReference.get() 都是返回 null。线程1一直这样添加,直到内存不足时,虚引用对象会被放到 ReferenceQueue 队列中,ReferenceQueue 队列可以帮助我们在虚引用对象被回收时及时得到通知,从而进行必要的清理工作。其实就是表示这个虚引用在死之前想要留一些遗言,人类就可以监控到这个引用队列看到谁在死之前还有遗言,去帮他实现一下。
补充:
虚引用(Phantom Reference)是 Java 引用类型中最弱的一种,虚引用对象被GC回收的时候会放入一个由 ReferenceQueue管理的队列中。
虚引用的一个常见用途是管理 DirectByteBuffer 对象,它可以让我们对 DirectByteBuffer 对象的回收时间进行监控,一旦 DirectByteBuffer 对象被GC回收,就可以通知我们进行必要的资源释放操作,比如释放内存映射文件等。
以下是一个使用虚引用管理DirectByteBuffer的简单示例代码:
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.nio.ByteBuffer;
public class DirectByteBufferTest {
private static final int BUFFER_SIZE = 1024 * 1024;
private static final int MAX_BUFFERS = 10;
private static final ByteBuffer[] buffers = new ByteBuffer[MAX_BUFFERS];
private static final ReferenceQueue<ByteBuffer> queue = new ReferenceQueue<>();
static {
for (int i = 0; i < MAX_BUFFERS; i++) {
buffers[i] = ByteBuffer.allocateDirect(BUFFER_SIZE);
new PhantomReference<>(buffers[i], queue);
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < MAX_BUFFERS; i++) {
ByteBuffer buffer = buffers[i];
System.out.println("buffer " + i + ": " + buffer);
buffers[i] = null;
buffer = null;
}
System.gc();
Thread.sleep(1000);
Reference<? extends ByteBuffer> ref;
while ((ref = queue.poll()) != null) {
ByteBuffer buffer = ref.get();
System.out.println("buffer " + buffer + " is released");
// Release resources here
}
}
}
该示例程序通过创建10个 DirectByteBuffer 对象并将它们的虚引用对象放入队列中来管理这些 DirectByteBuffer 对象。在程序运行的过程中,首先会打印出10个 DirectByteBuffer 对象的地址,然后将它们的引用全部置为 null,这样它们就可以被GC回收。程序接着调用 System.gc() 来触发一次垃圾回收,然后调用 Thread.sleep() 让程序睡眠1秒钟等待垃圾回收完成。最后程序从 ReferenceQueue 中取出所有被回收的 DirectByteBuffer 对象的虚引用,进行必要的资源释放操作。