引言
对一个容器,它内部究竟应该放什么。容器可以是内存,java的Collection实现类等等任何东西。
例如
1.LRU容器,要放最近使用过的元素,淘汰一直未被使用的元素。
2.java堆,要放有引用指向的对象。
3.通知栏,还未被看的消息。
通知栏
我们把java堆比作一个容器,而java堆不仅想保存有引用的对象,它想引入新的策略,例如某些对象堆满时可以移除;某些对象只要有垃圾回收动作就可以移除等。
换作是我们,我们应该如何做。
方法一,引入关键字。
以下是一个最普通的强引用。
Obejct o = new Object();
等价于
strong Object o = new Object();`
是否可以引入soft和weak等关键字来达到预期?即被soft修饰的引用是软引用,如果一个对象只被soft修饰的引用所指向,堆满时可以自动移除这些对象。weak修饰的引用时弱引用,如果一个对象只被weak修饰的引用所指向,那当虚拟机进行垃圾回收时,就会自动回收这些对象。
soft Object o = new Object();
weak Object o2 = new Object();
这样改动对于使用者来收非常容易理解。不过java并没有采用这一方案。
方法2
让一个类来作为引用,该类内部存有指向的对象。
java自己引入了包含SoftReference,WeakReference,PhantomReference在内的java.lang.ref包。即,
SoftReference sr = new SoftReference(new Object()); //等效于soft Object o = new Object();
WeakReference wr = new WeakReference(new Object()); //等效于weak Object o = new Object();
这种方案对语言本身体系改动较小,不用引入关键字,只需引入一个java包就OK。
但理解起来不太容易。为什么这么说?
注意上述方案2,我们的对象是new Object(),引用是new SoftReference()和
new WeakReference(),不是sr 和wr,sr和wr是强引用,指向两个含有引用功能的类Reference。
类被赋予了引用的功能,刚开始接触一定会晕,这里我看了好多博客和代码才想通这几个引用类究竟要干嘛。
先说SoftReference
即堆满时会释放只被软引用指向的对象。
ThreadLocal利用了SoftReference。
后面内容默认你知道ThreadLocal干啥用的。
下述代码, ReferenceTest类有一个ThreadLocal类型的var1变量,他在main线程赋值为"t1",
在t2线程赋值为"t2",并且在各自线程取到的都是本线程的值。
public class ReferenceTest {
//线程独享变量
ThreadLocal<String> var1 = new ThreadLocal();
public void setVar1(String value){
var1.set(value);
}
public static void main(String[] args){
ReferenceTest t = new ReferenceTest();
//main 线程赋值t1
t.setVar1("t1");
//t2线程赋值t2
Thread t2 = new Thread(
()->{t.setVar1("t2");}
);
t2.start();
}
}
java是在每个线程内部都维护了一个ThreadLocalMap,当一个线程去拿var1的值时(var1.get())
会去当前线程的Map里取值。如上图时刻1是在main线程的时候,var1指向的ThreadLocal指向了main的LocalMap。
而在时刻2,var1指向的ThreadLocal指向了t2线程的LocalMap。这是ThreadLocal的原理。
我们要强调的是Map中的Entry,它继承自SoftReference。而且指向了作为key的ThreadLocal变量,就是我们最上面说的类作为引用的情况。这样一个ThreadLocal对象有两种引用,一个是栈中的var1(强引用),一个是SoftReference。
当上述代码走完,栈种的var1变量就不再存在了,这时各线程中的Map的key就只剩一个Entry
的软引用,空间太满的话就可以回收掉。反之,若这里是正常的HashMap,ThreadLocal这个对象就一直有一个Entry的强引用,除非GCRoot走不到这个Entry,否则这个ThreadLocal不会回收。
key回收之后,当Map需要扩容时才会清除旧的value。
先判断容量足够与否,不足就清除旧值,还不足就扩容。
private void set(ThreadLocal<?> key, Object value) {
***省略大部分代码
**清除了部分旧的value仍然容量不足,进行rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void rehash() {
****对map进行大清除
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
**移除旧的实体
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
***移除条件是看key还存在与否。
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
WeakHashMap利用了WeakReference。
假设我们要构建一个缓存,将大图像对象保存为值,将图像名称保存为键。 我们想选择一个合适的地图实现来解决这个问题。
使用简单的HashMap不是一个好选择,因为值对象可能占用大量内存。 更重要的是,它们永远不会通过GC流程从缓存中回收,即使它们不再在我们的应用程序中使用。
理想情况下,我们需要一个允许GC自动删除未使用对象的Map实现。 当我们的应用程序中的任何地方没有使用大图像对象的键时,该条目将从内存中删除。
幸运的是,WeakHashMap具有这些特征。
WeakHashMap<UniqueImageName, BigImage> map = new WeakHashMap<>();
BigImage bigImage = new BigImage("image_id");
UniqueImageName imageName = new UniqueImageName("name_of_big_image");
map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));
imageName = null;
System.gc();//gc命令只是建议gc,并不会瞬时触发gc
Thread.currentThread().sleep(1000*10);
assertTrue(map.isEmpty());
上述代码的内存分析。
作为key的UniqueImageName对象有栈中的一个强引用,有Entry的一个弱引用。当imageName这个引用出栈之后,
仅剩的Entry弱引用无法阻止key的回收。当gc触发时,UniqueImageName(“name_of_big_image”) 这个key对象就被回收了。
同时这个Entry会被放入引用队列,我们可以通过这个队列拿到value,释放BigImage(“image_id”)资源。
补充:
引用队列,垃圾回收器在回收了弱引用指向的对象后,会把弱引用加入到引用队列中,我们可以另启一个线程来轮询引用队列,
这里就相当于拿到了Entry,就可以将其中的value对象赋null来释放资源了。
HashMap和WeakHashMap对比
如下图所示。
我们先看如果是普通的HashMap,即使我们不使用这个图像资源了(即栈中的imageName已经出栈),由于有map这个gcRoot指向堆中的HashMap对象,HashMap再指向Entry实体,Entry实体指向key这个强引用链还存在,Entry的key是释放不掉的。
WeakHashMap的Key则只剩下一个弱引用,不影响回收。
Value究竟要怎么回收?
等等,我们的重点不应该时回收value(大图像)吗?搞得这么复杂,就回收了个key。接下来就需要引用队列了。
当key指向的对象回收之后,Entry这个WeakReference会被后台线程放入ReferenceQueue中,我们就可以通过轮询这个队列拿到Entry中的value并把他赋null,一个Entry才算完全回收了。
WeakHashMap是用名为expungeStaleEntries的方法来释放value的。方法名翻译过来就是删除过时的条目
思考:我们如果把Entry指向value的引用也改为弱引用,是不是就不需要引用队列来做后续回收了呢?
这样做不可以,如果Entry到value的引用也变成弱引用。而栈中一般只有Key的强引用,此时的value有可能因为只有弱引用而被回收掉了。那就出现了明明有key的强引用却拿不到value情况。
我们想要的效果就是
栈中无key的强引用 ➡ 释放value资源。
java通过上述花式操作达到了此目的。
DirectBuffer利用了PhantomReference。
DirectBuffer分配的是堆外内存,需要我们手动去释放资源。
我们想要的效果就是
栈中无DirectBuffer的强引用 ➡ 释放堆外内存资源。
要实现这个效果 java写的还是挺复杂的,但道理跟上面讲的一样,大家可以看看别人的博客。有时间会展开这块的细节。
总结
java把引用指向的对象释放作为时机,把引用放入引用队列或者直接判断指向对象是否还存在,来释放另一些大资源或者虚拟机不能自动清理的资源(堆外内存)