类加载机制

返回 Java 基础
→ 相关:JVM(内存结构)· 反射(Class 对象)

类加载过程

加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
      ├────────────────────┤
              链接
阶段做什么
加载读取 .class 字节码,生成 Class 对象
验证校验字节码格式、语义合法性
准备为静态变量分配内存,赋默认值(int → 0,引用 → null)
解析将符号引用替换为直接引用(内存地址)
初始化执行静态代码块 <clinit>,赋实际值

准备阶段 static int x = 10 → x = 0
初始化阶段 → x = 10


触发类初始化的时机(主动引用)

  1. new 一个类的实例
  2. 访问类的静态字段或静态方法
  3. 反射调用(Class.forName()
  4. 子类初始化时,父类先初始化
  5. JVM 启动时的主类(含 main 方法的类)

被动引用不触发初始化

  • 通过子类引用父类的静态字段(只初始化父类)
  • 数组类型引用(MyClass[] arr = new MyClass[10]
  • 引用常量(static final 编译期常量,已内联)

类加载器

Bootstrap ClassLoader(启动类加载器)
 └─ Extension ClassLoader(扩展类加载器)
     └─ Application ClassLoader(应用类加载器)
         └─ 自定义 ClassLoader
类加载器负责加载
BootstrapJAVA_HOME/lib(rt.jar,String、Object 等核心类)
ExtensionJAVA_HOME/lib/ext
Applicationclasspath(自己写的代码、第三方 jar)

Bootstrap 是 C++ 实现的,在 Java 中获取到的是 null


双亲委派模型

流程:子加载器收到加载请求 → 先委托父加载器加载 → 父加载器找不到时,子加载器才自己加载。

App ClassLoader 收到加载 "com.example.Foo" 请求
  → 委托 Extension ClassLoader
    → 委托 Bootstrap ClassLoader
      → 找不到
    ← Extension ClassLoader 找不到
  ← App ClassLoader 自己加载

好处

  • 避免类重复加载
  • 保护核心类不被替换(你写一个 java.lang.String,Bootstrap 会优先加载 JDK 的)

打破双亲委派的场景

  • SPI 机制(如 JDBC):Bootstrap 加载接口,但实现类在 classpath,需要用线程上下文类加载器反向委托
  • 热部署(Tomcat):每个 webapp 有独立的 ClassLoader,隔离不同应用的类
  • OSGi:模块化,类加载关系是网状而非树状

初始化顺序

public class Parent {
    static { System.out.println("Parent 静态块"); }
    { System.out.println("Parent 实例块"); }
    public Parent() { System.out.println("Parent 构造器"); }
}
 
public class Child extends Parent {
    static { System.out.println("Child 静态块"); }
    { System.out.println("Child 实例块"); }
    public Child() { System.out.println("Child 构造器"); }
}

执行 new Child() 的输出:

Parent 静态块
Child 静态块
Parent 实例块
Parent 构造器
Child 实例块
Child 构造器

规律:静态 → 实例,父类 → 子类,静态块只执行一次。


自定义类加载器

继承 ClassLoader,重写 findClass() 方法:

public class MyClassLoader extends ClassLoader {
    private String classPath;
 
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = loadClassBytes(name);  // 从自定义位置读取字节码
        return defineClass(name, bytes, 0, bytes.length);
    }
}

重写 findClass 而不是 loadClass,可以保留双亲委派逻辑。
打破双亲委派需要重写 loadClass


相关链接