欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 新车 > Java虚拟机解剖:从字节码到机器指令的终极之旅(二)

Java虚拟机解剖:从字节码到机器指令的终极之旅(二)

2025/6/14 8:00:54 来源:https://blog.csdn.net/ldwtxwh/article/details/148620935  浏览:    关键词:Java虚拟机解剖:从字节码到机器指令的终极之旅(二)
第三章:类加载子系统

类加载时机

类加载发生在程序运行过程中的以下关键阶段:

类加载器类型及职责

2. 准备(Preparation)

为静态变量分配内存并设置默认初始值:

初始化阶段(Initialization)

执行类构造器<clinit>()方法,该方法由编译器自动生成:

class MyClass {
    static int a = initA(); // 静态变量赋值
    static {
        System.out.println("Static block"); // 静态代码块
    }
    static int b = initB();
    
    static int initA() { return 1; }
    static int initB() { return 2; }
}

  • 类加载子系统是Java虚拟机(JVM)的核心组件,负责将类和接口的二进制数据(.class文件)加载到内存中,并将其转换为JVM能够使用的运行时数据结构。它在JVM架构中处于承上启下的位置:

  • 核心功能

  • 定位和加载:从各种来源(文件系统、网络、JAR包等)查找并读取字节码

  • 链接:将加载的类合并到JVM运行时环境

  • 初始化:执行类的初始化逻辑

  • 显式创建实例:遇到new指令时

  • 提供访问入口:创建java.lang.Class对象作为访问类元数据的接口

  • new MyObject(); // 触发MyObject类加载

    访问静态成员:执行getstatic/putstatic/invokestatic指令时

  • int value = MyClass.STATIC_FIELD; // 触发MyClass加载

    反射调用:通过反射API操作类时

  • Class.forName("com.example.MyClass"); // 显式触发加载

    子类初始化:初始化子类时父类尚未加载

  • class Child extends Parent {} // 加载Child时先加载Parent

    JVM启动时:预先加载核心类如java.lang.Object

  • 接口默认方法:实现接口的类初始化时

  • interface MyInterface { default void method() {} }
    class Impl implements MyInterface {} // 加载Impl时加载MyInterface

    二、类加载过程

    加载阶段(Loading)

    加载阶段由类加载器完成,主要任务包括:

  • 查找字节码:通过全限定类名查找二进制数据

  • 读取字节流:将字节码读入内存

  • 创建Class对象:在堆中生成java.lang.Class实例

  • 类加载器实现加载路径职责范围
    BootstrapC++$JAVA_HOME/lib核心Java库(rt.jar)
    ExtensionJava$JAVA_HOME/lib/ext扩展库
    ApplicationJava$CLASSPATH应用程序类
    CustomJava自定义特殊需求
  • 类加载器层次关系
  • 链接阶段(Linking)

    1. 验证(Verification)

    确保.class文件符合JVM规范:

  • 文件格式验证:魔数(0xCAFEBABE)、版本号等

  • 元数据验证:语义检查(是否有父类、是否实现接口等)

  • 字节码验证:数据流和控制流分析

  • 符号引用验证:确保引用的类/字段/方法存在

  • 2. 准备(Preparation)

    为静态变量分配内存并设置默认初始值:

  • public static int value = 123; 
    // 准备阶段:value = 0
    // 初始化阶段:value = 123public static final int CONST = 456;
    // 准备阶段:CONST = 456 (final常量直接赋值)
    3. 解析(Resolution)

    将符号引用转换为直接引用:

  • 类/接口解析:将类名转换为Class对象引用

  • 字段解析:确定字段在内存中的偏移量

  • 方法解析:定位方法实际入口地址

  • 接口方法解析:类似方法解析

  • 初始化阶段(Initialization)

    执行类构造器<clinit>()方法,该方法由编译器自动生成:

  • class MyClass {static int a = initA(); // 静态变量赋值static {System.out.println("Static block"); // 静态代码块}static int b = initB();static int initA() { return 1; }static int initB() { return 2; }
    }

    编译器生成的<clinit>方法字节码:

  • 0: invokestatic  #2  // Method initA:()I
    3: putstatic     #3  // Field a:I
    6: getstatic     #4  // Field java/lang/System.out:Ljava/io/PrintStream;
    9: ldc           #5  // String Static block
    11: invokevirtual #6  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    14: invokestatic  #7  // Method initB:()I
    17: putstatic     #8  // Field b:I
    20: return

    初始化规则

  • 父类优先于子类初始化

  • 接口初始化不触发父接口初始化

  • 多线程环境下初始化操作会被正确加锁

