深入理解 java 中的 Soft references & Weak references & Phantom reference

引言

Ethan Nicholas 在他的一篇文章中说:他面试了20多个Java高级工程师,他们每个人都至少有5年的Java从业经验,当他问这些工程师对于Weak References 的理解时,只有其中的2个人知道Weak References 的存在,而这2个人中,只有1人知道如何去使用Weak References,而其他人甚至都不知道Java中有Weak References的存在。

大家可能会想,我接触了Java这么多年,从来都没有使用过Weak References啊,它真的是一个有用的技术吗?对于它有用没用,我相信大家看完这篇文章,理解了它以后,自有判断。如果你从业了3年以上的Java开发,你不知道如何使用Weak References也还可以原谅,有可能你的项目还没有那么复杂。但是如果你甚至都没见过它在哪使用的,我觉得你可能读的源码太少了。我相信大家都知道ThreadLocal 这个类吧,你可以去看一下它的静态内部类ThreadLocalMap, 如果你想见到更多它的应用,我给大家推荐个网站:searchcode,这个网站会根据你输入的代码片段,来搜索几大源码托管平台上使用你输入代码片段的工程,大家可以输入WeakReference试一试。

并不说只有你成为一个Weak References方面的专家,你才是一个优秀的Java开发者,但是,你至少要了解我们在什么样的场景下需要使用它,That’s enough.

由于理解Weak References && soft references 会涉及到JVM的垃圾收集的一些知识,如果你对这方面没有了解,请你参考我的这篇文章:Hotspot虚拟机- 垃圾收集算法和垃圾收集器

Java中的4种reference

在Java中,有4种reference类型,它们从强到弱,依次如下:

  1. Strong reference : 大家平常写代码的引用都是这种类型的引用,它可以防止引用的对象被垃圾回收。
  2. Soft reference : 它引用的对象只有在内存不足时,才会被回收。
  3. Weak reference : 它并不会延长对象的生命周期,即它不能阻止垃圾收集器回收它所引用的对象。
  4. Phantom reference : 它与上面的3种类型有很大的不同,它的get() 方法始终返回null,即通过这个引用,你甚至都不能获取它所引用的对象,如果你看它的源码,它的构造器必须要给定一个ReferenceQueue,当然了,你也可以把它设置为空,但是这样的引用 一点意义都没有。我在下文中会结合Phantom reference的作用来解释为什么会这样。

在这一小节中,我总结了各个引用的作用。如果大家不太明白,没关系,我会在下文中更详细地解释它们各自的用法。

Strong reference

如果大家对垃圾收集机制有所了解,你们就会知道JVM标记一个对象是否为垃圾是根据可达性算法。 我们平常写的代码其实都是Strong reference,被Strong reference所引用的对象它会保持这个对象到GC roots的可达性,以防被JVM标记为垃圾对象,从而被回收。比如下面的代码就是一个Strong reference

String str = new String("hello world");

Soft reference

GC使Java程序员免除管理内存的痛苦,但是这并不意味着我们可以不关心对象的生命同期,如果我们不注意Java对象地生命周期,这很可能会导致Java出现内存泄露。

Object loitering

在我详细解释Soft reference之前,请大家先阅读下面的这段代码,仔细想一想它可能出现什么样的问题?

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
    }
}

对于上面的程序而言,如果我把byteArray字节数组放到getFileChecksum方法中完全没有问题,但是,上面的程序把byteArray字节数组从局部变量提升到实例变量会出现很多问题。比如,由于你需要共享byteArray变量,从而你不得不去考虑线程安全问题,而上面的程序在getFileChecksum方法上加上了synchronized 关键字,这大大降低了程序的可扩展性。

先不去深入讨论上面程序出现的其它问题,让我们来探讨一下它出现的内存泄露问题。上述代码的主要功能就是根据文件的内容去计算它的checksum,如果上述代码的if 条件不成立,它会不断地重用字节数组,而不是重新分配它。除非LeakyChecksum对象被gc,否则这个字节数组始终不会被gc,由于程序到它一直是可达的。而且更糟糕的是,随着程序的不断运行,这个字节数组只会不断增大,不会减小,它的大小始终都和它处理过的最大的文件的大小一致,这样很可能会导致JVM更频繁地GC,降低应用程序地性能。大多数情况下,这个字节数组所占的空间要比它实际要用的空间要大,而多余的空间又不能被回收利用,这导致了内存泄露。

Soft references 解决上面的内存泄露问题

对于只被Soft references所引用的对象,我们称它为softly reachable objects. 只要可得到的内存很充足,softly reachable objects 通常不会被gc. JVM要比我们的程序更加了解内存的使用情况,如果可得到的内存紧张,那么JVM就会频繁地进行垃圾回收,从而释放更多的内存空间,供我们使用。因此,上述程序的字节数组缓存由于一直是可达的,即使在内存很紧张的情况下,它也不会被回收掉,这无疑给垃圾收集器更大的压力,使其更频繁地GC.

那么有没有一种解决方案可以做到这样呢,如果我们的内存很充足,我们就保持这样的缓存在内存中,不被gc; 但是,当我们的内存吃紧时,就把它释放掉。那么大家想一想,谁可以做到这一点呢?答案是JVM,因为它最了解内存的使用情况,我们可以借助它的力量来达到我们的目标,而Soft references 可以帮我们Java 程序员借助JVM的力量。下面,让我们来看看如果用SoftReference 改写上面的代码。

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
    }
}

从上面的代码我们可以看出,一旦走出if 语句,字节数组对象就只被Soft references 所引用,成为了softly reachable objects. 对于垃圾收集器来说,它只会在真正需要内存的时候才会去回收softly reachable objects. 现在,如果我们的内存不算吃紧,这个字节数组buffer会一直保存在内存中。在抛出OutOfMemoryError 之前,垃圾收集器一定会clear掉所有的soft references.

Soft references 与缓存

从上面的例子中,我们看到了如何用soft reference 去缓存1个对象,然后让JVM去决定什么时候应该把对象从缓存中清除。对于严重依赖缓存提升性能的应用而言,用Soft references 做缓存并不合适,我们应该去找一个更全面的缓存框架去做这件事。但是由于它 “cheap and dirty” 的缓存机制, 对于一些小的应用场景,它还是很有吸引力的。

Weak References

由于JVM帮我们管理Java程序的内存,我们总是希望当一个对象不被使用时,它会被立即回收,即一个对象的逻辑生命周期要与它的实际生命周期相一致。但是有些时候,由于写程序人的疏忽,没有注意对象的生命周期,导致对象的实际生命周期要比我们期望它的生命周期要长。这种情况叫做 unintentional object retention. 下面我们来看看由实例变量HashMap 导致的内存泄露问题。

HashMap 导致的内存泄露问题

用Map去关联短暂对象的元数据很容易出现unintentional object retention 问题。比如你想关联一个Socket连接与用户的信息,由于你并不能干涉Socket 对象的实现,向里面加用户数据,因此最常用的做法就是用全局Map 来做这样的事。代码如下:

public class SocketManager {
   
    private Map<Socket,User> m = new HashMap<Socket,User>();
     
    public void setUser(Socket s, User u) {
   
        m.put(s, u);
    }
    public User getUser(Socket s) {
   
        return m.get(s);
    }
    public void removeUser(Socket s) 
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值