循环引用内存泄漏_使用软引用堵塞内存泄漏

垃圾收集可能使Java程序不受内存泄漏的影响,至少对于“内存泄漏”的定义足够狭窄,但这并不意味着我们可以完全忽略Java程序中对象生存期的问题。 当我们对对象生命周期的关注不足或破坏管理对象生命周期的标准机制时,Java程序中的内存泄漏通常会发生。 例如, 上次我们看到在试图将元数据与瞬态对象相关联时,未能明确划分对象的生命周期如何导致意外的对象保留。 还有其他成语可以类似地忽略或破坏对象生命周期管理,也可能导致内存泄漏。

游荡对象

清单1中的LeakyChecksum类说明了一种内存泄漏形式,有时也称为对象游荡 ( LeakyChecksum该类提供了getFileChecksum()方法来计算文件内容的校验和。 getFileChecksum()方法将文件的内容读取到缓冲区中以计算校验和。 一个更直接的实现将只是将缓冲区分配为getFileChecksum()的局部变量,但是此版本比之更“聪明”,而是将缓冲区缓存在实例字段中以减少内存流失。 这种“优化”通常无法实现预期的节省。 对象分配比许多人认为的便宜。 (还要注意,将缓冲区从局部变量升级为实例变量会使类在没有其他同步的情况下不再具有线程安全性;简单的实现不需要将getFileChecksum()声明为synchronized并且在并发调用时将提供更好的可伸缩性。)

清单1.表现出“对象游荡”的类
// BAD CODE - DO NOT EMULATE
public class LeakyChecksum {
    private byte[] byteArray;
    
    public synchronized int getFileChecksum(String fileName) {
        int len = getFileSize(fileName);
        if (byteArray == null || byteArray.length < len)
            byteArray = new byte[len];
        readFileContents(fileName, byteArray);
        // calculate checksum and return it
    }
}

此类有很多问题,但让我们关注内存泄漏。 缓存的决定最有可能是基于这样的假设,即在程序中将多次对其进行调用,因此,重用缓冲区而不是重新分配缓冲区将更加有效。 但是结果是,缓冲区永远不会被释放,因为程序始终可以访问该缓冲区(除非LeakyChecksum对象被垃圾回收)。 更糟糕的是,虽然它可以增长,但无法缩小,因此LeakyChecksum永久保留与处理的最大文件一样大的缓冲区。 至少,这对垃圾收集器造成了压力,并需要更频繁地收集垃圾。 为计算将来的校验和而保持较大的缓冲区可能不是最有效地使用可用内存。

LeakyChecksum中出现问题的LeakyChecksum是该缓冲区在逻辑上是getFileChecksum()操作的局部区域,但是通过将其提升为实例字段来人为地延长了其生命周期。 结果,该类必须管理缓冲区本身的生命周期,而不是让JVM执行。

软参考

在上一期中 ,我们看到了弱引用如何为应用程序提供一种在程序使用对象时到达对象的替代方法,而又不延长其寿命。 Reference另一个子类(软引用)可以实现不同但相关的目的。 在弱引用允许应用程序创建不干扰垃圾收集的引用的情况下,软引用允许应用程序通过将某些对象指定为“可消耗”来争取垃圾收集器的帮助。 尽管垃圾收集器在弄清楚应用程序正在使用和不使用哪些内存方面做得很好,但是由应用程序决定可用内存的最合适用途。 如果应用程序对保留哪些对象的决定不当,则性能会受到影响,因为垃圾回收器必须更努力地工作才能防止应用程序用尽内存。

缓存是一项常见的性能优化,它允许应用程序重用先前计算的结果,而不用重新计算。 缓存是CPU利用率和内存使用率之间的折衷,这种折衷的理想平衡取决于可用的内存量。 如果缓存太少,将无法获得预期的性能优势; 如果使用过多的内存,则性能可能会受到影响,因为在缓存上消耗了太多的内存,因此没有足够的可用空间用于其他目的。 因为垃圾收集器通常比应用程序更适合确定内存需求,所以在做出这些决定时请垃圾收集器帮助是有道理的,这就是软引用对我们的作用。

如果仅剩余的对对象的引用是弱引用或软引用,则称该对象是可软到达的 。 垃圾收集器不会像处理弱可访问对象那样积极地收集可软访问对象-而是仅在确实“需要”内存时才收集可软访问对象。 软引用是对垃圾收集器说的一种方式,“只要内存不太紧,我想保留这个对象。但是如果内存真的很紧,那就继续收集它,我会处理接着就,随即。” 垃圾收集器需要清除所有软引用,然后才能抛出OutOfMemoryError

我们可以使用软引用来管理缓存的缓冲区,从而解决LeakyChecksum的问题,如清单2所示。现在,只要不急需该内存,就保留该缓冲区,但是如果有必要,可以由垃圾回收器将其回收。 :

清单2.使用软引用修复LeakyChecksum
public class CachingChecksum {
    private SoftReference<byte[]> bufferRef;
    
    public synchronized int getFileChecksum(String fileName) {
        int len = getFileSize(fileName);
        byte[] byteArray = bufferRef.get();
        if (byteArray == null || byteArray.length < len) {
            byteArray = new byte[len];
            bufferRef.set(byteArray);
        }
        readFileContents(fileName, byteArray);
        // calculate checksum and return it
    }
}

穷人的藏书室

CachingChecksum使用软引用来缓存单个对象,并让JVM处理何时从缓存中逐出对象的细节。 同样,软引用也经常在GUI应用程序中用于缓存位图图形。 是否可以使用软引用的关键是应用程序是否可以从正在缓存的数据丢失中恢复。

如果需要缓存多个对象,则可以使用Map ,但是可以选择如何使用软引用。 您可以将缓存作为Map<K, SoftReference<V>>SoftReference<Map<K,V>> 。 后一种选项通常是可取的,因为它对收集器的工作量较小,并且在内存需求量很大时,可以更轻松地回收整个缓存。 有时会错误地使用弱引用而不是软引用来构建缓存,但这会导致缓存性能下降。 在实践中,弱引用将在对象变得难以访问之后(通常在再次需要缓存的对象之前)很快被清除,因为次要垃圾回收会频繁运行。

对于严重依赖缓存来提高性能的应用程序,软引用可能太过迟钝了,它当然不能替代提供灵活到期,复制和事务缓存的复杂缓存框架。 但是,作为一种“廉价而肮脏的”缓存机制,它是一种具有吸引力的性价比。

与弱引用一样,可以使用关联的引用队列创建软引用,并且当垃圾回收器清除该引用时,该引用就会排队。 对于软引用,引用队列没有像弱引用那样有用,但是它们可用于引发管理警报,提示应用程序开始内存不足。

垃圾收集器如何处理参考

弱引用和软引用都扩展了抽象Reference类( 幻像引用也是如此) ,将在以后的文章中进行讨论。 引用对象由垃圾收集器专门处理。 当垃圾收集器在跟踪堆的过程中遇到Reference时,它不会标记或跟踪引用对象,而是将Reference放置在已知活动Reference对象的队列中。 跟踪之后,收集器将识别软可访问对象-这些对象不存在强引用,但软引用存在。 然后,垃圾收集器根据当前收集回收的内存量和其他策略考虑因素,评估此时是否需要清除软引用。 如果要清除的软引用具有相应的引用队列,则将它们排队。 然后将其余的软可访问对象(未清除的对象)视为一个根集,并使用这些新的根继续进行堆跟踪,以便可以标记通过活动软引用可访问的对象。

在处理软引用之后,将识别出一组弱可访问对象-没有强引用或软引用的对象。 这些已清除并排队。 所有Reference类型在入队之前都已清除,因此处理事后清除的线程永远无法访问引用对象,只能访问Reference对象。 因此,将References与引用队列一起使用时,通常会子类化适当的引用类型,然后直接在设计中使用它(如WeakHashMap ,其Map.Entry扩展了WeakReference )或存储对实体的引用需要清理。

参考处理的性能成本

参考对象在垃圾回收过程中引入了一些额外的成本。 在每个垃圾回收中,必须构造一个活动的Reference对象的列表,并且必须适当地处理每个引用,这会增加每个Collection的每个Reference开销,而不管当时是否在收集引用。 Reference对象本身要进行垃圾回收,并且可以在引用对象之前进行收集,在这种情况下,它们不会排队。

基于数组的集合

当数组用于实现数据结构(例如堆栈或循环缓冲区)时,会出现对象游荡的另一种形式。 清单3中的LeakyStack类显示了由数组支持的堆栈的实现。 在pop()方法中,递减顶部指针后, elements仍然维护对从堆栈弹出的对象的引用。 这意味着即使该程序不再实际使用该引用,该程序仍然可以访问该对象,这可以防止该对象被垃圾回收,直到该位置被将来的push()重用为止。

清单3.在基于数组的集合中游荡对象
public class LeakyStack {
    private Object[] elements = new Object[MAX_ELEMENTS];
    private int size = 0;
    
    public void push(Object o) { elements[size++] = o; }
    
    public Object pop() { 
        if (size == 0)
            throw new EmptyStackException();
        else {
            Object result = elements[--size];
            // elements[size+1] = null;
            return result;
        } 
    }
}

在这种情况下,对象游荡的解决方法是将参考从堆栈中弹出后将其清空,如清单3中注释掉的代码行所示。但是,在这种情况下-类管理自己的内存-在少数情况下,将不再需要的对象明确为空是一个好主意。 在大多数情况下,激进地使被认为是未使用的引用无效会导致根本不提高性能或内存利用率。 通常会导致性能NullPointerExceptionNullPointerException 。 该算法的链接实现将不会出现此问题。 在链接的实现中,链接节点的生存期(以及因此对要存储的对象的引用)将自动与对象存储在集合中的持续时间绑定在一起。 弱引用可以用来解决此问题-维护一系列弱引用而不是强引用-但实际上, LeakyStack管理自己的内存,因此负责确保清除对不再需要的对象的引用。 使用数组来实现堆栈或缓冲区是一种优化,它减少了分配,但给实现者带来了更大的负担,使其必须认真管理存储在数组中的引用的生存期。

摘要

软引用(如弱引用)可以通过在垃圾回收收集器的帮助下进行垃圾回收决策来帮助应用程序防止对象游荡。 仅当应用程序可以容忍丢失软引用对象时,软引用才适合使用。


翻译自: https://www.ibm.com/developerworks/java/library/j-jtp01246/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值