JVM在实践中如何收集软引用?

2022-09-03 02:10:30

我在JVM中运行了两个单独的缓存(一个由第三方库控制),每个缓存都使用软引用。我更喜欢 JVM 在库控制的缓存之前清除我的受控缓存。SoftReference javadoc 指出:

在虚拟机抛出 OutOfMemoror 之前,保证已清除对可软访问对象的所有软引用。否则,不会对清除软引用的时间或清除对不同对象的一组此类引用的顺序施加任何限制。但是,鼓励虚拟机实现对清除最近创建或最近使用的软引用产生偏见。

此类的直接实例可用于实现简单缓存;此类或派生的子类也可以在较大的数据结构中使用,以实现更复杂的缓存。只要软引用的引用是强可及的,即实际在使用中,软引用就不会被清除。因此,例如,复杂的缓存可以通过保留对这些条目的强引用来防止其最近使用的条目被丢弃,而剩余的条目则由垃圾回收器自行决定丢弃。

在实践中,常见的JVM实现,特别是HotSpot,如何处理软引用?他们是否像规范所鼓励的那样“对清除最近创建或最近使用的软引用有偏见”?


答案 1

看起来它可以调整,但事实并非如此。并发标记扫描收集器挂起在默认堆的实现上,显然仅在执行 .must_clear_all_soft_refs()true_last_ditch_collection

bool GenCollectedHeap::must_clear_all_soft_refs() {
  return _gc_cause == GCCause::_last_ditch_collection;
}

虽然对失败分配的正常处理对堆的方法有三次连续调用,但在do_collectCollectorPolicy.cpp

