JVM 类加载机制

这里写目录标题

一、类加载的概念

类加载 是指JVM将类的字节码文件(.class)从磁盘加载到内存 ,并进行验证、准备、解析和初始化,最终形成可以被JVM直接使用的Java类型的过程。

1、核心特征

类加载的三个核心特性:

  1. 延迟加载(Lazy Loading)
  2. 类只在第一次使用时才加载
  3. 不是程序启动时就加载所有类
  4. 缓存机制(Caching)
  5. 已加载的类会缓存,避免重复加载
  6. 缓存Key: 全限定类名 + ClassLoader实例

3. 唯一性(Uniqueness)

  • 同一个类在同一个ClassLoader下只加载一次
  • 不同ClassLoader加载的类视为不同类

2、为什么需要类加载

类加载器核心信息总结表

类加载器 作用范围 加载路径
启动类加载器(Bootstrap) 加载 JVM 核心类(比如java.lang.String JAVA_HOME/jre/lib/rt.jar 等核心 jar
扩展类加载器(Extension) 加载 JVM 扩展类 JAVA_HOME/jre/lib/ext 目录
应用程序类加载器(App) 加载用户编写的类和第三方 jar 应用 classpath 路径

要不要我帮你补充一份类加载器工作流程示意图,直观展示双亲委派模型的执行顺序?

当前文件内容过长,豆包只阅读了前 33%。

  1. 平台无关性:
  • 同一份字节码在不同平台运行
  • .class文件是平台中立的
  1. 安全性:
  • 防止恶意字节码破坏JVM
  • 需要验证字节码的合法性
  1. 动态性:
  • 支持动态加载、热替换
  • 如OSGi、热部署
  1. 内存管理:
  • 合理使用内存,按需加载
  • 不是一次性加载所有类
  1. 优化:
  • 字节码优化、即时编译
  • 提高执行效率

二、类加载的时机

JVM采用延迟加载(Lazy Loading) 策略。这意味着类不会在程序启动时就全部加载到内存中,而是在第一次真正需要使用的时候才加载

java 复制代码
public class LazyLoadingDemo {
    public static void main(String[] args) {
        System.out.println("程序开始运行");

        // 此时User类还没有被加载
        System.out.println("准备使用User类...");

        // 只有当第一次使用时,User类才会被加载
        User user = new User();
        user.sayHello();

        System.out.println("程序结束");
    }
}

class User {
    static {
        System.out.println("User类被加载并初始化了!");
    }

    public void sayHello() {
        System.out.println("Hello from User!");
    }
}

运行上面的代码,你会看到:

plain 复制代码
程序开始运行
准备使用User类...
User类被加载并初始化了!
Hello from User!
程序结束

1、触发类加载的六大场景

虽然JVM采用延迟加载策略,但在某些特定情况下,类会被触发加载。以下是六种会触发类加载的场景

1、创建类的实例

java 复制代码
// 当使用new关键字时,类会被加载
MyClass obj = new MyClass();

// 反射创建实例也会触发加载
Class<?> clazz = Class.forName("com.example.MyClass");
Object obj = clazz.newInstance();

2、访问类的静态成员

java 复制代码
class MyClass {
    static int staticField = 10;  // 非final静态字段
    static final int CONSTANT = 20;  // final静态常量
    
    static {
        System.out.println("MyClass静态代码块执行");
    }
}

public class Main {
    public static void main(String[] args) {
        // 情况A:访问非final静态字段(会触发加载)
        System.out.println(MyClass.staticField);  
        // 输出:MyClass静态代码块执行\n10
        
        // 情况B:访问final静态常量(不会触发加载!)
        System.out.println(MyClass.CONSTANT);  
        // 只输出:20,不执行静态代码块
    }
}

重要区别

  • 访问非final静态字段:会触发类加载和初始化
  • 访问final静态常量:不会触发类加载(编译器直接内联替换)

3、调用类的静态方法

java 复制代码
class Utility {
    static {
        System.out.println("Utility类被加载");
    }
    
    public static void helper() {
        System.out.println("调用helper方法");
    }
}

// 调用静态方法会触发类加载
Utility.helper();  // 输出:Utility类被加载\n调用helper方法

4、反射调用(Class.forName)

java 复制代码
// 使用Class.forName显式加载类
Class<?> clazz = Class.forName("com.example.MyClass");

// 使用ClassLoader.loadClass也会加载(但不一定初始化)
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> clazz2 = loader.loadClass("com.example.MyClass");

**注意Class.forName()默认会执行初始化,而ClassLoader.loadClass()只加载不初始化。

5、初始化子类(父类先被加载)

java 复制代码
class Parent {
    static {
        System.out.println("Parent类被加载");
    }
}

class Child extends Parent {
    static {
        System.out.println("Child类被加载");
    }
}

// 初始化子类时,父类会先被加载
Child child = new Child();
// 输出:Parent类被加载\nChild类被加载

VM会保证:父类的初始化一定在子类之前完成

6、JVM启动时的主类

java 复制代码
# 当你运行java命令时,指定的主类会被加载
java com.example.MainApp
# MainApp类会在程序启动时立即加载

2、不会触发类加载的特殊情况

了解什么情况会触发类加载很重要,但了解什么情况不会触发同样重要!

引用类的常量(final static)

java 复制代码
class Constants {
    // 编译时常量(基本类型或String的字面量)
    public static final int MAX_SIZE = 1024;
    public static final String APP_NAME = "MyApp";
    
    // 运行时常量(需要计算)
    public static final long CURRENT_TIME = System.currentTimeMillis();
    
    static {
        System.out.println("Constants类被加载");
    }
}

public class Main {
    public static void main(String[] args) {
        // 不会触发加载(编译器直接替换为字面量)
        System.out.println(Constants.MAX_SIZE);  // 直接输出1024
        System.out.println(Constants.APP_NAME);  // 直接输出"MyApp"
        
        // 会触发加载(需要运行时计算)
        System.out.println(Constants.CURRENT_TIME);  // 输出:Constants类被加载\n时间戳
    }
}

通过数组定义引用类

java 复制代码
class MyClass {
    static {
        System.out.println("MyClass被加载");
    }
}

// 创建数组不会触发元素类的加载
MyClass[] array = new MyClass[10];  // 不输出任何东西

// 只有当真正创建数组元素时才会加载
array[0] = new MyClass();  // 输出:MyClass被加载

类名.class语法

java 复制代码
// 获取Class对象不会触发初始化(但会触发加载)
Class<?> clazz = MyClass.class;  // 只加载,不初始化

// 如果要验证是否已加载,可以使用-XX:+TraceClassLoading

3、常见误区

误区1:"程序启动时会加载所有需要的类"

真相:JVM按需加载,只有用到的类才会被加载。你可以通过监控验证:

java 复制代码
# 添加这些JVM参数观察类加载
-XX:+TraceClassLoading
-XX:+PrintGCDetails

误区2:"import语句会触发类加载"

真相:import只是编译时的声明,不会导致运行时加载。

java 复制代码
import java.util.ArrayList;  // 这不会导致ArrayList被加载

public class Main {
    public static void main(String[] args) {
        // 只有这里使用时才会加载
        ArrayList<String> list = new ArrayList<>();
    }
}

误区3:"子类被加载时,父类的方法也会被加载"

真相:父类的方法只有在被调用时才会解析和验证,不是加载子类时就全部加载。

4、总结

理解类加载时机对于编写高效、可维护的Java程序至关重要。记住这些关键点:

  1. 延迟加载是默认策略:类在第一次真正使用时才加载
  2. 六种触发场景:new、访问静态成员、调用静态方法、反射、子类初始化、主类
  3. final常量是特例:不会触发类加载(如果是编译时常量)
  4. 数组和import不触发:它们不会导致类被加载
  5. 父子类加载顺序:父类先于子类加载和初始化

通过合理利用类加载机制,我们可以:

  • 优化启动性能:减少不必要的类加载
  • 实现热部署:动态重新加载修改的类
  • 构建插件系统:独立加载和卸载功能模块
  • 节省内存:及时卸载不再需要的类

类加载机制是JVM优雅设计的一部分,理解它不仅能帮助你写出更好的代码,还能在遇到相关问题时快速定位和解决。

三、类加载的过程

1、类为什么要"加载"

在理解类加载过程之前,我们需要明白一个核心问题:为什么需要类加载?

java 复制代码
// 我们写的Java代码
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

// 编译后的字节码(部分)
cafe babe 0000 0034 0010 0a00 0300 0d07
000e 0700 0f01 0006 3c69 6e69 743e 0100
...

字节码不能直接执行,它需要经过JVM的"翻译"和"准备"。类加载就是这个翻译和准备的过程,主要解决四个问题:

  1. 平台无关性:同一份.class文件在不同平台运行
  2. 安全性:防止恶意字节码破坏JVM
  3. 内存管理:按需加载,合理使用内存
  4. 动态性:支持热部署、动态代理等高级特性

2、类加载的生命周期

类加载的生命周期如下:

前5个阶段(加载、验证、准备、解析、初始化)构成了类加载的核心过程,后2个阶段是类的使用和回收。

1、加载

加载是类加载过程的第一步,主要完成三件事:

1 加载的三大任务

加载的三大任务:

  • 任务1:获取类的二进制字节流
  • 任务2:将字节流转换为方法区的运行时数据结构
  • 任务3:在堆中创建Class对象,作为访问入口
java 复制代码
public class LoadingPhase {
    
    public Class<?> loadClass(String className) throws ClassNotFoundException {
        // 任务1:获取类的二进制字节流
        byte[] bytecode = getClassByteStream(className);
        
        // 任务2:将字节流转换为方法区的运行时数据结构
        ClassMetadata metadata = convertToRuntimeStructure(bytecode);
        
        // 任务3:在堆中创建Class对象,作为访问入口
        Class<?> classObj = createClassObject(metadata);
        
        return classObj;
    }
    
    // 字节流可以从多种来源获取
    private byte[] getClassByteStream(String className) {
        Sources sources = {
            1. 本地文件系统(最常见)
            2. ZIP/JAR/WAR包
            3. 网络下载(Applet)
            4. 运行时计算生成(动态代理)
            5. 数据库或加密文件
            6. 其他文件(JSP生成的类)
        };
        
        // 根据来源获取字节流
        return fetchBytesFromSource(className, sourceType);
    }
}
2 加载阶段的独特之处

加载阶段有一个重要特点:它是唯一一个可以由开发人员干预的阶段。通过自定义类加载器,我们可以控制从哪里获取字节流。

java 复制代码
// 自定义类加载器示例
public class CustomClassLoader extends ClassLoader {
    private String classPath;
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 从自定义位置读取字节码
            byte[] data = loadClassData(name);
            // 调用defineClass完成加载
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    }
    
    private byte[] loadClassData(String className) throws IOException {
        // 自定义加载逻辑...
        String path = className.replace('.', '/') + ".class";
        FileInputStream fis = new FileInputStream(classPath + "/" + path);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            baos.write(buffer, 0, bytesRead);
        }
        
        fis.close();
        return baos.toByteArray();
    }
}

