JVM- 浅谈Java 类加载过程

基本概念

Java 类加载过程是 Java 虚拟机(JVM)运行 Java 程序时的重要组成部分。这个过程主要包括以下几个阶段:

  1. 加载(Loading):

    • 在这个阶段,JVM 通过类的全限定名来获取此类的二进制字节流。
    • 加载的数据来源可以是 .class 文件、网络、其他文件系统,甚至是动态生成的字节码。
    • 加载后,数据被转换为方法区内的数据结构(比如类型信息、常量池、方法数据等)。
    • 创建一个代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口。
  2. 链接(Linking):

    • 验证(Verification):确保被加载的类满足 JVM 规范,没有安全问题。
    • 准备(Preparation):为类变量(静态变量)分配内存,并设置默认初始值,这些变量所使用的内存在方法区中进行分配。
    • 解析(Resolution):将类、接口、字段和方法的符号引用转换为直接引用。
  3. 初始化(Initialization):

    • 这个阶段是执行类构造器 () 方法的过程。
    • () 方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并而成。
    • 初始化阶段是执行这个类构造器的过程。
  4. 使用(Using):

    • 在完成了初始化后,类就可以被使用了,比如创建实例、调用类的静态方法等。
  5. 卸载(Unloading):

    • 当类不再被需要时,它会被卸载。卸载条件通常是该类的 Class 对象没有任何引用,且其类加载器的实例也被垃圾回收。
    • 卸载后,该类所占用的资源会被释放。

这个过程是 Java 程序运行的基础,确保了 Java 程序可以在不同的环境下以相同的方式执行,同时也支持了 Java 强大的动态性能。

加载(Loading)阶段

Java 类加载(Loading)阶段是 Java 类加载过程中的第一阶段,其主要任务是将类的二进制数据从不同的数据源转换为方法区内的数据结构,并在堆中创建一个 java.lang.Class 对象来封装这些数据结构。这一阶段的详细过程如下:

1. 类的定位

  • 查找类的二进制数据:类加载器首先需要找到类的二进制表示。这通常涉及到从文件系统中读取 .class 文件,但也可以从其他源如网络、ZIP 文件、运行时生成的数据等地方获取。
  • 类加载器:Java 使用了委托模型来加载类。通常,类加载器会首先请求父类加载器加载类。如果父类加载器无法找到或加载该类,子类加载器才会尝试加载。

2. 类的读取

  • 读取二进制数据:类加载器将从其源中读取类的二进制数据。这些数据包括 Java 类的各种组成部分,如方法、字段和其他关联的元数据。

3. 解析为内部结构

  • 转换为方法区的表示:读取进来的二进制数据被转换成方法区中的内部数据结构。这包括运行时常量池、字段和方法数据。
  • 运行时常量池:它是方法区的一部分,用于存储编译期生成的各种字面量和符号引用,这部分内容将在后续的链接阶段被使用。

4. 创建 java.lang.Class 对象

  • 在堆中创建 Class 对象:JVM 在堆中创建一个 java.lang.Class 对象,代表刚刚加载的类。这个对象作为程序访问方法区内类定义数据的入口,同时也被用于反射操作。
  • 关联引用:这个 Class 对象包含了指向方法区内类定义数据的引用,如对运行时常量池的引用等。

5. 链接到其他类和接口

  • 在类加载过程中,如果发现当前类有引用到其他类或接口(例如继承或实现),那么这些类或接口也将被加载。

6. 安全检查

  • 验证格式:检查加载的类或接口的格式是否符合 JVM 规范。
  • 安全性检查:如果启用了安全管理器,这一阶段还可能涉及安全性检查。

总结

类的加载是一个相对复杂的过程,涉及查找、读取、解析类数据,并在 JVM 内部创建相应的表示。这个过程是整个类生命周期中最初的阶段,为后续的链接、初始化等阶段奠定基础。由于 JVM 规范对类加载器的实现没有严格限制,因此不同的 JVM 实现或不同的类加载器可能在具体实现上有所差异。

链接(Linking)阶段

Java 类加载过程中的链接(Linking)阶段紧随类的加载(Loading)阶段,主要负责将已加载的类或接口的二进制数据合并到 Java 虚拟机的运行时状态中。链接阶段包括三个子阶段:验证(Verification)、准备(Preparation)和解析(Resolution)。这些阶段确保了加载的类在逻辑上正确,且与 JVM 的内存结构兼容。

1. 验证(Verification)

验证是确保加载的类或接口遵循 Java 虚拟机规范,并且不会对 JVM 的安全造成威胁的过程。

  • 文件格式验证:检查加载的类或接口是否具有正确的内部结构(例如正确的魔数、版本号)。
  • 元数据验证:检查类或接口的元数据是否符合 Java 语言规范,如数据类型、方法签名。
  • 字节码验证:进行更深入的检查,确保代码遵循逻辑约束,比如控制流、变量初始化、方法调用等的正确性。
  • 符号引用验证:确保符号引用指向正确的对象。

2. 准备(Preparation)

准备阶段负责为类中的静态变量分配内存,并设置类变量的初始默认值。

  • 内存分配:在方法区中为类变量(也称为静态字段)分配内存。这不包括实例变量,实例变量会在对象实例化时随对象一起分配在堆中。
  • 默认初始化:静态变量在准备阶段会被初始化为默认值,例如 int 类型的默认值为 0,引用类型的默认值为 null。这些默认值通常不等同于在 Java 代码中指定的值。

3. 解析(Resolution)

