bigywater.tech
Java · 335

ClassValue:为每个 Class 建立可回收缓存

框架经常要「按 Class 缓存一点东西」。用 HashMap 会拖住类加载器,ClassValue 为什么不会?并发第一次访问会怎样?比 ConcurrentHashMap 快多少?这篇逐项验证,并用真实的类加载器检查泄漏。

8 次
8 个线程同时第一次取同一个类的值时 computeValue 的调用次数(结果只保留一个)
2.24 vs 3.10 ns
ClassValue.get 与 ConcurrentHashMap.get 的耗时
泄漏 vs 回收
static ConcurrentHashMap 泄漏类加载器;ClassValue 不泄漏(值引用 Class 也不)

基本行为与并发

实测 · 本机执行输出
1 ClassValue 基本行为:
  NAME.get(String.class) = info:String;再取一次 = info:String;computeValue 调用次数 = 1
  NAME.get(Integer.class) = info:Integer;子类 ArrayList / List 各自计算:info:ArrayList,info:List;调用次数 = 4
  remove(String.class) 之后再取:info:String;调用次数 = 5
2 8 个线程同时第一次取同一个类的值:所有线程拿到的值 [42];computeValue 实际被调用了 8 次(允许多个线程各算一次,但只有一个结果被保留)
3 computeValue 抛异常:get 把异常原样抛出:IllegalStateException;再取一次会再算一次:仍然抛出
4 computeValue 返回 null:get 返回 null;调用次数 1(null 也被缓存了)

取值耗时

实测 · 本机执行输出
5 按 Class 取缓存值(8 个类轮流,5000 万次取最小):ClassValue.get 2.24 ns;ConcurrentHashMap.get 3.10 ns;HashMap.get 4.74 ns;IdentityHashMap.get 4.46 ns(校验 0)

类加载器泄漏

实测 · 本机执行输出
  none                       → 加载器已被回收
  classvalue                 → 加载器已被回收
  classvalue-holds-class     → 加载器已被回收
  chm                        → 加载器仍然存活(泄漏)
  weak-plain                 → 加载器已被回收
  weak-value-refs-key        → 加载器仍然存活(泄漏)

分步演示

ClassValue
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;耗时是自写循环取 3 次最小值;泄漏检查每种缓存一个新 JVM、最多 8 次 System.gc()
    • 没有验证:ClassValue 在高并发下 computeValue 争用的更多细节、ClassValue 值的内存占用、JDK 27 的行为、与 java.lang.reflect.ClassFile / 框架里 ClassValue 的实际用法、WeakHashMap 值不引用 key 时的回收时机与 ReferenceQueue 的清理延迟
    京ICP备2026067502号-1京公网安备11010502063487号