2、验证

验证是安全防线,确保加载的字节码不会危害JVM。它包含四个层次:

文件格式验证
java 复制代码
class FileFormatVerifier {
    
    void verify(byte[] bytecode) throws ClassFormatError {
        // 检查1:魔数(Magic Number)
        // .class文件前4个字节必须是0xCAFEBABE
        if (bytecode[0] != (byte)0xCA || bytecode[1] != (byte)0xFE ||
            bytecode[2] != (byte)0xBA || bytecode[3] != (byte)0xBE) {
            throw new ClassFormatError("Invalid magic number");
        }
        
        // 检查2:版本号
        int minorVersion = ((bytecode[4] & 0xFF) << 8) | (bytecode[5] & 0xFF);
        int majorVersion = ((bytecode[6] & 0xFF) << 8) | (bytecode[7] & 0xFF);
        
        if (majorVersion > 61) {  // Java 17对应的主版本号
            throw new UnsupportedClassVersionError(
                "Unsupported version: " + majorVersion + "." + minorVersion);
        }
        
        // 检查3:常量池tag验证
        verifyConstantPoolTags(bytecode);
        
        // 检查4:索引引用是否指向有效位置
        verifyIndexReferences(bytecode);
    }
}
元数据验证
java 复制代码
class MetadataVerifier {
    
