要让注解在运行时被jvm正确加载并供反射读取,必须使用@retention(retentionpolicy.runtime);source级注解编译时即丢弃,class级虽写入class文件但jvm不解析,仅runtime级会被jvm加载到元空间并挂载至反射对象,使getannotation()可返回非null实例。

要让注解在运行时被 JVM 正确加载并供反射读取,@Retention(RetentionPolicy.RUNTIME) 是唯一可行的策略。其他两种策略(SOURCE 和 CLASS)在类加载阶段就已“消失”,反射永远拿不到注解实例。
注解在 JVM 中的加载路径
注解不是独立存在的数据,它依附于类、方法或字段的元数据中。JVM 加载类时,会把 class 文件中的常量池、字段表、方法表等结构解析进元空间(Metaspace)。只有 RUNTIME 级注解才会被 JVM 解析并保留在类元数据里,成为 Class、Method、Field 对象的一部分。
- SOURCE:编译器一过就丢,class 文件里根本没有,JVM 压根没见过
- CLASS:写进了 class 文件的 attribute 区(如 RuntimeVisibleAnnotations),但 JVM 类加载器不解析它,元空间里不留痕
- RUNTIME:JVM 显式解析该 attribute,并将注解信息挂载到对应反射对象的内部结构中,getAnnotation() 才有东西可返回
为什么反射调用 getAnnotation() 依赖 RUNTIME
Class.getAnnotation()、Method.getAnnotation() 这些方法底层并不去读 class 文件,而是直接查 JVM 已加载的元数据快照。如果注解没进元空间,它们就像查一个空字段——返回 null,不报错,也不提示。
- 调用 method.getAnnotation(MyAnno.class) 返回 null?不是代码写错了,是注解根本没加载进来
- 即使你用 javap -v 看到 class 文件里有 Annotation 属性,只要不是 RUNTIME,JVM 就当它不存在
- Spring、Jackson、JUnit 等框架全靠这一机制工作,它们启动时扫描的就是元空间里的 RUNTIME 注解
不同策略对 JVM 内存和启动的影响
RUNTIME 注解会占用元空间内存,且随类一起长期驻留;而 SOURCE 和 CLASS 完全不参与运行时生命周期,零开销。
- 大量自定义 RUNTIME 注解 → 元空间占用略增,尤其在类多、注解字段多时
- 若注解只用于生成代码(如 Lombok 的 @Data),用 SOURCE 更轻量,避免无谓内存驻留
- 字节码工具(如 ASM 修改 class)需要注解留在 class 文件里但不进 JVM,选 CLASS 最合适
验证注解是否成功加载进 JVM 的方法
不能只看编译通过或 javap 输出,得在运行时确认:
- 用调试器停在 getAnnotation() 调用后,观察返回值是否为非 null 实例
- 用 jcmd 或 jstat 查看 Metaspace 使用量,加注解前后对比是否有增量
- 用 -XX:+TraceClassLoading 查日志,确认类加载时是否打印了相关注解解析行为(需 JDK 支持)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











