JVM 类加载机制详解

[复制链接]
发表于 2025-12-30 14:09:09 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
JVM 类加载机制详解

在 Java 假造机(JVM)中,类加载机制是一个非常紧张的构成部门,它负责将类的字节码文件加载到内存中,并举行一系列的处置处罚,终极使类可以或许被假造机利用。本文将详细先容 JVM 类加载机制的干系内容。
一、类加载的概念

在 JVM 假造机实现规范中,通过 ClassLoader 类加载器把 *.class 字节码文件(文件流)加载到内存,并对字节码文件内容举行验证、预备、分析和初始化,终极形成可以被假造机直接利用的 java.lang.Class 对象,这个过程被称作类加载。类是在运行期间第一次利用时,被类加载器动态加载至 JVM。JVM 不会一次性加载全部类,由于如许会占用许多内存。
二、类的生命周期

类的生命周期包罗以下 7 个阶段:

  • 加载(Loading):通过类的完全限定名称获取界说该类的 *.class 字节码文件的二进制字节省,将其转换为运行时存储布局,并在内存中天生代表该类的 Class 对象。
  • 验证(Verification):确保 *.class 字节码文件中包罗的信息符合当前假造机的要求,且不会危害假造机的安全,包罗文件格式验证、元数据验证、字节码验证和符号引用验证。
  • 预备(Preparation):为类变量分配内存并设置初始值(一样平常为 0 值,常量除外),实例变量不在此阶段分配内存。
  • 分析(Resolution):将常量池的符号引用更换为直接引用。
  • 初始化(Initialization):真正开始实验类中界说的 Java 步调代码,是假造机实验类构造器 <clinit>() 方法的过程。
  • 利用(Using):步调对类举行实例化、调用其方法等操纵。
  • 卸载(Unloading):当类不再被利用时,由垃圾接纳器举行卸载。
竣事类生命周期的几种场景:

  • 实验 System.exit() 方法。
  • 步调正常实验竣事。
  • 步调实验中碰到了非常或错误而非常停止。
  • 操纵体系出现错误或欺凌竣事步调而导致 JVM 假造机历程停止。
三、类加载过程

(一)加载

在加载阶段,JVM 紧张完成以下 3 件事:

  • 获取字节省:通过类的完全限定名称获取界说该类的 *.class 字节码文件的二进制字节省。转换存储布局:将该字节省表现的静态存储布局转换为 Metaspace 元空间区的运行时存储布局。
  • 天生 Class 对象:在内存中天生一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。
*.class 字节码文件的加载方式有多种:

  • 本地文件体系直接读取。
    1. // 例如,可以从本地文件系统直接读取
    2. FileInputStream fis = new FileInputStream("MyClass.class");
    3. byte[] byteArray = new byte[fis.available()];
    4. fis.read(byteArray);
    5. fis.close();
    6. // 也可以从网络中通过服务器响应读取,如 Web Applet 技术
    7. // 还可以从 JAR、EAR、WAR 等压缩文件中读取等
    复制代码
  • 从网络中通过服务器相应读取,比方 Web Applet 技能。
  • 从 JAR、EAR、WAR 等压缩文件中读取。
  • 运行时通过动态署理技能天生字节码文件,比方在 java.lang.reflect.Proxy 利用 ProxyGenerator.generateProxyClass 的署理类的二进制字节省。
  • 由其他文件或容器天生,比方由 tomcat 将 *.jsp 文件翻译成 *.java 文件后,编译天生对应的 *.class 字节码文件。
在加载阶段完成之后,*.class 字节码文件的类信息数据就会存储在元空间,同时在 JVM 假造机堆区天生一个该类的 Class 对象。
(二)验证