三、类加载器

双亲委派模型

双亲委派模型是Java类加载的基础机制:

工作流程

  1. 类加载请求首先委派给父加载器

  2. 父加载器递归向上委派

  3. 顶层Bootstrap加载器尝试加载

  4. 父加载器无法加载时,子加载器尝试加载

  5. 最终由发起请求的加载器完成加载或抛出ClassNotFoundException

优势

  • 安全性:防止核心API被篡改

  • 一致性:保证类在JVM中的唯一性

  • 高效性:避免重复加载

自定义类加载器

实现步骤

  1. 继承java.lang.ClassLoader

  2. 重写findClass()方法

  3. 在findClass()中读取字节码

  4. 调用defineClass()定义类

示例:数据库类加载器

public class DatabaseClassLoader extends ClassLoader {private final DataSource dataSource;public DatabaseClassLoader(DataSource dataSource) {this.dataSource = dataSource;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 1. 从数据库读取字节码byte[] bytes = loadClassBytesFromDB(name);// 2. 定义类return defineClass(name, bytes, 0, bytes.length);}private byte[] loadClassBytesFromDB(String className) {try (Connection conn = dataSource.getConnection();PreparedStatement stmt = conn.prepareStatement("SELECT bytecode FROM class_store WHERE class_name=?")) {stmt.setString(1, className);try (ResultSet rs = stmt.executeQuery()) {if (rs.next()) {return rs.getBytes("bytecode");}}} catch (SQLException e) {throw new RuntimeException("Failed to load class from DB", e);}throw new ClassNotFoundException(className);}
}

四、类加载机制的优化与注意事项

性能优化

根源:类加载器与加载的类相互引用

2. 内存泄漏

类加载顺序

案例3:Klass与InstanceKlass结构体

在HotSpot JVM中,类元数据通过Klass体系存储:

示例:查看String类布局

  1. 并行加载:使用ClassLoader.registerAsParallelCapable()启用并行加载

  2. public class ParallelClassLoader extends URLClassLoader {static {registerAsParallelCapable();}// ...
    }

    缓存优化:合理使用缓存但避免内存泄漏

  3. private final Map<String, Class<?>> cache = Collections.synchronizedMap(new WeakHashMap<>());

  4. 类索引优化:在大型应用中优化类查找算法常见问题与解决方案

  5. 1. 类冲突问题

  6. 现象java.lang.LinkageErrorClassCastException

  7. ClassLoader loader1 = new CustomClassLoader();
    ClassLoader loader2 = new CustomClassLoader();Class<?> class1 = loader1.loadClass("com.example.MyClass");
    Class<?> class2 = loader2.loadClass("com.example.MyClass");// 看似相同的类,实际不同
    System.out.println(class1 == class2); // false

    解决方案

  8. 使用同一类加载器加载相关类

  9. 使用接口隔离技术(OSGi规范)

  10. 2. 内存泄漏

  11. 根源:类加载器与加载的类相互引用

