你不可不知的Java引用类型之——SoftReference源码详解
定义
SoftReference是软引用,其引用的对象在内存不足的时候会被回收。只有软引用指向的对象称为软可达(softly-reachable)对象。
说明
垃圾回收器会在内存不足,经过一次垃圾回收后,内存仍旧不足的时候回收掉软可达对象。在虚拟机抛出OOM之前,会保证已经清除了所有指向软可达对象的软引用。
如果内存足够,并没有规定回收软引用的具体时间,所以在内存充足的情况下,软引用对象也可能存活很长时间。
JVM会根据当前内存的情况来决定是否回收softly-reachable对象,但只要referent有强引用存在,该referent就一定不会被清理,因此SoftReference适合用来实现memory-sensitive caches。软引用的回收策略在不同的JVM实现会略有不同。
另外,JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近使用情况和创建时间来综合决定是否回收该referent。
一般而言,SoftReference对象会在垃圾回收器回收其内部referent后,才会被放入其注册的引用队列中(如果创建时注册了的话)。
Soft reference objects, which are cleared at the discretion of the garbage collector in response to memory demand.
就是说,软引用具体什么时候回收最终还是由虚拟机自己决定的,所以不同虚拟机对软引用的回收方式会有些不一样。
SoftReference源码
public class SoftReference<T> extends Reference<T> {
/**
* 由垃圾回收器负责更新的时间戳
*/
static private long clock;
/**
* 在get方法调用时更新的时间戳,当虚拟机选择软引用进行清理时,可能会参考这个字段。
*/
private long timestamp;
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
/**
* 返回引用指向的对象,如果referent已经被程序或者垃圾回收器清理,则返回null。
*/
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
SoftReference类内部代码很少,两个成员变量,clock是一个静态变量,是由垃圾回收器负责更新的时间戳,在JVM初始化时,会对变量clock进行初始化,同时,在JVM发生GC时,也会更新clock的值,所以clock会记录上次GC发生的时间点。
timestamp是在创建和更新时更新的时间戳,将其更新为clock的值,垃圾回收器在回收软引用对象时可能会参考timestamp。
SoftReference类有两个构造函数,一个是不传引用队列,一个传引用队列。在创建时,都会更新timestamp,将其赋值为clock的值,get方法也并没有什么骚操作,只是简单的调用 super.get() 并在返回值不为null时更新timestamp。
软引用何时回收
前面说过,软引用会在内存不足的时候进行回收,但是回收时并不会一次性全部回收,而是会使用一定的回收策略。
下面以最常用的虚拟机HotSpot进行说明。下面是Oracle文档中的说明:
The default value is 1000 ms per megabyte, which means that a soft reference will survive (after the last strong reference to the object has been collected) for 1 second for each megabyte of free space in the heap
默认的生存周期为1000ms/Mb,举个具体的栗子:
假设,堆内存为512Mb,并且可用内存为400Mb,我们创建一个object A,用软引用创建一个引用A的缓存对象cache,以及另一个object B 引用object A。此时,由于B持有A的强引用,所以对象A是强可达并且不会被垃圾回收器回收。

如果B被删除了,那么A仅剩下一个软引用cache引用它,如果A在400s内没有再次被强引用关联,它将会在超时后被删除。

下面是一个控制软引用的栗子:
public class SoftRefTest {
public static class A{
}
public static class B{
private A strongRef;
public void setStrongRef(A ref) {
this.strongRef = ref;
}
}
public static SoftReference<A> cache;
public static void main(String[] args) throws InterruptedException{
//用一个A类实例的软引用初始化cache对象
SoftRefTest.A instanceA = new SoftRefTest.A();
cache = new SoftReference<SoftRefTest.A>(instanceA);
instanceA = null;
// instanceA 现在是软可达状态,并且会在之后的某个时间被垃圾回收器回收
Thread.sleep(10000);
...
SoftRefTest.B instanceB = new SoftRefTest.B();
//由于cache仅持有instanceA的软引用,所以无法保证instanceA仍然存活
instanceA = cache.get();
if (instanceA == null){
instanceA = new SoftRefTest.A();
cache = new SoftReference<SoftRefTest.A>(instanceA);
}
instanceB.setStrongRef(instanceA);
instanceA = null;
// instanceA现在与cache对象存在软引用并且与B对象存在强引用,所以它不会被垃圾回收器回收
...
}
}
但是需要注意的是,被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉,所以在之前一篇中说明里的栗子里,软引用是不会被释放掉的。
所以,你仍然需要手动去清理它们,否则也会导致OOM的产生,这里也举一个小栗子:
public class SoftReferenceTest{
public static class MyBigObject{
int[] data = new int[128];
}
public static int CACHE_INITIAL_CAPACITY = 100_000;
// 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
public static Set<SoftReference<MyBigObject>> cache = new HashSet<>(CACHE_INITIAL_CAPACITY);
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
MyBigObject obj = new MyBigObject();
cache.add(new SoftReference<>(obj));
if (i%10_000 == 0){
System.out.println("size of cache:" + cache.size());
}
}
System.out.println("End");
}
}
使用的虚拟机参数为:
-Xms4m -Xmx4m -Xmn2m
输出如下:
size of cache:1
size of cache:10001
size of cache:20001
size of cache:30001
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
最终抛出了OOM,但这里的原因却并不是Java heap space,而是GC overhead limit exceeded ,之所以会抛出这个错误,是由于虚拟机一直在不断回收软引用,回收进行的速度过快,占用的cpu过大(超过98%),并且每次回收掉的内存过小(小于2%),导致最终抛出了这个错误。
对于这里,合适的处理方式是注册一个引用队列,每次循环之后将引用队列中出现的软引用对象从cache中移除。
public class SoftReferenceTest{
public static int removedSoftRefs = 0;
public static class MyBigObject{
int[] data = new int[128];
}
public static int CACHE_INITIAL_CAPACITY = 100_000;
// 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
public static Set<SoftReference<MyBigObject>> cache = new HashSet<>(CACHE_INITIAL_CAPACITY);
public static ReferenceQueue<MyBigObject> referenceQueue = new ReferenceQueue<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
MyBigObject obj = new MyBigObject();
cache.add(new SoftReference<>(obj, referenceQueue));
clearUselessReferences();
}
System.out.println("End, removed soft references=" + removedSoftRefs);
}
public static void clearUselessReferences() {
Reference<? extends MyBigObject> ref = referenceQueue.poll();
while (ref != null) {
if (cache.remove(ref)) {
removedSoftRefs++;
}
ref = referenceQueue.poll();
}
}
}
使用同样的虚拟机配置,输出如下:
End, removed soft references=97319
HotSpot虚拟机对于软引用的处理
就HotSpot虚拟机而言,常用的回收策略是基于当前堆大小的LRU策略(LRUCurrentHeapPolicy),会使用clock的值减去timestamp,得到的差值,就是这个软引用被闲置的时间,如果闲置足够长时间,就认为是可被回收的。
bool LRUCurrentHeapPolicy::should_clear_reference(oop p,
jlong timestamp_clock) {
jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
assert(interval >= 0, "Sanity check");
if(interval <= _max_interval) {
return false;
}
return true;
}
这里 timestamp_clock 即SoftReference中clock的值,即上次GC时间。java_lang_ref_SoftReference::timestamp(p)可以获取引用中timestamp的值。
那么这个足够长的时间 _max_interval是怎么计算的呢?
void LRUCurrentHeapPolicy::setup() {
_max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
其中SoftRefLRUPolicyMSPerMB默认1000,所以可以看出这个回收时间与上次GC后的剩余空间大小有关,可用空间越大,_max_interval就越大。
如果GC之后,堆的可用空间还很大的话,SoftReference对象可以长时间的在堆中而不被回收。反之,如果GC之后,只剩下很少的内存可用,那么SoftReference对象便会很快进行回收。
SoftReference在一定程度上会影响垃圾回收,如果软可达对象中对应的referent多次垃圾回收仍然不满足释放条件,那么它会停留在堆的老年代,占据很大部分空间,在JVM没有抛出OutOfMemoryError前,它有可能会导致频繁的Full GC,会对性能有一定的影响。
小结
- 软引用的具体回收时间与具体虚拟机有关
- 软引用中会在创建和调用get方法的时候更新内部timestamp,提供给虚拟机回收时进行参考
- hotspot虚拟机对于软引用使用的是LRU策略,回收时会根据软引用被闲置的时间和当前内存综合进行判断
你不可不知的Java引用类型之——SoftReference源码详解的更多相关文章
- 你不可不知的Java引用类型之——WeakReference源码详解
定义 WeakReference是弱引用,该引用不会影响垃圾回收器对对象的回收,不会影响对象的生命周期. 说明 当虚拟机在某个时间点决定要回收一个弱可达(weakly-reachable)对象时,会自 ...
- 你不可不知的Java引用类型之——ReferenceQueue源码详解
定义 ReferenceQueue是引用队列,用于存放待回收的引用对象. 说明 对于软引用.弱引用和虚引用,如果我们希望当一个对象被垃圾回收器回收时能得到通知,进行额外的处理,这时候就需要使用到引用队 ...
- 你不可不知的Java引用类型之——PhantomReference源码详解
定义 PhantomReference是虚引用,该引用不会影响不会影响对象的生命周期,也无法从虚引用中获取对象实例. 说明 源码介绍部分其实也没多大内容,主要内容都在前面介绍中说完了.PhantomR ...
- 数据结构与算法系列2 线性表 链表的分类+使用java实现链表+链表源码详解
数据结构与算法系列2.2 线性表 什么是链表? 链表是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表的链接次序实现的一系列节点组成,节点可以在运行时动态生成,每个节点包括两个 ...
- 你不可不知的Java引用类型之——Reference源码解析
定义 Reference是所有引用类型的父类,定义了引用的公共行为和操作. reference指代引用对象本身,referent指代reference引用的对象,下文介绍会以reference,ref ...
- 数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解
数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解 对数组有不了解的可以先看看我的另一篇文章,那篇文章对数组有很多详细的解析,而本篇文章则着重讲动态数组,另一篇文章链接 ...
- Java集合【6.1】-- Collection接口源码详解
目录 一.Collection接口简介 二.Collection源码分析 三.Collection的子类以及子类的实现 3.1 List extend Collection 3.2 Set exten ...
- Java多线程学习之线程池源码详解
0.使用线程池的必要性 在生产环境中,如果为每个任务分配一个线程,会造成许多问题: 线程生命周期的开销非常高.线程的创建和销毁都要付出代价.比如,线程的创建需要时间,延迟处理请求.如果请求的到达率非常 ...
- Java集合——TreeMap源码详解
)TreeMap 是一个有序的key-value集合,它是通过红黑树实现的.因为红黑树是平衡的二叉搜索树,所以其put(包含update操作).get.remove的时间复杂度都为log(n). (2 ...
随机推荐
- python 调用c语言函数
虽然python是万能的,但是对于某些特殊功能,需要c语言才能完成.这样,就需要用python来调用c的代码了 具体流程: c编写相关函数 ,编译成库 然后在python中加载这些库,指定调用函数. ...
- @vue/cl构建得项目下,postcss.config.js配置,将px转化成rem
依赖包: postcss-pxtorem 配置: 在项目根目录下创建 postcss.config.js 配置如下: module.exports = () => ({ plugins: [ r ...
- Mac eos 环境搭建
最近EOS官网出了文档,说可以跑通一个独立测试的节点了.这周尝试自行搭建了一下,由于自己不完全做mac下的开发,且这玩意是C写的,所以make起来遇到不少坑,记录下来.附完整踩坑.填坑记录. 1. 搭 ...
- Webpack中publicPath设置
webpack中的path是当我们build的时候,输出项目打包文件的位置. webpack中的publicPath是我们打算放到web服务器下的目录,如果我们要放到网站的根目录下,那么就无需设置.如 ...
- 深入理解Java虚拟机阅读心得(三)
Java中提倡的自动内存管理最终可以归结为自动化的解决两个问题: 给对象分配内存 回收分配给对象的内存 先说说回收这一方面的两个主要知识点 一.垃圾收集算法 1.标记-清理算法 首先标记出所有需要回收 ...
- nginx实现动态/静态文件缓存(week4_day1_part2)-技术流ken
nginx实现静态文件缓存实战 1.nginx静态文件缓存 如果要熟练使用nginx来实现文件的缓存,那下面的几个指令你必须要牢记于心 指令1:proxy_cache_path 作用:设置缓存数据的相 ...
- scala程序开发入门
scala程序开发入门,快速步入scala的门槛: 1.Scala的特性: A.纯粹面向对象(没有基本类型,只有对象类型).Scala的安装与JDK相同,只需要解压之后配置环境变量即可:B.Scala ...
- 基于stm32的水质监测系统项目基础部分详细记录
基于stm32的水质监测系统项目基础部分详细记录 软件环境:MDK5 硬件环境:STM32F103ZET6 开发板.颜色传感器.串口屏.串口打印机 搭建工程模板 在进行项目软件的撰写时,首先新建一个基 ...
- MVC Post 提交表单 允许他提交参数包含html标记的解决方法
MVC Post 提交表单的时候,如果参数中包含html标记,则需要在控制器上方加上 [ValidateInput(false)]标记后就可以正常提交表单了例如: [HttpPost] [Valida ...
- python之strip()小记
描述 Python strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列. 注意:该方法只能删除开头或是结尾的字符,不能删除中间部分的字符. 语法 strip()方法语法: ...