    void verify(ClassMetadata metadata) {
        // 检查1:类是否有父类(除Object外)
        if (!metadata.className.equals("java/lang/Object") && 
            metadata.superClassName == null) {
            throw new IncompatibleClassChangeError("类必须有父类");
        }
        
        // 检查2:是否继承了final类
        if (metadata.superClass != null && 
            metadata.superClass.isFinal()) {
            throw new VerifyError("不能继承final类");
        }
        
        // 检查3:是否实现了抽象方法
        verifyAbstractMethods(metadata);
        
        // 检查4:字段/方法是否与父类冲突
        verifyOverrideRules(metadata);
    }
}
字节码验证

字节码验证使用数据流分析技术,确保程序在运行时不会出现以下问题:

java 复制代码
class BytecodeVerifier {
    
    void verify(byte[] bytecode) {
        // 创建控制流图
        ControlFlowGraph cfg = buildControlFlowGraph(bytecode);
        
        // 数据流分析
        for (BasicBlock block : cfg.getBlocks()) {
            // 模拟执行,验证类型正确性
            simulateExecution(block);
            
            // 检查操作数栈
            verifyOperandStack(block);
            
            // 检查局部变量表
            verifyLocalVariables(block);
            
            // 检查跳转目标
            verifyBranchTargets(block);
        }
        
        // 验证StackMapTable属性(Java 6+)
        verifyStackMapTable(bytecode);
    }
    