  12. public class LeakyClassLoader extends ClassLoader {private final Map<String, Class<?>> classes = new HashMap<>();@Overrideprotected Class<?> findClass(String name) {// 加载类Class<?> clazz = ...;classes.put(name, clazz); // 强引用导致无法GCreturn clazz;}
    }

    解决方案

  13. private final Map<String, WeakReference<Class<?>>> classes = new ConcurrentHashMap<>();

    深度案例分析

    案例1:Tomcat类加载器架构

    Tomcat需要同时支持:

  14. 应用隔离:不同Web应用使用独立类空间

  15. 资源共享:公共库只需加载一次

  16. 核心设计

  17. Common:加载Tomcat核心和公共库

  18. Catalina:加载Tomcat内部实现

  19. Shared:加载Web应用共享库

  20. WebApp:每个Web应用独立加载器

  21. 类加载顺序

  22. public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 1. 检查本地缓存Class<?> clazz = findLoadedClass(name);if (clazz != null) return clazz;// 2. 避免WebApp覆盖核心类if (name.startsWith("java.")) {return super.loadClass(name, resolve);}// 3. 尝试WebApp类加载器try {clazz = findClass(name);if (clazz != null) return clazz;} catch (ClassNotFoundException ignore) {}// 4. 委托给Shared类加载器return super.loadClass(name, resolve);}
    }
    案例2:热部署实现

    热部署关键是通过新建类加载器实现类更新:

  23. public class HotDeployer implements Runnable {private final String className;private final File classFile;private volatile Class<?> loadedClass;public HotDeployer(String className, File classFile) {this.className = className;this.classFile = classFile;}public void run() {// 1. 创建新的类加载器URLClassLoader newLoader = new URLClassLoader(new URL[]{classFile.getParentFile().toURI().toURL()},getClass().getClassLoader());try {// 2. 加载新版本类Class<?> newClass = newLoader.loadClass(className);// 3. 创建实例并替换旧引用Object newInstance = newClass.getDeclaredConstructor().newInstance();loadedClass = newClass;// 4. 旧类加载器将在GC时卸载} catch (Exception e) {// 处理异常}}public Class<?> getCurrentClass() {return loadedClass;}
    }

    卸载条件

  24. 类的所有实例已被GC

  25. 类的ClassLoader实例已被GC

  26. 类的java.lang.Class对象没有引用

  27. 案例3:Klass与InstanceKlass结构体

  28. 在HotSpot JVM中,类元数据通过Klass体系存储:

  29. // hotspot/share/oops/klass.hpp
    class Klass : public Metadata {// 共享元数据volatile jint _layout_helper;Symbol* _name;
    };// hotspot/share/oops/instanceKlass.hpp
    class InstanceKlass: public Klass {// 类特定元数据Array<Method*>* _methods;Array<Klass*>* _local_interfaces;Array<Klass*>* _transitive_interfaces;InstanceKlass* _array_klasses;InstanceKlass* _java_mirror;ClassLoaderData* _class_loader_data;
    };

    内存布局

  30. 使用HSDB查看类元数据

  31. 启动HSDB:jdk/bin/java -cp sa-jdi.jar sun.jvm.hotspot.HSDB

  32. 附加到目标JVM进程

  33. 查看类信息:

    • Class Browser:浏览已加载类

    • Inspector:查看对象内存布局

    • Universe:查看堆内存概况

    • Class: java.lang.String
      Superclass: java.lang.Object
      Loader: bootstrap
      Fields:- value: [C @offset 12- hash: I @offset 16- coder: B @offset 20
      Methods:- hashCode()I- equals(Ljava/lang/Object;)Z- ...

      总结

    • 类加载时机:由JVM规范严格定义,主要发生在首次主动引用时

    • 加载过程三阶段

      • 加载:定位字节码并创建Class对象

      • 链接:验证、准备和解析

      • 初始化:执行<clinit>方法

    • 双亲委派模型:保障安全性和一致性的核心机制

    • 自定义类加载器:实现热部署、模块化等高级特性的关键

    • 性能优化:并行加载、缓存管理和类索引优化

    • 常见问题:类冲突和内存泄漏需特别关注

    • 生产实践

      • Tomcat通过分层加载器实现应用隔离

      • 热部署通过新建类加载器实现类更新

      • Klass体系是JVM类元数据的内部表示

    • 下一篇将从内存的角度去讲解jvm底层的实现逻辑,感兴趣的可以收藏持续关注

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词