类加载与反射
约 1138 字大约 4 分钟
布欧-Lewyon
2026-05-16
类加载机制让 JVM 能在运行时动态加载类,反射则让程序可以在运行时检查或调用任意类的方法与字段——这两者构成了 Java 动态能力的基础。
类加载过程
JVM 将 .class 字节码加载到内存并转换为 Class 对象的过程分为三个步骤:
加载(Loading)
通过类的全限定名查找并加载字节码,从文件系统、JAR 包或网络等来源读取二进制流,在堆中生成 Class 对象。
链接(Linking)
- 验证:校验字节码格式合法(魔数、版本号、字节码约束等)。
- 准备:为
static字段分配内存并赋默认零值(如0、null、false)。 - 解析:将常量池中的符号引用替换为直接引用(如类、接口、字段、方法的引用)。
初始化(Initialization)
执行 <clinit>() 方法——即所有 static 字段的赋值语句和 static 代码块。初始化阶段会触发父类的初始化(先父后子)。
触发类初始化的条件
以下情况会触发类初始化(主动引用):
new MyClass(); // new 实例
Class.forName("MyClass"); // 反射
MyClass.STATIC_FIELD; // 访问 static 字段(非编译期常量)
MyClass.staticMethod(); // 调用 static 方法以下情况不会触发初始化(被动引用):
SubClass[] arr = new SubClass[10]; // 数组定义
SubClass.PARENT_CONSTANT; // 引用父类的静态常量
Class<SubClass> clazz = SubClass.class; // 获取 Class 字面量双亲委派模型
┌──────────────────────────────┐
│ Bootstrap ClassLoader │ 加载 rt.jar / java.*
│ (C++ 实现,非 Java) │
└─────────────┬────────────────┘
↓
┌──────────────────────────────┐
│ Extension ClassLoader │ 加载 jre/lib/ext/*
└─────────────┬────────────────┘
↓
┌──────────────────────────────┐
│ Application ClassLoader │ 加载 classpath 上的类
└─────────────┬────────────────┘
↓
┌──────────────────────────────┐
│ 自定义 ClassLoader │ 用户自定义加载逻辑
└──────────────────────────────┘工作机制:加载一个类时,先委托父加载器尝试加载,父加载器能加载则直接返回,不能加载才由子加载器自己尝试。
优点:保证核心类库(java.lang.String)始终由 Bootstrap ClassLoader 加载,避免用户自定义的同名类替换核心类(沙箱安全)。
打破双亲委派
常见场景:Tomcat 为每个 Web 应用加载各自的类(可加载不同版本的类库);SPI(ServiceLoader)由 Bootstrap ClassLoader 启动但需委托线程上下文类加载器加载第三方实现。
反射入门
反射(Reflection)让程序在运行时获取类的信息并操作其成员。核心 API 在 java.lang.reflect 包。
获取 Class 对象
// 方式一:类字面量(最推荐,编译时检查)
Class<String> clazz1 = String.class;
// 方式二:实例的 getClass()
String s = "hello";
Class<?> clazz2 = s.getClass();
// 方式三:Class.forName(最灵活,需要全限定名)
Class<?> clazz3 = Class.forName("java.lang.String");操作字段、方法、构造器
// 获取字段的值
Field field = obj.getClass().getDeclaredField("name");
field.setAccessible(true); // 绕过 private 检查
String name = (String) field.get(obj);
// 调用方法
Method method = obj.getClass().getMethod("sayHello", String.class);
Object result = method.invoke(obj, "World");
// 调用构造器
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object instance = constructor.newInstance("test");反射的优缺点
优点:框架实现的基石(Spring IoC 容器通过反射实例化 Bean、依赖注入;JUnit 通过反射查找 @Test 方法)。
缺点与注意:
- 性能开销:反射比直接调用慢 1-2 个数量级,高频路径应缓存
Method/Field对象。 - 安全性:
setAccessible(true)绕过了封装,可能破坏类的内部不变量。 - 编译期检查丧失:方法名、参数类型的拼写错误在运行时才暴露。
何时用反射
- 框架 / 库代码中需要根据配置动态创建类(如 Spring、MyBatis)。
- 开发工具(IDE 的代码补全、调试器)。
- 业务代码中尽量避免——能用接口、泛型、Lambda 表达的就不要用反射。
小结
- 类加载分三步:加载(读字节码)→ 链接(验证/准备/解析)→ 初始化(执行 static 块)。
- 双亲委派模型保证核心类库不被篡改,自定义 ClassLoader 可打破该模型。
- 反射通过
Class对象获取类的字段、方法、构造器,setAccessible(true)可访问private成员。 - 反射是框架的基石,但业务代码应谨慎使用(性能开销、类型安全缺失)。
- 易错:
getMethod()只获取public方法(含继承的),getDeclaredMethod()获取当前类所有声明的方法(不含继承的)。存取字段同理。 - 思考任务:用反射实现一个简单的"字符串转对象"工具:读取
key=value格式的配置,通过 setter 方法反射调用设置到对象的同名属性。
上一节:内存与 GC
下一节:JVM 诊断与性能
