跟小白领悟JDK动态代理——核心类库里的缓存优化WeakCache(中)

14 阅读8分钟

WeakCache类的基本信息

类定义

// final➕default权限,符合最小权限原则(迪米特法则),仅为JDK代理定制化设计
final class WeakCache<K, P, V> {
    // 如果类加载器被回收了,会通知到这个引用队列,回头就可以清除缓存
    private final ReferenceQueue<K> refQueue = new ReferenceQueue<>();
    // 总的缓存,下面原生注解的意思是,ConcurrentMap不支持null,
    // 但他通过定义了一个叫null的Object对象来解决这个问题了
    // 那为什么要解决这个问题呢?因为最顶层父加载器Bootstrap ClassLoader是c++实现的,
    // 如果类加载器是null了,那他不是真的null,而是代表了Bootstrap ClassLoader。
    // the key type is Object for supporting null key
    private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map
        = new ConcurrentHashMap<>();
    // 反转缓存,便于快速查找是否这个值存在于二级缓存
    private final ConcurrentMap<Supplier<V>, Boolean> reverseMap
        = new ConcurrentHashMap<>();
    // 如果计算第二级缓存的key
    private final BiFunction<K, P, ?> subKeyFactory;
    // 如何产生代理类对象
    private final BiFunction<K, P, V> valueFactory;

    /**
     * Construct an instance of {@code WeakCache}
     *
     * @param subKeyFactory a function mapping a pair of
     *                      {@code (key, parameter) -> sub-key}
     * @param valueFactory  a function mapping a pair of
     *                      {@code (key, parameter) -> value}
     * @throws NullPointerException if {@code subKeyFactory} or
     *                              {@code valueFactory} is null.
     */
     // 该构造函数在Proxy的weakCache属性定义处使用
    public WeakCache(BiFunction<K, P, ?> subKeyFactory,
                     BiFunction<K, P, V> valueFactory) {
        this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
        this.valueFactory = Objects.requireNonNull(valueFactory);
    }
    ...
}

泛型参数说明

<K, P, V> 对应<ClassLoader,Class[],Class>,注意,进入正题前,先公布缓存内容的答案,弱引用对ClassLoader的封装对象是第一级key,弱引用对Class[]的封装是第二级key,Factory类对象(WeakCache类的私有内部类)是临时缓存值,CacheValue类对象(WeakCache类的私有静态内部类,弱引用且封装了生产的动态代理类)是最终缓存值。 其中K为类加载器不必多说,这个类加载器是加载目标对象类的类加载器,生成的代理类也会由该类加载器进行加载,原因很简单,类加载器实现了类之间的可见性,因此,需要使用同一层面的类加载器(由于双亲委派模型,子类加载器可见父类加载器,但反之不可见)。 P是一个类对象集合,其实稍作解释便可理解,这个类对象其实是目标对象实现的所有接口的类对象数组,相应的动态生成的代理类也会实现这些接口。 V是Class对象,即最终生产的代理对象类。 用来生产代理类对象的方法被封装在存在于java.lang.reflect.Proxy类中的ProxyClassFactory类中,没错,就是动态生成的代理类统一的父类Proxy类的内部类。

private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>>

BiFunction是一个常见的函数式接口,提供ClassLoader和Class[](这两个上面已经分析类),返回Class,这个类对象就是动态生成的代理类对象,所以其实生成代理类的核心逻辑其实就被封装在了ProxyClassFactory实现了BiFunction接口的apply方法里。 本文暂时不详细学习和理解ProxyClassFactory对BiFunction接口的apply方法的实现逻辑,重点在WeakCache缓存的实现,来看get方法:

get方法分析