验证阶段紧张确保 *.class 字节码文件中包罗的信息符合当前假造机的要求,并不会危害假造机的安全。验证阶段会完成下面四个阶段的查验:

  • 文件格式验证:验证字节省是否符合 *.class 字节码文件格式的规范,且能被当前版本的假造机处置处罚。
    1. // 检查是否以魔数 0xCAFEBABE 开头
    2. if (byteArray[0]!= 0xCA || byteArray[1]!= 0xFE || byteArray[2]!= 0xBA || byteArray[3]!= 0xBE) {
    3.     throw new ClassFormatError("Invalid magic number in class file");
    4. }
    5. // 检查主、次版本号是否在当前虚拟机处理范围之内
    6. int majorVersion = (byteArray[4] << 8) | byteArray[5];
    7. int minorVersion = (byteArray[6] << 8) | byteArray[7];
    8. if (majorVersion > JVM_SUPPORTED_MAJOR_VERSION || (majorVersion == JVM_SUPPORTED_MAJOR_VERSION && minorVersion > JVM_SUPPORTED_MINOR_VERSION)) {
    9.     throw new UnsupportedClassVersionError("Unsupported class file version");
    10. }
    11. // 检查常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)等其他验证内容
    12. //...
    复制代码
  • 元数据验证:对字节码形貌的信息举行语义分析,以包管其形貌的信息符合 Java 语言规范的要求。
    1. // 检查这个类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类)
    2. if (className!= "java.lang.Object" &&!classHasSuperclass(byteArray)) {
    3.     throw new ClassFormatError("Class has no superclass");
    4. }
    5. // 检查这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)等其他验证内容
    6. //...
    复制代码
  • 字节码验证:通过数据流和控制流分析,确定步调语义是正当的、符合逻辑的。
    1. // 保证跳转指令不会跳转到方法体以外的字节码指令上
    2. for (int i = 0; i < bytecodeLength; i++) {
    3.     int opcode = byteArray[i] & 0xFF;
    4.     if (opcode == JUMP_OPCODE) {
    5.         int targetOffset = ((byteArray[i + 1] << 8) | byteArray[i + 2]) & 0xFFFF;
    6.         if (targetOffset < 0 || targetOffset >= bytecodeLength) {
    7.             throw new BytecodeVerificationError("Invalid jump target");
    8.         }
    9.     }
    10.     // 检查其他字节码验证规则,如类型转换的有效性、操作数栈与指令的配合等
    11.     //...
    12. }
    复制代码
  • 符号引用验证:发生在假造机将符号引用转化为直接引用的时间,这个转化动作将在毗连的第三个阶段 —— 分析阶段中发生,确保分析动作能正常实验。
    1. // 检查符号引用中通过字符串描述的全限定名是否能找到对应的类
    2. String classNameFromSymbol = getClassNameFromSymbolReference(byteArray);
    3. try {
    4.     Class.forName(classNameFromSymbol);
    5. } catch (ClassNotFoundException e) {
    6.     throw new SymbolReferenceVerificationError("Class not found in symbol reference");
    7. }
    8. // 检查在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段等验证内容
    9. //...
    复制代码
    为什么须要验证呢?Java 语言本身是相对安全的语言,但 *.class 字节码文件并不肯定要求用 Java 源码编译而来,可以利用任何途径,以致可用十六进制编译器直接编写来产生 *.class 字节码文件。类的加载是 JVM 针对 *.class 字节码文件的读取加载机制,以是假造机如果不查抄输入的字节省,大概会由于载入了有害的字节省而导致体系瓦解,以是验证是假造机对自身掩护的一项紧张工作。别的,通过类加载机制的验证环节,可以加强表明器的运行期实验性能,由于表明器在运行期间无需再对每条实验指令举行查抄。
(三)预备

类变量是被 static 修饰的变量,预备阶段为类变量分配内存并设置初始值,利用的是元空间区的内存。实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。初始值一样平常为 0 值。
  1. // 例如,下面的类变量 value 被初始化为 0 而不是 123
  2. public static int value = 123;
  3. // 如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0
  4. public static final int CONSTANT_VALUE = 123;
复制代码
(四)分析

将常量池的符号引用更换为直接引用。
(五)初始化

初始化阶段才真正开始实验类中界说的 Java 步调代码。初始化阶段是假造机实验类构造器 <clinit>() 方法的过程。在预备阶段,类变量已经赋过一次体系要求的初始值,而在初始化阶段,根据步调员通过步调订定的主观操持去初始化类变量和别的资源。
<clinit>() 是由编译器主动网络类中全部类变量的赋值动作和静态语句块中的语句归并产生的,编译器网络的次序由语句在源文件中出现的次序决定。以是,静态语句块只能访问到界说在它之前的类变量,界说在它之后的类变量只能赋值,不能访问。
  1. // 例如以下代码中静态变量 i 只能赋值,不能访问,因为 i 定义在静态代码块的后面
  2. static {
  3.     // 这里不能访问 i,会报错
  4.     // System.out.println(i);
  5. }
  6. static int i = 10;
复制代码
<clinit> 线程安全,假造机会包管一个类的 <clinit>() 方法在多线程情况下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程实验这个类的 <clinit>() 方法,别的线程都会壅闭期待,直到运动线程实验 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操纵,就大概造成多个线程壅闭,在实际过程中,该壅闭非常潜伏,险些不会被察觉。
四、类加载的机会

(一)主动引用

假造机规范中并没有欺凌束缚何时举行加载,但是规范严酷规定了只有下列六种情况必须对类举行加载:

  • 当碰到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,好比 new 一个对象,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    1. // 当 jvm 执行 new 指令时会加载类,即当程序创建一个类的实例对象
    2. MyClass obj = new MyClass();
    3. // 当 jvm 执行 getstatic 指令时会加载类,即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)
    4. int staticValue = MyClass.staticVariable;
    5. // 当 jvm 执行 putstatic 指令时会加载类,即程序给类的静态变量赋值
    6. MyClass.staticVariable = 10;
    7. // 当 jvm 执行 invokestatic 指令时会加载类,即程序调用类的静态方法
    8. MyClass.staticMethod();
    复制代码
  • 利用 java.lang.reflect 包的方法对类举行反射调用时如 Class.forName("..."),或 newInstance() 等等。如果类没初始化,须要触发类的加载。
    1. try {
    2.     Class<?> clazz = Class.forName("MyClass");
    3.     Object instance = clazz.newInstance();
    4. } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
    5.     e.printStackTrace();
    6. }
    复制代码
  • 加载一个类,如果其父类还未加载,则先触发该父类的加载。
    1. class ChildClass extends ParentClass {}
    2. // 当加载 ChildClass 时,如果 ParentClass 未加载,会先加载 ParentClass
    复制代码
  • 当假造机启动时,用户须要界说一个要实验的主类(包罗 main() 方法的类),假造机会先加载这个类。
  • 当一个接口中界说了 JDK8 新到场的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。