    // 示例:验证操作数栈
    void verifyOperandStack(BasicBlock block) {
        // 跟踪栈的深度和类型
        Stack<Type> operandStack = new Stack<>();
        
        for (Instruction ins : block.getInstructions()) {
            switch (ins.getOpcode()) {
                case ILOAD:    // 加载int到栈
                    operandStack.push(Type.INT);
                    break;
                    
                case IADD:     // int加法
                    if (operandStack.size() < 2) {
                        throw new VerifyError("栈下溢");
                    }
                    Type type1 = operandStack.pop();
                    Type type2 = operandStack.pop();
                    if (type1 != Type.INT || type2 != Type.INT) {
                        throw new VerifyError("操作数类型错误");
                    }
                    operandStack.push(Type.INT);
                    break;
                    
                case IRETURN:  // 返回int
                    if (operandStack.isEmpty()) {
                        throw new VerifyError("栈为空");
                    }
                    Type returnType = operandStack.pop();
                    if (returnType != Type.INT) {
                        throw new VerifyError("返回类型错误");
                    }
                    break;
                    
                // ... 其他指令验证
            }
            
            // 检查栈深度是否超限(最大65535)
            if (operandStack.size() > 65535) {
                throw new VerifyError("操作数栈溢出");
            }
        }
    }
}
符号引用验证
java 复制代码
class SymbolicReferenceVerifier {
    
    void verify(ClassMetadata metadata, ClassLoader loader) {
        // 验证所有符号引用是否可以被解析
        
        // 1. 验证类引用
        for (String className : metadata.referencedClasses) {
            try {
                loader.loadClass(className.replace('/', '.'));
            } catch (ClassNotFoundException e) {
                throw new NoClassDefFoundError("类不存在: " + className);
            }
        }
        
        // 2. 验证字段引用
        for (FieldRef ref : metadata.fieldReferences) {
            verifyFieldExists(ref);
        }
        
        // 3. 验证方法引用
        for (MethodRef ref : metadata.methodReferences) {
            verifyMethodExists(ref);
        }
    }
}

3、准备

准备阶段为类变量(static变量)分配内存并设置初始值 。这里有个重要概念:初始值 ≠ 代码中的赋值值

准备阶段的核心逻辑
java 复制代码
class PreparationPhase {
    
    void prepare(ClassMetadata metadata) {
        // 为所有static变量分配内存并设置零值
        for (FieldInfo field : metadata.staticFields) {
            // 根据类型分配内存
            MemoryAllocation allocation = allocateMemory(field);
            
            // 设置零值(Zero Value)
            // 注意:此时不执行任何Java代码的赋值操作!
            setZeroValue(field, allocation.address);
            
            // 特殊情况处理
            handleSpecialCases(field, allocation);
        }
    }
    
    // 零值规则
    void setZeroValue(FieldInfo field, long address) {
        switch (field.getType()) {
            case "I":    // int
            case "S":    // short  
            case "B":    // byte
            case "C":    // char
            case "Z":    // boolean
                writeInt(address, 0);
                break;
                
            case "J":    // long
                writeLong(address, 0L);
                break;
                
            case "F":    // float
                writeFloat(address, 0.0f);
                break;
                
            case "D":    // double
                writeDouble(address, 0.0);
                break;
                
            case "L":    // 引用类型
            case "[":
                writeReference(address, null);
                break;
        }
    }
    
    // 特殊情况:final static常量
    void handleSpecialCases(FieldInfo field, MemoryAllocation allocation) {
        if (field.isFinal() && field.isStatic()) {
            // 检查是否有ConstantValue属性
            ConstantValueAttribute constAttr = field.getAttribute(
                ConstantValueAttribute.class);
            
            if (constAttr != null) {
                // 对于编译时常量,在准备阶段直接赋实际值
                Object constantValue = constAttr.getValue();
                setFieldValue(field, allocation.address, constantValue);
                
                // 记录为编译时常量(后续可能被内联优化)
                markAsCompileTimeConstant(field);
            }
        }
    }
}
常见误解
java 复制代码
public class PreparationMisunderstanding {
    
