但凡是写过几行java代码的,都知道java中的引用分为4种: 强引用 、 软引用 、 弱引用 、 虚引用 。
其中广大开发者最热衷的莫过于 软引用 了。因为它能保证在内存足够时,我们创建的对象完好的存活在内存中。同时当内存不足时,则将软引用指向的对象交由GC回收。
抛个砖
但是Java工程师不能认为SoftReference就是万无一失的保险锁,并且肆无忌惮的使用。
被 软引用 对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,并且是一个 强引用
上面这句话有点绕,不是很容易理解。用一张图来表示如下:
用一段更具体的代码表示如下:
1 软引用也是强引用
虽然MyObject被软引用SoftReference引用,但是软引用对象自身被强引用集合Set所引用,这机会导致SoftReference对象本身不会被GC回收掉。如果我们不断的向Set中添加对象,终将导致OOM。如下所示:
虽说也是OOM,但是 造成 OOM的原因却有点特殊:
java.lang.OutOfMemoryError : GC overhead limit exceeded
造成这种OOM的原因在于: 虚拟机一直在不断回收软引用,回收进行的速度过快,占用的cpu过大(超过98%),并且每次回收掉的内存过小(小于2%),导致最终抛出了这个错误。
2 内存足够,软引用也能被GC回收
除了OOM,软引用还有另外一个问题或许会刷新你对它的认知。
比如如下代码:
运行结果如下:
也就是说GC之后,内存足够所以 软引用 所关联的SoftObject并没有被GC回收掉。这是正常运行下的情况,很容易理解。
但是如果对代码作如下改动:
主要是添加了一个2s的睡眠,并再次调用一次GC操作。这时再次运行效果如下:
加了一个2s的休眠,再次执行一次GC, 软引用 竟然被回收了!!!!
引个玉
为了弄清楚为什么会发生上述情况,那就必须深入分析JVM内部GC时是如何处理 软引用 的。接下来看下GC回收这部分源码是如何对SoftReference做处理的。
先看下SoftReference的源码:
在SoftReference中有两个比较重要的变量:
clock: 在JVM发生GC时,也会更新clock的值,所以clock会记录上次GC发生的时间点
timestamp: 记录对象被访问(get方法被调用)时最近一次GC的时间
我们经常说 软引用 是在内存不足时被GC回收,但是如何定义“内存不足”就是通过clock和timestamp这两个变量来决定的。
接下来看下GC回收时的源码,大部分实现代码是在c++层 ReferenceProcessor.cpp 中。
当GC开始时,将 _soft_ref_timestamp_clock 设置为当前时间,它对应SoftReference中的clock变量。如下:
当垃圾回收器遍历完所有的GC Roots之后,在执行对象清理之前,会调用 ReferenceProcessor::process_discovered_references 函数。 在这个函数中对所有的引用进行处理,用来区分哪些引用是可以清理的,哪些是不能清理的。
可以看出,JVM中的各种引用都是在这个方法中进行处理的。包括 软引用 、 弱引用 、 虚引用 等。重点来看对 软引用 的处理:
重点代码就是在 process_phase 1 函数中,这个函数主要作用就是判断当前系统中的“内存是否足够”,如果内存足够,则将 软引用 从refs_list中移除,如下:
如上图红框所示,GC最终会调用一个 policy 的 should_clear_reference 函数来决定这个 软引用 是否需要清除。
policy就是虚拟机在执行GC时,对 软引 用 引用的回收策略。一共有4 种回收策略:
软引用回收策略 | 具体策略 |
NeverClearPolicy | 从不清理 |
AlwaysClearPolicy | 总是清理 |
LRUCurrentHeapPolicy | 最近未使用即清理(根据当前堆空间剩余来评估最近时间) |
LRUMaxHeapPolicy | 最近未使用即清理(根据最大可使用堆空间剩余来评估最近时间) |
对于NeverClearPolicy和AlwaysClearPolicy我们基本不会使用到,不需要关注。
LRUCurrentHeapPolicy和LRUMaxHeapPolicy中 should_clear_reference 函数的实现一致,如下所示:
可以看出,是否需要清理 软引用 跟几个条件有关:
interval:当前时间与上次GC回收的执行时间
_max_interval: 最大回收间隔
LRUCurrentHeapPolicy 和 LRUMaxHeapPolicy唯一的不同就是对 _max_interval 的初始化值不同:
从图中可以看出:
LRUCurrentHeapPolicy使用 get_head_at_last_gc() 获取的是当前可用堆的大小
LRUMaxHeapPolicy使用 get_heap_used_at_last_gc() 获取的是最大堆大小
总结一下就是:
GC对于存活时间大于 _max_interval 的软引用会进行回收。而这个 _max_interval 的值是基于对内存大小和上次GC回收时间一起计算出来的。
最后用一个公式来表示一个软引用是否可以被回收如下:
划重点: 我们平时说软引用会在内存不足时被GC回收。这里说的内存不足不仅仅是指空间大小,还有时间的限制。这就解释了为什么在文章开始的例子中睡眠2s之后,再执行GC 软引用 就被回收了!
小验证
在上面的源码中,有一个参数--SoftRefLRUPolicPerMB 。我们在执行java命令时可以通过 -XX:SoftRefLRUPolicyMSPerMB 这个参数来设置它的值。
既然如此,那不妨将参数设置大一点,如下所示设置2s,再次执行之前的代码,则软引用不会被回收,也间接验证了上面源码的执行流程。
脑袋是不是嗡嗡的了?鉴于这种情况,Android官方在对SoftReference的介绍中,也已经不建议使用它来实现缓存功能:
原因就是因为 SoftReference 无法提供足够的信息可以让 runtime 很轻松地决定 clear 它还是 keep 它。