解析阶段是将类、接口、字段和方法的符号引用转换为直接引用的过程。

  • 符号引用:这是一种抽象的引用,用类的全限定名、字段的名称和描述符等表示。
  • 直接引用:这是一种具体的引用,直接指向目标的内存地址或者是能间接定位到目标的指针。直接引用在类型解析后生成。
  • 解析动作:包括类和接口的解析、字段解析、类方法解析、接口方法解析等。这个过程可能涉及到加载其他未被加载的类。

总结

链接阶段是类加载过程的重要组成部分,它确保了类或接口被正确地整合到 JVM 的内部结构中。通过验证、准备和解析这三个子阶段,JVM 确保了代码的安全性和稳定性,同时为后续的初始化阶段做好准备。这个过程对于保持 Java 应用的健壮性和跨平台功能至关重要。不同的 JVM 实现可能在细节上有所差异,但大体流程是一致的。

初始化(Initialization)阶段

Java 类的初始化(Initialization)阶段是类加载过程的一个重要环节。在此阶段,Java 虚拟机(JVM)负责执行类构造器 () 方法,该方法由编译器自动合成,用于初始化类变量和执行静态代码块。以下是初始化阶段的详细介绍:

1. 触发条件

类的初始化阶段会在满足以下任一条件时触发:

  • 当创建一个类的实例时(例如,使用 new 关键字)。
  • 当访问一个类的静态方法或静态字段时(除了使用 finalstatic 定义的常量字段,因为它们在编译时就已被解析)。
  • 当使用 java.lang.reflect 包的方法对类进行反射调用时。
  • 当初始化一个类的派生类时(首先需要初始化其父类)。
  • 当虚拟机启动时,用户指定的主类(包含 main() 方法的类)被初始化。

2. () 方法

  • 合成过程:编译器自动收集类中的所有静态变量赋值动作和静态语句块(static {} 块),按照这些语句在源文件中的顺序合成 () 方法。
  • 执行:该方法不需要显式调用,由 JVM 在类初始化时自动执行。

3. 初始化过程

  • 单线程:初始化一个类包括执行该类的 () 方法。JVM 保证这个方法在多线程环境中被安全地执行,即同一类的 () 方法在多个线程中只会被执行一次。
  • 父类初始化:如果一个类有父类,JVM 会先初始化其父类,除非父类已经被初始化。
  • 执行顺序:静态变量的赋值和静态代码块的执行顺序严格遵循它们在类中定义的顺序。

4. 特殊情况

  • 接口初始化:当一个接口中定义了静态字段(final 且 static),且这个字段被用到时,会触发该接口的初始化。
  • 未被使用的类:如果一个类没有被使用,那么它可能永远不会被初始化。

5. 错误处理

  • 异常:如果在初始化过程中发生了异常,并且没有被捕获,那么后续尝试初始化这个类的行为将会被 JVM 标记为错误,并抛出 java.lang.NoClassDefFoundError 或类似的错误。

总结

初始化阶段是类加载过程的关键部分,负责执行类构造器 () 方法,以初始化类变量和执行静态代码块。JVM 通过精确控制和同步,确保类在多线程环境下安全地进行初始化。初始化阶段是类生命周期中非常重要的一环,它为类的后续使用做好了准备。

使用(Using)和卸载(Unloading)

在 Java 类加载的生命周期中,使用(Using)和卸载(Unloading)是最后两个阶段。它们标志着类在 Java 虚拟机(JVM)中的活跃使用和最终的回收。

使用(Using)

在类被加载、链接、初始化之后,它就处于可使用状态。这个阶段,类的功能可以被完全利用,具体包括:

  1. 创建实例:可以通过 new 关键字创建类的实例。
  2. 访问静态成员:可以访问类的静态方法和静态字段。
  3. 反射操作:可以通过反射机制查询类信息、访问成员、调用方法等。
  4. 实现接口:如果类实现了某个接口,可以通过这个接口引用它的实例。
  5. 继承:可以被其他类继承,除非它是一个 final 类。

这个阶段中,类是完全活跃的,可以被应用程序自由使用。

卸载(Unloading)

卸载是类生命周期的最终阶段。在这个阶段,类由 JVM 从内存中移除。类的卸载发生在以下情况:

  1. 类加载器的实例被回收:如果一个类的类加载器的实例被垃圾回收器回收,那么由该类加载器加载的所有类也会被卸载。
  2. 类的实例和 Class 对象无引用:如果一个类没有任何活跃的实例,且其 java.lang.Class 对象也没有在任何地方被引用,那么 JVM 可以选择卸载这个类。
  3. 卸载的条件:Java 规范并没有强制要求 JVM 必须卸载类,也没有规定具体的卸载时间点。因此,不同的 JVM 实现可能有不同的卸载策略。

卸载后,类的二进制数据和在方法区中的所有结构都会被回收,释放出相应的内存空间。

注意点

  • 类卸载的稀有性:在许多 Java 应用程序和运行时环境中,类的卸载不是一件常见的事情。尤其是对于使用系统类加载器或扩展类加载器加载的类,它们通常会伴随 JVM 的整个生命周期。
  • 持久代和元空间:在早期版本的 JVM 中,类的元数据存储在持久代(PermGen)。在 Java 8 及以后的版本中,持久代被元空间(Metaspace)所替代,元空间的大小可以动态调整,减少了类卸载的需求。

总结来说,使用阶段是类在 JVM 中的活跃期,此时类的各项功能都可被利用。卸载阶段是类生命周期的终结,此时类及其相关资源被回收,但在实际应用中类的卸载并不频繁。这两个阶段标志着类在 JVM 中的生命周期的完整性。

你可能感兴趣的:(Java基础,jvm,java,开发语言)