    // 误解:准备阶段会把value设为10
    // 真相:准备阶段value=0,初始化阶段才设为10
    static int value = 10;
    
    // 特殊情况:final static常量
    // 准备阶段MAX就直接是100
    static final int MAX = 100;
    
    // 另一个特殊情况:非编译时常量
    // 准备阶段TIME=0,初始化阶段才计算
    static final long TIME = System.currentTimeMillis();
    
    static {
        System.out.println("静态代码块执行");
    }
    
    public static void main(String[] args) {
        // 这里会输出什么?
        System.out.println("value = " + value);  // 10
        System.out.println("MAX = " + MAX);      // 100
        System.out.println("TIME = " + TIME);    // 时间戳
    }
}

运行顺序:

  1. 准备阶段:value=0, MAX=100,TIME=0
  2. 初始化阶段:执行静态代码块和赋值 → value=10, TIME=当前时间戳

4、解析

解析阶段将符号引用转换为直接引用。这是连接静态存储和动态运行的关键步骤。

什么是符号引用和直接引用?
java 复制代码
class ReferenceExample {
    
    // 符号引用:一种描述性的引用
    SymbolicReference symRef = new SymbolicReference(
        "java/lang/String",      // 类名
        "length",                // 字段名
        "()I"                    // 描述符
    );
    
    // 直接引用:指向内存的具体指针
    DirectReference dirRef = new DirectReference(
        0x7f123456,             // 方法区中的类地址
        0x1000,                 // 方法在vtable中的偏移量
        null                    // 可能的内联缓存信息
    );
}
解析过程
java 复制代码
class ResolutionPhase {
    
    void resolveAll(ClassMetadata metadata, ClassLoader loader) {
        // 1. 类或接口解析
        resolveClasses(metadata.classReferences, loader);
        
        // 2. 字段解析
        resolveFields(metadata.fieldReferences);
        
        // 3. 方法解析
        resolveMethods(metadata.methodReferences);
        
        // 4. 接口方法解析
        resolveInterfaceMethods(metadata.interfaceMethodReferences);
    }
    
    // 类解析示例
    Class<?> resolveClass(String className, ClassLoader loader, 
                         Class<?> fromClass) {
        
        // 步骤1:加载目标类
        Class<?> targetClass;
        try {
            targetClass = loader.loadClass(className);
        } catch (ClassNotFoundException e) {
            throw new NoClassDefFoundError("类不存在: " + className);
        }
        
        // 步骤2:检查访问权限
        checkAccessPermission(fromClass, targetClass);
        
        // 步骤3:验证兼容性
        verifyCompatibility(fromClass, targetClass);
        
        // 步骤4:缓存解析结果
        cacheResolution(className, targetClass);
        
        return targetClass;
    }
    
    // 方法解析(考虑虚方法分派)
    Method resolveMethod(MethodRef ref, Class<?> fromClass) {
        // 1. 查找声明类
        Class<?> declaringClass = resolveClass(
            ref.getDeclaringClassName(), 
            fromClass.getClassLoader(),
            fromClass
        );
        
        // 2. 查找方法(考虑重载)
        Method method = findMethod(declaringClass, 
            ref.getName(), ref.getDescriptor());
        
        // 3. 检查访问权限
        checkMethodAccess(fromClass, method);
        
        // 4. 虚方法处理(分配vtable索引)
        if (!method.isStatic() && !method.isPrivate()) {
            int vtableIndex = allocateVTableIndex(method);
            ref.setVTableIndex(vtableIndex);
        }
        
        return method;
    }
}
早解析 vs 晚解析

JVM有两种解析策略:

java 复制代码
class ResolutionTiming {
    
    // 早解析(Eager Resolution)
    // - 在类加载的链接阶段完成所有解析
    // - 优点:启动时一次性完成,运行时不暂停
    // - 缺点:启动慢,可能解析不用的引用
    
    // 晚解析(Lazy Resolution)
    // - 只在第一次使用时才解析
    // - 优点:启动快,按需解析
    // - 缺点:运行时可能暂停进行解析
    
