ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CGLIB 类为什么越生越多:先复现缓存键,再谈元空间

CGLIB 类为什么越生越多:先复现缓存键,再谈元空间

“代理类很多”与“元空间泄漏”之间,还隔着缓存键、类加载器和引用存活期。只看某个时刻已经加载的类数,无法判断它们还在增长、已经可卸载,还是确实被长寿命对象保留。

CGLIB 的 AbstractClassGenerator 公开了缓存相关行为,真实排查应跟到部署版本里的代码路径,而不是笼统地说“打开缓存就好了”。源码 同一个父类,也可能因为接口集合、回调类型、命名策略或加载器不同而生成不同形状的代理。

先数稳定的形状,再数类

我会先记录代理创建请求的结构化键,观察重复请求是否真的重复。若每次把 UUID 放进键里,即使缓存功能开启,命中率也不会好。反过来,若忽略了决定类行为的字段,只追求同一个键,可能缓存出行为错误的类。

然后才比较类加载速率、卸载情况、加载器数量和堆引用链。它们是互相补充的证据。某次 GC 是否卸载类,要看实际收集器与配置;不能从“Full GC 发生过”推导“类加载器一定已释放”。

用 Python 展示键空间,明确不冒充 JVM 复现

下面用动态type作为“生成一种形状”的教学替身。重复的规范化键只创建一次,换了加载器命名空间后则故意创建另一份。这能验证缓存键设计,不能测量 Java 元空间。

图对比稳定缓存键与变化的键空间。缓存只是减少重复生成的一种手段;它自己若长时间持有加载器或类,也可能扩大存活对象集合,必须一起检查生命周期。

class ShapeFactory: def __init__(self): self.cache = {} self.generated = 0 def get(self, loader_namespace, base_name, methods): key = (loader_namespace, base_name, tuple(sorted(set(methods)))) if key not in self.cache: self.generated += 1 self.cache[key] = type("Shape" + str(self.generated), (), {"shape_key": key}) return self.cache[key] factory = ShapeFactory() a = factory.get("loader-1", "Order", ["save", "read"]) b = factory.get("loader-1", "Order", ["read", "save", "read"]) assert a is b and factory.generated == 1 c = factory.get("loader-2", "Order", ["read", "save"]) assert c is not a and factory.generated == 2 assert len(factory.cache) == 2 print("same shape reused; loader namespace creates a distinct shape")

封面类加载器图标表示类与加载器的引用需要核对,并不表示每个代理类一定创建一个新加载器;多个生成类可以属于同一个加载器。

例子跑通以后,还要验证什么

这段代码已在本地 macOS、CPython 3.9.6 下运行,实际输出为same shape reused; loader namespace creates a distinct shape。它验证的是例子中的数据约束与分支,不包含线上服务的吞吐或延迟测量。

没有运行 Java、CGLIB、JFR 或元空间压力测试。Python 动态类和 JVM 类卸载机制不同;例子不提供元空间容量、48 万代理类或“十秒恢复”的结论。实际复现需要固定 JDK、框架依赖、加载器层次与代理配置,并记录完整的类加载与卸载证据。

延伸阅读

  • CGLIB AbstractClassGenerator:以实际源码核对缓存路径;master 不是部署版本的替代品。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进