HeapWord* GenCollectorPolicy::satisfy_failed_allocation(size_t size,
                                                    bool   is_tlab) {

它尝试收集,尝试重新分配,尝试在失败时扩展堆,然后作为最后的努力,尝试收集清除软引用。

对最后一个集合的评论非常有说服力(也是唯一一个触发清除软引用的评论)

  // If we reach this point, we're really out of memory. Try every trick
  // we can to reclaim memory. Force collection of soft references. Force
  // a complete compaction of the heap. Any additional methods for finding
  // free memory should be here, especially if they are expensive. If this
  // attempt fails, an OOM exception will be thrown.
  {
    IntFlagSetting flag_change(MarkSweepAlwaysCompactCount, 1); // Make sure the heap is fully compacted

    gch->do_collection(true             /* full */,
                       true             /* clear_all_soft_refs */,
                       size             /* size */,
                       is_tlab          /* is_tlab */,
                       number_of_generations() - 1 /* max_level */);
  }

--- 为了回应显而易见的事实,我描述的是弱引用,而不是软引用---

在实践中,我想象当JVM被要求进行垃圾回收以响应它们试图避免.OutOfMemoryError

要使 s 与所有四个 Java 1.4 垃圾回收器以及新的 G1 收集器兼容,决策必须仅取决于可访问性确定。当收获和压缩发生时,确定对象是否可访问为时已晚。这表明(但不要求)存在一个集合“上下文”,该上下文根据堆中的可用内存可用性确定可访问性。这样的上下文必须表明在尝试遵循它们之前不遵循。SoftReferenceSoftReference

由于避免垃圾回收是以完全收集、停止世界的方式专门安排的,因此堆管理器在收集发生之前设置“不要遵循”标志的情况并不难想象。OutOfMemoryErrorSoftReference

---好吧,所以我决定“必须以这种方式工作”的答案还不够好---

从源代码 src/share/vm/gc_implementation/concurrentMarkSweep/vmCMSOperations.cpp(亮点是我的)

实际“执行”垃圾回收的操作:

  170 void VM_GenCollectFullConcurrent::doit() {

我们最好是一个VM线程,否则“程序”线程就是垃圾回收!

  171   assert(Thread::current()->is_VM_thread(), "Should be VM thread");

我们是并发收集器,因此最好同时安排!

  172   assert(GCLockerInvokesConcurrent || ExplicitGCInvokesConcurrent, "Unexpected");
  173 

抓取堆(其中包含 GCCause 对象)。

  174   GenCollectedHeap* gch = GenCollectedHeap::heap();

检查我们是否需要前景“年轻”集合

  175   if (_gc_count_before == gch->total_collections()) {
  176     // The "full" of do_full_collection call below "forces"
  177     // a collection; the second arg, 0, below ensures that
  178     // only the young gen is collected. XXX In the future,
  179     // we'll probably need to have something in this interface
  180     // to say do this only if we are sure we will not bail
  181     // out to a full collection in this attempt, but that's
  182     // for the future.

程序线程是否不与堆进行干预?

  183     assert(SafepointSynchronize::is_at_safepoint(),
  184       "We can only be executing this arm of if at a safepoint");

从堆中提取垃圾回收原因(此收集的原因)。

  185     GCCauseSetter gccs(gch, _gc_cause);

做一个完整的年轻空间的集合

请注意,他在堆的 must_clear_all_soft_refs 标志的值中传递,在 OutOfMemory 方案中必须设置为 true,并且在任一情况下都指示“do_full_collection”不跟随软引用

  186     gch->do_full_collection(gch->must_clear_all_soft_refs(),
  187                             0 /* collect only youngest gen */);

_gc_cause是一个枚举,它(此处为猜测)设置为在第一次尝试避免时,之后失败(尝试收集暂时性垃圾)_allocation_failureOutOfMemoryError_last_ditch_collection

快速浏览一下内存“堆”模块,就会发现调用软引用被显式清除(在“正确”条件下),并带有该行do_full_collectiondo_collection

  480   ClearedAllSoftRefs casr(do_clear_all_soft_refs, collector_policy());

--- 原始帖子适用于那些想要了解弱引用---

在 Mark and Sweep 算法中,不会从主线程跟踪软引用(因此不会标记,除非其他分支可以通过非软引用到达它)。

在复制算法中,不会复制对象软引用指向(同样,除非它们由其他非软引用到达)。

基本上,当从执行的“主”线程跟踪引用网络时,不会遵循软引用。这允许对它们的对象进行垃圾回收,就像它们没有指向它们的引用一样。

值得一提的是,软引用几乎从不孤立使用。它们通常用于设计对对象具有多个引用的对象,但只需要清除一个引用即可触发垃圾回收(为了便于维护容器,或者不需要查找昂贵引用的运行时性能)。


答案 2

在 HotSpot FAQ 中发现了一条信息,这些信息可能已过时:http://www.oracle.com/technetwork/java/hotspotfaq-138619.html#gc_softrefs

什么决定了何时刷新软引用对象?

从 1.3.1 开始,可软访问的对象将在上次引用后保持活动状态一段时间。默认值为堆中每个可用兆字节的生存期一秒。可以使用 -XX:SoftRefLRUPolicyMSPerMB 标志调整此值,该标志接受表示毫秒的整数值。例如,要将值从 1 秒更改为 2.5 秒,请使用以下标志:

-XX:SoftRefLRUPolicyMSPerMB=2500

Java HotSpot Server VM 使用最大可能的堆大小(使用 -Xmx 选项设置)来计算剩余可用空间。

Java 热点客户端 VM 使用当前堆大小来计算可用空间。

这意味着服务器 VM 的一般趋势是增加堆而不是刷新软引用,因此 -Xmx 对何时对垃圾回收软引用有重大影响。

另一方面,客户端 VM 将更倾向于刷新软引用,而不是增加堆。

上述行为适用于 1.3.1 到 Java SE 6 版本的 Java HotSpot 虚拟机。但是,此行为不是 VM 规范的一部分,并且可能会在将来的版本中发生更改。同样,-XX:SoftRefLRUPolicyMSPerMB 标志也不保证存在于任何给定的版本中。

在版本 1.3.1 之前,Java HotSpot VM 会在找到软引用时清除它们。

更多细节见:http://jeremymanson.blogspot.com/2009/07/how-hotspot-decides-to-clear_07.html(由MiserableVariable的评论提供)