    // 现代JVM通常采用混合策略
    void hybridResolution() {
        // 1. 启动时解析必要的引用(如父类、接口)
        resolveEssentialReferences();
        
        // 2. 运行时按需解析其他引用
        // 使用invokedynamic指令支持晚解析
        handleInvokeDynamic();
    }
}

5、初始化

初始化是类加载的最后一步 ,也是真正开始执行Java代码的阶段。

初始化触发条件

见第2章节

初始化过程详解
java 复制代码
class InitializationPhase {
    
    // JVM使用<clinit>()方法进行类初始化
    // <clinit>()由编译器自动生成
    
    void initialize(Class<?> clazz) {
        // 获取初始化锁(每个类唯一)
        Object initLock = getClassInitLock(clazz);
        
        synchronized (initLock) {
            // 双重检查锁定
            if (isInitialized(clazz)) {
                return;
            }
            
            // 标记为正在初始化(防止递归)
            markInitializing(clazz);
            
            try {
                // 步骤1:递归初始化父类
                Class<?> superClass = clazz.getSuperclass();
                if (superClass != null && !isInitialized(superClass)) {
                    initialize(superClass);
                }
                
                // 步骤2:初始化接口(如果定义了默认方法)
                initializeInterfacesWithDefaultMethods(clazz);
                
                // 步骤3:执行<clinit>()方法
                executeClinitMethod(clazz);
                
                // 步骤4:标记为已初始化
                markInitialized(clazz);
                
            } catch (Throwable t) {
                // 初始化失败
                markInitializationFailed(clazz, t);
                throw new ExceptionInInitializerError(t);
            }
        }
    }
    
    // <clinit>()方法的生成规则
    byte[] generateClinitMethod(ClassMetadata metadata) {
        BytecodeWriter writer = new BytecodeWriter();
        
        // 按源码顺序收集初始化代码
        List<InitializationCode> initCodes = collectInitCodes(metadata);
        
        for (InitializationCode code : initCodes) {
            if (code instanceof StaticFieldAssignment) {
                // 生成静态字段赋值代码
                generateFieldAssignment(writer, (StaticFieldAssignment) code);
                
            } else if (code instanceof StaticBlock) {
                // 插入静态代码块
                writer.write(((StaticBlock) code).getBytecode());
            }
        }
        
        // 添加return指令
        writer.writeReturn();
        
        return writer.toByteArray();
    }
}
初始化顺序示例
java 复制代码
public class InitializationOrder {
    
    // 静态字段(按定义顺序初始化)
    static int a = init("a", 1);
    
    static {
        System.out.println("第一个静态代码块");
    }
    
    static int b = init("b", 2);
    
    static {
        System.out.println("第二个静态代码块");
    }
    
    static int c = init("c", 3);
    
    private static int init(String name, int value) {
        System.out.println("初始化 " + name + " = " + value);
        return value;
    }
    
    // 父类
    static class Parent {
        static {
            System.out.println("Parent类初始化");
        }
    }
    
    // 子类
    static class Child extends Parent {
        static {
            System.out.println("Child类初始化");
        }
    }
    
    public static void main(String[] args) {
        System.out.println("=== 开始测试 ===");
        
        // 触发Child类初始化
        new Child();
        
        System.out.println("=== 测试结束 ===");
    }
}
java 复制代码
=== 开始测试 ===
初始化 a = 1
第一个静态代码块
初始化 b = 2
第二个静态代码块
初始化 c = 3
Parent类初始化
Child类初始化
=== 测试结束 ===

顺序:静态->父类->子类

重要规则

  1. 父类先于子类初始化
  2. 静态字段和静态代码块按源码顺序执行
  3. 每个类只初始化一次

四、类加载器

类加载的 "加载" 阶段,由类加载器(ClassLoader)负责。JVM 提供了 3 种默认类加载器,还有用户可自定义的类加载器,它们形成 "双亲委派模型"。

类加载器的核心价值

  • 延迟加载:类在第一次使用时才加载
  • 安全性:防止核心API被篡改
  • 隔离性:不同模块可以使用不同版本的类
  • 动态性:支持热部署和动态扩展

3 种默认类加载器