public V get(K key, P parameter) {
    // K是类加载器,P是接口类对象数组,jdk代理是通过实现目标类实现的接口来实现的
    // 所以这里要求接口类对象数组不能为空不过分吧。
    Objects.requireNonNull(parameter);
    
    // 清除缓存,现在只知道字面意思是这个,下面会具体分析源码(很简单)
    expungeStaleEntries();

    // 获取缓存的一级key,是通过类加载器和引用队列来计算
    // 大白话就是返回一个弱引用(CacheKey是弱引用类的子类)封装的类加载器对象
    // 这里会有一个疑问,因为CacheKey.valueOf方法会返回一个新对象,
    // 这会导致,即使类加载器都是一个,但是返回的key都是新的弱引用封装对象,
    // 那岂不是永远中不了缓存?答案就在CacheKey类重写了equal和hashCode。
    // 最后把这个弱引用添加到私有变量refQueue引用队列的订阅里
    // 以便回收的时候(上一行代码)可以从refQueue中得到要清除哪些key
    Object cacheKey = CacheKey.valueOf(key, refQueue);
    
    // 下面一行是原生注解,“懒加载(延迟加载)为特定的cacheKey安装二级valuesMap”
    // lazily install the 2nd level valuesMap for the particular cacheKey
    // 什么意思呢?字面理解valuesMap就是二级缓存Map
    // 从上面可以推断出map(ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>>)
    // 这个私有变量就是我们的总缓存,里面也肉眼可见的套了两层
    // 那么valuesMap就是ConcurrentMap<Object, Supplier<V>>>了吧
    ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
    
    // 这里分析整个if代码块,如果第二层缓存是空
    if (valuesMap == null) {
        // 就开始创建。
        ConcurrentMap<Object, Supplier<V>> oldValuesMap
            = map.putIfAbsent(cacheKey,
                              valuesMap = new ConcurrentHashMap<>());

        // 这里用在此判断旧的第二层缓存是否为空,
        // 其实用到了乐观锁,这个代码块并没有做并发控制,
        // 如果走到这发现oldValuesMap不为null,说明前一个执行到判断上一行代码的线程已经提前
        // 给创建好了第二层缓存,这里用就是了,因为你不知道创建oldValuesMap的线程运行到哪了,
        // 可能干了很多事。这个写法还是很值得学习的。
        if (oldValuesMap != null) {
            valuesMap = oldValuesMap;
        }
    }

    // 下面是原生注解“创建subKey并从valuesMap中检索可能存在的由该subKey存储的Supplier<V>”
    // create subKey and retrieve the possible Supplier<V> stored by that subKey from valuesMap
    // 也就是说第二级缓存的key是通过弱引用封装的类加载器和接口类数组共同计算的,
    // 从Proxy类对WeakCache的构造函数赋值可以看出,这个subKeyFactory是一个KeyFactory私有静态内部类,
    // 和ProxyClassFactory类一样,同样实现了BiFunction接口,apply接口实现了生产第二级缓存key的逻辑
    // 实际上,计算的过程仅与接口类数组的长度有关,和类加载器无关,
    // 返回的第二级缓存key是同样是一个弱引用,弱引用的封装值是接口类数组。
    // 事情突然有意思起来了,实际上二级缓存,最外层的缓存key是封装了类加载器的弱引用
    // 第二级缓存的key是封装了接口类对象数组的弱引用,同样重写了equal和hashCode。
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));

    // 最终获取到了Factory实例或者CacheValue实例
    Supplier<V> supplier = valuesMap.get(subKey);
    Factory factory = null;
    // 下面循环还是乐观锁的并发控制
    while (true) {
        // 至少二番战才会到这
        // 第一次是null;第二次是Factory对象;第三次是CacheValue对象
        if (supplier != null) {
            // supplier might be a Factory or a CacheValue<V> instance
            // 如果是Factory的实例,则get方法内调用了
            //(委托)ProxyClassFactory的apply方法来生产代理类对象,同时创建CacheValue类封装该对象,
            // 并返回;如果是CacheValue对象,则直接返回封装的代理类对象。
            V value = supplier.get();
            if (value != null) {
                return value;
            }
        }
        
        // 下面原生注解讲了能走到这的所有条件
        // else no supplier in cache
        // or a supplier that returned null (could be a cleared CacheValue
        // or a Factory that wasn't successful in installing the CacheValue)

        // lazily construct a Factory
        if (factory == null) {
            factory = new Factory(key, parameter, subKey, valuesMap);
        }
        
        // 情况1:supplier为null,说明是第一次获取
        if (supplier == null) {
            supplier = valuesMap.putIfAbsent(subKey, factory);
            if (supplier == null) {
                // successfully installed Factory
                supplier = factory;
            }
            // 如果putIfAbsent返回非null,说明其他线程已经放入了一个supplier
            // 继续循环使用那个supplier
            // else retry with winning supplier
        } 
        // 情况2:supplier不为null,但可能已经失效
        else {
            // 这里campare and swap
            if (valuesMap.replace(subKey, supplier, factory)) {
                // successfully replaced
                // cleared CacheEntry / unsuccessful Factory
                // with our Factory
                supplier = factory;
            } else {
                // retry with current supplier
                // 替换失败,说明其他线程已经更新了supplier
                // 获取最新的supplier
                supplier = valuesMap.get(subKey);
            }
        }
    }
}

expungeStaleEntries方法分析

值得注意的是expungeStaleEntries方法,其实就是在get前先看一下订阅了回收通知的引用队列里有没有值,有的话就去清除相应的缓存。

private void expungeStaleEntries() {
    CacheKey<K> cacheKey;
    while ((cacheKey = (CacheKey<K>)refQueue.poll()) != null) {
        cacheKey.expungeFrom(map, reverseMap);
    }
}

总结:

动态代理类的生成代价较高 每次调用 Proxy.newProxyInstance() 都会触发类的动态生成、定义和初始化。这个过程涉及字节码生成、类加载等操作,相对耗时。通过缓存已生成的代理类,可以显著提升性能。 相同接口和类加载器下代理类是可复用的 如果多次请求相同的接口组合和类加载器,生成的代理类结构是一样的,没有必要重复创建。缓存使得这些代理类可以被安全地复用。 防止内存泄漏与过度占用内存 使用 WeakCache 这种弱引用机制,可以确保即使类加载器不再使用,也不会导致内存泄漏。同时又能保持高频使用的代理类在缓存中存在。 支持并发访问 缓存机制内部使用了并发控制(如 ConcurrentMap 和 乐观锁的写法),保证多线程环境下高效、安全地获取代理类。

个人体会:

其实key的回收是很苛刻的,因为让类加载器做了弱引用的封装,类加载器如果被回收,说明加载的类对象都被回收了,因为每个类对象都持有对类加载器的强引用,进而类对象都被回收了,说明类的实例对象也被回收了,这里必有目标对象,从而目标对象都被回收了,说明代理对象也没有了意义。