(二)被动引用

除主动引用之外,全部引用类的方式都不会触发加载,称为被动引用。
被动引用的常见例子包罗:

  • 通过子类引用父类的静态字段,不会导致子类加载。
    1. class ParentClass {
    2.     public static int staticField = 10;
    3. }
    4. class ChildClass extends ParentClass {}
    5. // 这里不会加载 ChildClass,只会加载 ParentClass
    6. int value = ChildClass.staticField;
    复制代码
  • 通过数组界说来引用类,不会触发此类的加载。该过程会对数组类举行加载,数组类是一个由假造机主动天生的、直接继续自 Object 的子类,此中包罗了数组的属性和方法。
    1. MyClass[] array = new MyClass[10];
    2. // 这里只会加载数组类,不会加载 MyClass
    复制代码
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到界说常量的类,因此不会触发界说常量的类的加载。
    1. class ConstantClass {
    2.     public static final int CONSTANT = 10;
    3. }
    4. class OtherClass {
    5.     public static void main(String[] args) {
    6.         int value = ConstantClass.CONSTANT;
    7.         // 这里不会加载 ConstantClass
    8.     }
    9. }
    复制代码
    五、类加载器

    (一)什么是类加载器

    在类加载过程的加载阶段,通过类的完全限定名,获取形貌类的二进制流的实现类,被称为 “类加载器”。
    (二)类加载器分类

    从 JVM 假造机的角度来讲,只存在以下两种差异的类加载器:

    • 启动类加载器(Bootstrap ClassLoader):利用 C++ 实现,是假造机的一部门。它负责将存放在 <JRE_HOME>\lib 目次中的,大概被 -Xbootclasspath 参数所指定的路径中的,而且是假造机辨认的(仅按照文件名辨认,如 rt.jar,名字不符合的类库纵然放在 lib 目次中也不会被加载)类库加载到假造机内存中。比方 java.util.*,java.io.*,java.lang.* 类等常用根本库都是由启动类加载器加载。启动类加载器无法被 Java 步调直接引用。
    • 别的类的加载器:利用 Java 实现,独立于假造机,继续自抽象类 java.lang.ClassLoader。
    从 Java 开辟职员的角度看,类加载器可以分别得更过细一些:

    • 扩展类加载器(Extension ClassLoader):该类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现,负责将 <JRE_HOME>/lib/ext 大概被 java.ext.dir 体系变量所指定路径中的全部类库加载到内存中,比方 swing 系列、内置的 js 引擎、xml 分析器等以 javax 开头的扩展类库都是由扩展类加载器加载,开辟者可以直接利用扩展类加载器。
    • 应用步调类加载器(Application ClassLoader):该类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此也被称为体系类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,好比我们本身编写的自界说类或第三方 jar 包。开辟者可以直接利用这个类加载器,如果应用步调中没有自界说过本身的类加载器,一样平常情况下这个就是步调中默认的类加载器。
    (三)什么情况下须要自界说类加载器?


    • 隔离加载类。在某些框架内举行中央件与应用的模块之间举行隔离,把类加载到差异的情况。
    • 修改类加载方式。
    • 扩展加载源。好比从数据库、网络、电视机顶盒举行类加载。
    • 防止源码走漏。好比编译时字节码举行加密,须要通过自界说类加载器对字节码举行解密还原。
    六、双亲委派模子

    应用步调是由三种类加载器相互共同,从而实现类加载,除此之外还可以到场本身界说的类加载器。类加载器之间的条理关系,称为双亲委派模子(Parents Delegation Model)。该模子要求除了顶层的启动类加载器外,别的的类加载器都要有本身的父类加载器。这里的父子关系一样平常通过组合关系(Composition)来实现,而不是继续关系(Inheritance)。
    (一)双亲委派工作机制

    一个类加载器起首将类加载哀求转发到父类加载器,只有当父类加载器无法完成时才实验本身加载。
    (二)双亲委派的作用


    • 每个类只会加载一次,办理了各个类加载器加载根本类的同一标题(根本类库由上层的加载器举行加载)。
    • 防止恶意粉碎的类加载,内存中不会出现多份同样的字节码的体系类,包管 Java 步调安全稳固运行。
    比方:java.lang.Object 存放在 rt.jar 中,如果编写别的一个 java.lang.Object 并放到 ClassPath 中,步调可以


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表