类加载器 作用范围 加载路径
启动类加载器(Bootstrap) 加载 JVM 核心类(比如java.lang.String JAVA_HOME/jre/lib/rt.jar 等核心 jar
扩展类加载器(Extension) 加载 JVM 扩展类 JAVA_HOME/jre/lib/ext 目录
应用程序类加载器(App) 加载用户编写的类和第三方 jar 应用 classpath 路径

1. Bootstrap ClassLoader(启动类加载器)

  • 父加载器:无(null)
  • 加载范围:JAVA_HOME/jre/lib下的核心库(rt.jar、resources.jar等)
  • 实现:原生代码(C/C++)实现,Java中不可见
  • 获取方式:String.class.getClassLoader()返回null

2. Extension ClassLoader(扩展类加载器)

  • 父加载器:Bootstrap ClassLoader
  • **加载范围**JAVA_HOME/jre/lib/ext目录
  • **实现**sun.misc.Launcher$ExtClassLoader
  • **获取方式*ClassLoader.getSystemClassLoader().getParent()

3. Application/System ClassLoader(应用/系统类加载器)

  • 父加载器:Extension ClassLoader
  • 加载范围:`CLASSPATH路径下的类
  • 实现:sun.misc.Launcher$AppClassLoader
  • 获取方式:`ClassLoader.getSystemClassLoader()

4. 自定义ClassLoader

  • 父加载器:默认是`AppClassLoader,可指定
  • 加载范围:自定义路径
  • 实现:继承ClassLoade类
java 复制代码
public class ClassLoaderHierarchy {
    
    public static void main(String[] args) {
        // 演示类加载器层次结构
        
        // 1. Bootstrap ClassLoader(启动类加载器)
        //    - 由C++实现,Java中为null
        //    - 加载JAVA_HOME/jre/lib目录下的核心类库
        ClassLoader bootstrap = String.class.getClassLoader();
        System.out.println("String的类加载器: " + bootstrap);  // null
        
        // 2. Platform ClassLoader(平台类加载器,JDK9+)
        //    - 替代JDK8的Extension ClassLoader
        //    - 加载JAVA_HOME/jre/lib/ext目录或系统变量指定的目录
        ClassLoader platform = ClassLoader.getPlatformClassLoader();
        System.out.println("平台类加载器: " + platform);
        
        // 3. Application ClassLoader(应用类加载器,也叫System ClassLoader)
        //    - 加载classpath或系统属性java.class.path指定的类
        ClassLoader app = ClassLoader.getSystemClassLoader();
        System.out.println("应用类加载器: " + app);
        
        // 4. 当前类的类加载器
        ClassLoader current = ClassLoaderHierarchy.class.getClassLoader();
        System.out.println("当前类的类加载器: " + current);
        
        // 打印完整的层次结构
        printClassLoaderHierarchy(current);
    }
    
    static void printClassLoaderHierarchy(ClassLoader loader) {
        System.out.println("\n=== 类加载器层次结构 ===");
        while (loader != null) {
            System.out.println(loader);
            loader = loader.getParent();
        }
        System.out.println("Bootstrap ClassLoader (null)");
    }
}
java 复制代码
String的类加载器: null
平台类加载器: jdk.internal.loader.ClassLoaders$PlatformClassLoader@1a2b3c4d
应用类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@5e8c92f4
当前类的类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@5e8c92f4

=== 类加载器层次结构 ===
jdk.internal.loader.ClassLoaders$AppClassLoader@5e8c92f4
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1a2b3c4d
Bootstrap ClassLoader (null)

双亲委派模型

双亲委派是类加载器的核心设计模式 ,它确保了类的唯一性安全性。

**双亲委派模型的工作过程:**如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载

双亲委派的优势和劣势

优势:

  • 避免重复加载: 子类加载器先委托父类加载器尝试加载类,确保同一个类(全限定名相同)只会被一个类加载器加载一次,避免内存中出现多个相同的 Class 对象,保证程序运行一致性。
  • **安全性: **JVM 核心类(如<font style="color:rgb(31, 35, 41);background-color:rgba(0, 0, 0, 0);">java.lang.String</font>)由启动类加载器优先加载,开发者无法通过自定义同名类覆盖核心类,防止核心 API 被篡改,避免程序运行混乱。
  • 类的唯一性

劣势:

  • **灵活性不足: **严格的层级委托机制限制了特殊加载需求,例如某些框架(如 Tomcat)需要对不同 Web 应用的类进行隔离加载,必须打破双亲委派才能实现。
  • 无法适配动态场景:对于需要动态加载类(如热部署、模块化部署)的场景,双亲委派的固定层级会阻碍类的重新加载,需额外开发复杂逻辑绕开机制。
  • 子类加载器依赖父类:如果父类加载器加载了某个类,子类加载器无法再加载该类的不同版本,导致无法实现类的版本隔离,不适用于需要多版本类共存的场景(如插件化开发)。
java 复制代码
public class ParentDelegationAdvantages {
    
    // 优势1:避免重复加载
    void avoidDuplicateLoading() {
        // 同一个类在多个加载器间共享
        // 节省内存,保证类的一致性
    }
    
    // 优势2:安全性
    void security() {
        // 核心类库由Bootstrap加载
        // 自定义的java.lang.String不会被加载
        // 防止核心API被篡改
        
        try {
            // 尝试加载自定义的java.lang.String
            CustomClassLoader loader = new CustomClassLoader();
            Class<?> customString = loader.loadClass("java.lang.String");
            System.out.println("这里永远不会执行");
        } catch (ClassNotFoundException e) {
            System.out.println("安全保护:无法加载java.lang.String");
        }
    }
    
    // 优势3:类的唯一性
    void classUniqueness() {
        // 同一个类在不同加载器下被视为不同类
        // 但通过双亲委派,避免了这种情况
        
        // ClassA由AppClassLoader加载
        // ClassA在CustomClassLoader中不会被重复加载
        // 而是委派给AppClassLoader
    }
}

打破双亲委派的场景

在Java的世界里,双亲委派模型被奉为类加载的"黄金法则"。但正如所有规则都有例外,真正的高手不仅知道如何遵守规则,更知道何时应该优雅地打破它。

场景:

  • 场景1:SPI(Service Provider Interface)机制
  • 场景2:热部署需求
  • 场景3:模块化与版本隔离
  • 场景4:代码保护与加密
java 复制代码
public class RealWorldChallenges {
    
    // 场景1:SPI(Service Provider Interface)机制
    class SPIChallenge {
        /*
        问题描述:
        JDBC Driver由Bootstrap加载器加载(在rt.jar中)
        但具体的数据库驱动(如MySQL Driver)在ClassPath中
        Bootstrap加载器看不到ClassPath中的类!
        
        结果:DriverManager无法加载具体的数据库驱动
        */
        
        void demonstrate() {
            // 这行代码在纯双亲委派下会失败
            Connection conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/test", 
                "user", "pass"
            );
        }
    }
    
    // 场景2:热部署需求
    class HotDeployChallenge {
        /*
        问题描述:
        企业应用需要7x24小时运行
        代码更新不能重启服务器
        但双亲委派会缓存已加载的类
        
        结果:修改后的类无法重新加载
        */
        
        void demonstrate() throws Exception {
            // 第一次加载
            ClassLoader loader1 = new CustomClassLoader();
            Class<?> clazz1 = loader1.loadClass("MyService");
            
            // 修改MyService后...
            // 双亲委派会直接返回缓存的Class对象
            // 无法加载新版本!
            Class<?> clazz2 = loader1.loadClass("MyService");
            System.out.println(clazz1 == clazz2);  // true
        }
    }
    
    // 场景3:模块化与版本隔离
    class ModuleIsolationChallenge {
        /*
        问题描述:
        大型应用有多个模块
        不同模块可能依赖同一库的不同版本
        双亲委派只加载第一个找到的版本
        
        结果:版本冲突,功能异常
        */
        
        void demonstrate() {
            // 模块A需要v1.0的commons-lang
            // 模块B需要v2.0的commons-lang
            // 双亲委派下,只能加载一个版本
            // 另一个模块会出错!
        }
    }
    
    // 场景4:代码保护与加密
    class CodeProtectionChallenge {
        /*
        问题描述:
        商业软件需要保护源代码
        类文件需要加密存储
        但标准类加载器只能加载明文的.class文件
        
        结果:无法实现代码加密保护
        */
    }
}
相关推荐
我尽力学18 小时前
JVM类加载子系统、类加载机制
jvm
小罗和阿泽19 小时前
java [多线程基础 二】
java·开发语言·jvm
小罗和阿泽19 小时前
java 【多线程基础 一】线程概念
java·开发语言·jvm
隐退山林19 小时前
JavaEE:多线程初阶(一)
java·开发语言·jvm
xie_pin_an19 小时前
C++ 类和对象全解析:从基础语法到高级特性
java·jvm·c++
是一个Bug20 小时前
Java后端开发面试题清单(50道)
java·开发语言·jvm
曹轲恒20 小时前
JVM——类加载机制
jvm
木风小助理20 小时前
Android 数据库实操指南:从 SQLite 到 Realm,不同场景精准匹配
jvm·数据库·oracle
xxxmine20 小时前
JVM类加载机制
jvm