引言

对一个容器,它内部究竟应该放什么。容器可以是内存,java的Collection实现类等等任何东西。
例如
1.LRU容器,要放最近使用过的元素,淘汰一直未被使用的元素。
2.java堆,要放有引用指向的对象。
3.通知栏,还未被看的消息。

通知栏

java引用目录中文件 java怎么引用_java引用目录中文件


我们把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。

java引用目录中文件 java怎么引用_引用_02


类被赋予了引用的功能,刚开始接触一定会晕,这里我看了好多博客和代码才想通这几个引用类究竟要干嘛。

先说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引用目录中文件 java怎么引用_java_03


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());

上述代码的内存分析。

java引用目录中文件 java怎么引用_java引用目录中文件_04


作为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则只剩下一个弱引用,不影响回收。

java引用目录中文件 java怎么引用_java_05

Value究竟要怎么回收?

等等,我们的重点不应该时回收value(大图像)吗?搞得这么复杂,就回收了个key。接下来就需要引用队列了。

java引用目录中文件 java怎么引用_ThreadLocal_06

当key指向的对象回收之后,Entry这个WeakReference会被后台线程放入ReferenceQueue中,我们就可以通过轮询这个队列拿到Entry中的value并把他赋null,一个Entry才算完全回收了。

WeakHashMap是用名为expungeStaleEntries的方法来释放value的。方法名翻译过来就是删除过时的条目

java引用目录中文件 java怎么引用_java_07

思考:我们如果把Entry指向value的引用也改为弱引用,是不是就不需要引用队列来做后续回收了呢?

这样做不可以,如果Entry到value的引用也变成弱引用。而栈中一般只有Key的强引用,此时的value有可能因为只有弱引用而被回收掉了。那就出现了明明有key的强引用却拿不到value情况。
我们想要的效果就是
栈中无key的强引用 ➡ 释放value资源。
java通过上述花式操作达到了此目的。

DirectBuffer利用了PhantomReference。

DirectBuffer分配的是堆外内存,需要我们手动去释放资源。
我们想要的效果就是
栈中无DirectBuffer的强引用 ➡ 释放堆外内存资源。
要实现这个效果 java写的还是挺复杂的,但道理跟上面讲的一样,大家可以看看别人的博客。有时间会展开这块的细节。

总结

java把引用指向的对象释放作为时机,把引用放入引用队列或者直接判断指向对象是否还存在,来释放另一些大资源或者虚拟机不能自动清理的资源(堆外内存)