介绍
在 Java 中,默认情况我们使用的普通引用都是强引用(Strong Reference),通常我们并不关心这些引用对象是如何以及何时被垃圾回收。但在有些场景,我们需要更精细的控制对象何时可用以及垃圾回收的时机。
在 java.lang.ref
包下包含了一组类,这些类为对象的垃圾回收提供了更大的灵活性,当存在可能会耗尽内存的大对象时,这些类显得特别有用。有三个继承自抽象类 Reference 的类:SoftReference(软引用)、WeakReference(弱引用)、PhantomReference(虚引用),当垃圾回收器在进行对象 ”可达性“ 判断时,上述这些派生类为垃圾回收器提供了不同级别的间接指示。
💥对象可达(Reachable)是指对象可以在程序中的某处找到。这意味着在栈中有一个普通的引用指向这个对象,或者可能是一个引用指向某个对象,而这个对象含有另一个引用指向正在讨论的对象,也可能有更多的中间链接。如果一个对象是 ”可达的“,垃圾回收器就不能释放它,因为它正在为你的程序所用,换句话说,如果一个对象被强引用对象链引用时,它就不能被垃圾收集。如果一个对象不是 ”可达的“,那么你的程序也就无法使用到它,所以回收它是安全的。
强引用
如果没有特别指定,当我们创建一个对象时,这个对象的引用类型默认为强引用,builder
就是一个强引用,这种引用使得被引用的对象不符合 GC 的条件,也就是说,每当一个对象能够通过强引用进行访问时(可达时),它就不能被垃圾回收。
StringBuilder builder = new StringBuilder();
如上垃圾回收器无法对 StringBuilder 对象进行回收,因为我们在 builder 变量中持有对它的强引用。如果我们将builder 设置为 null
,此时垃圾回收器能够对其进行回收,因为没有任何引用指向它。
builder = null;
如果想继续持有对某个对象的引用,希望以后还能访问到该对象,但是也希望允许垃圾回收器释放它,这时就应该使用 Reference 对象。这样你可以继续使用该对象,而在内存消耗殆尽的时候又允许释放该对象。使用 Reference 对象作为你和普通对象之间的代理,此外一定不能有强引用指向这个对象,这样你就可以达到上述目的,如果垃圾回收器发现某个对象通过强引用是可达的,该对象就不会被释放。
Reference
Reference 对象的派生类 SoftReference、WeakReference、PhantomReference 对应着不同级别的 “可达性”,强度依次减弱。在创建这些对象时,可以为其绑定一个 ReferenceQueue,对于 SoftReference、WeakReference 这是可选的,而 PhantomReference 必须依赖于 ReferenceQueue。
接下来我们将对这些 Reference 派生类进行逐一学习,让我们做一些准备工作:
-
• 创建 BigObj 类用于模拟大对象,并覆盖了它的
finalize()
:
/** 模拟大对象 */
@Slf4j
@RequiredArgsConstructor
class BigObj {
private final String identity;
/** 100 MB */
private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 100);
@Override
protected void finalize() {
log.info("finalize " + identity);
}
@Override
public String toString() {
return "BigObj{" + "identity='" + identity + ''' + '}';
}
}
SoftReference
软引用(SoftReference)指示垃圾收集器能够根据内存需求自行决定对其所代理的对象进行清除,软引用通常用于实现内存敏感的应用。
当进行 GC 回收时,垃圾回收器如果确定对象只是软引用可达的,那么它可以选择对这些对象进行清除(但不是必须),然后将它们对应的 Reference 对象放入 ReferenceQueue 队列中(如果指定了的话)。在虚拟机抛出 OutOfMemoryError 之前,保证所有只能通过软引用访问的对象都已清除。对于清除软可达对象的时间和不同软可达对象之间的顺序,JDK 没有做任何强制限制,但是鼓励 JVM 实现偏向于清除最近创建或最近使用的软可达对象。
🍊API 使用
软用引的使用很简单,它提供了两个重载的构造方法,参数一用于指定要代理的对象,参数二用于指定 ReferenceQueue 队列(可选):
// 创建代理 BigObj 对象的软引用
SoftReference<BigObj> ref = new SoftReference<>(new BigObj("tom"));
// 创建代理 BigObj 对象并绑定 ReferenceQueue 的软引用
SoftReference<BigObj> ref = new SoftReference<>(new BigObj("jack"), refQueue);
通过 get
和 clear
方法,我们能够获取和清除其代理的对象,这些 API 继承自 Reference 类:
// 获取其代理的对象
BigObj obj = ref.get();
if(obj != null) {
// do something
}
// 释放
obj = null;
// 清除其代理的对象
ref.clear()
由于软引用指向的对象随时都有可能被回收,因此在使用之前我们应该进行判空检查,如果你将其赋值给了一个强引用,在使用完之后记得释放,否则将导致它不能被回收。
💥相比于下文讨论的弱引用(WeakReference),软引用指向的对象通常会存活更长的时间,它会尝试抵抗 GC 的回收,直到没有内存可用,并且存在抛出 OutOfMemoryError 的风险。注意:这不意味着普通 GC 无法回收软引用可达的对象,只是说它可能会存活更久,对于软引用,唯一的保证就是在抛出 OutOfMemoryError 之前所有只有软可达的对象都已被清除。
鉴于上述原因,你可能需要调整堆的大小才能看到它被回收的测试效果,通过 -Xms, -Xmx
参数去指定堆的大小:
// JVM 参数:-Xms200m -Xmx200m
public static void main(String[] args) {
// 创建代理 BigObj 对象的软引用
// BigObj 持有一个 100MB 的 buffer 属性
SoftReference ref = new SoftReference<>(new BigObj("tom"));
// 创建一个 BigObj 对象,触发内存不足,抛出 OutOfMemoryError
// ref 指向的 BigObj 对象 tom 将被回收,执行 BigObj 的 finalize()
new BigObj("jack");
}
// output
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space...
// finalize tom
🍊应用案例
软引用最常见的应用案例就是用于实现缓存,使用该类或拓展子类可用在更大的数据结构中以实现更复杂的缓存。Guava Cache 就是一个非常好的实现。
只要软引用的指示对象是强可达的,即实际正在使用,软引用就不会被清除。利用这一点,复杂的高速缓存可以通过保留对这些条目的强引用来防止丢弃其最近使用的条目,而让垃圾收集器自行决定丢弃剩余的条目。
ReferenceQueue
ReferenceQueue 用来作 “回收前的清理工作” 的工具。
当 Reference 代理的普通对象被清理时(准确的说是可达性状态发生变化时,下一篇文章详细分析),垃圾回收器会将对应的 Reference 对象放入 ReferenceQueue 队列中,并且通过相关 API 我们能够获取到 Reference 对象。利用这一点,我们能够知道对象是何时被垃圾回收器回收的,然后可以去执行一些清理工作(如 Java 堆外资源的重新分配)。
💥当 Reference 代理的普通对象被清理时 Reference 对象要是可达的,即 Reference 对象本身没有被回收,这样 Reference 对象才能被放入 ReferenceQueue 队列中,否则将不会入队列。
ReferenceQueue 提供的 API 如下:
-
•
poll()
: 轮询队列查看是否有可用 Reference 对象,如果队列为空,该方法立即返回 null,否则返回队首的对象并将其从队列中删除。 -
•
remove()
: 删除队首的元素,如果队列为空,则阻塞直到有一个可用。 -
•
remove(long timeout)
: 删除队首的元素,如果队列为空,则阻塞直到有一个引用对象可用或给定的超时期限到期。
接下来我们来看一个如何利用 ReferenceQueue 去监听对象被何时被 GC 回收的一个简单例子。
private static final ReferenceQueue<BigObj> refQueue = new ReferenceQueue<>();
static void useReferenceQueueToListenObjectReclaim() {
// 创建 WeakReference 并代理 BigObj,并将其绑定至 ReferenceQueue
WeakReference<BigObj> weakRef = new WeakReference<>(new BigObj("tom"), refQueue);
// 启动一个新线程监听对象被 GC 回收的状态
CompletableFuture.runAsync(() -> {
while (true) {
// 通常会定期的调用 poll 去轮询队列中是否有引用对象
Reference<? extends BigObj> ref = refQueue.poll();
// weakRef 对象入队列,表示其代理的对象被 GC 回收,此时 ref.get() 获取被代理的对象总是返回 null
if (ref != null) {
log.info("监听到对象被回收啦, 这里可以执行一些清理工作, ref == weakRef: {},
ref.get() = {}", ref == weakRef, ref.get());
break;
}
ThreadUtils.silentSleep(100, TimeUnit.MILLISECONDS);
}
});
// 通过 WeakReference 获取其代理的对象
BigObj obj = weakRef.get();
if(obj != null) {
log.info("GC 回收之前, ref.get() = {}", weakRef.get());
}
obj = null; // 清空引用,使其能被 GC 回收
// 执行 GC
System.gc();
// 执行 GC 后,通过 WeakReference 获取其代理的对象为 null
log.info("执行 GC 之后, ref.get() = {}", weakRef.get());
}
// ouput
// GC 回收之前, ref.get() = BigObj{identity='tom'}
// 执行 GC 之后, ref.get() = null
// finalize tom
// 监听到对象被回收啦, 这里可以执行一些清理工作, ref == weakRef: true, ref.get() = null
让我们分析一下上述代码:
-
1. 我们声明了一个静态的 ReferenceQueue 对象 refQueue。
-
2. 创建 WeakReference 并代理 BigObj 对象,并将其绑定至 ReferenceQueue。
-
3. 创建一个新线程用于监听对象 GC 回收的状态: 当对象被回收时,它对应的 Reference 对象会被放入 ReferenceQueue 队列,通过在 while 循环定期调用队列的 poll 方法,直到 poll 返回值不为 null,代表对象被回收,现在我们可以去做一些清理工作,注意此时通过返回的 Reference 获取对象总是返回 null,因为对象已被回收。
-
4. 在对象被 GC 回收之前,我们可以通过
weakRef.get()
获取它,但由于在实际使用时我们并不清楚其何时被回收,因此正确的做法是每次在使用前对其进行判空。此时需要意识到存在一个强引用指向它,如果引用没有被释放,它将不会被 GC 回收。 -
5. 执行
System.gc()
,向 JVM 发出一个请求 GC 回收的信号(JVM会尽最大的努力去执行,但不保证一定执行)。在对象被 GC 回收时,也会调用它的finalize
方法,该方法通常用来执行一些清理工作(Java 语言规范不保证finalize
方法会被及时的执行,也不保证一定执行)。 -
6. 执行 GC 后,通过
weakRef#get
总是返回 null,因为对象已被回收。
💥通过 CompletableFuture 默认创建的异步任务使用的是 ForkJoinPool.commonPool()
系统级线程池,因此都是后台(daemon)线程启动,这意味着当所有非后台线程(user thread) 结束后,程序也就终止了,所以如果在 main 方法中运行上述代码,你可能看不到监听线程的输出,有很多种方法解决这个问题:
-
1. 注释
ThreadUtils.silentSleep()
。 -
2. 在 main 执行一些耗时操作,如
Thread.sleep()
。 -
3. 指定使用的线程池,让其以用户线程(user thread)启动,如
CompletableFuture.runAnsyc(task, Executors.newSingleThreadExecutor())
。
💥GC 执行 finalize()
流程:当对象变得(GC Roots)不可达进行回收时,GC 会判断对象是覆盖了 finalize()
,如果未覆盖则直接回收,否则执行 finalize()
,将其放入 Finalizer 队列中,由一组低优先级的线程进行执行,执行 finalize()
之后,GC 会再次判断该对象是否可达,若不可达则回收,否则,如果在 finalize()
中对象被赋值给其他引用,则对象复活。注意 JVM 保证 finalize()
最多被执行一次,对象复活后再次被 GC finalize()
不再被执行。
原文始发于微信公众号(Codingloop):软引用、弱引用、虚引用
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/251579.html