Java 类加载机制解密一探到底

news/2024/7/21 18:00:16/文章来源:https://blog.csdn.net/lizhong2008/article/details/139253243

类加载是 Java 程序在运行期执行之前的重要环节,它决定着程序的运行效率和稳定性。本文将为您深入剖析 Java 类加载机制的整个生命周期,揭开神秘面纱,让您彻底掌握这一核心知识点。


一、类的生命周期概述


类的生命周期在Java中指的是从类被加载到虚拟机内存中,到最终被卸载的整个过程。包括以下5个阶段:加载、验证、准备、解析和初始化。其中加载、验证、准备、初始化这4个阶段的顺序是确定的,只有解析阶段在特定情况下可以在初始化之后再开始。


在这里插入图片描述


1、加载(Loading)

加载是类生命周期的开始阶段。在这个阶段,Java虚拟机(JVM)会通过类的全名(包括包名和类名)来找到这个类的.class文件,并把这个类加载到内存中。这个过程涉及到了类文件的查找和读取。


2、验证(Verification)

在验证阶段,JVM确保加载的.class文件符合JVM规范,没有损坏,并且不会对JVM造成安全威胁。验证包括文件格式验证、元数据验证、字节码验证和符号引用验证。

3、准备(Preparation)

准备阶段是为类的静态变量分配内存并设置初始值的过程。初始值通常是数据类型的默认值,例如,对于int类型是0,对于引用类型是null。


4、解析(Resolution)

解析是JVM将常量池中的符号引用替换为直接引用的过程。符号引用是一组符号来描述目标,而直接引用则是直接指向目标的指针、相对偏移量或是一个能直接定位到目标的句柄。


5、初始化(Initialization)

初始化是执行类构造器()方法的过程。这个特殊的方法是由编译器自动收集类中的所有静态变量的赋值动作和静态代码块(static{}块)中的语句合并产生的。当某个类被主动使用时,()方法会被执行。

下面是一个简单的Java类,演示了类的生命周期:

// MyClass.java
public class MyClass {static {// 静态代码块System.out.println("MyClass static block");}public MyClass() {// 构造器System.out.println("MyClass constructor");}public static void main(String[] args) {System.out.println("MyClass is being loaded");new MyClass(); // 触发类的初始化}static int value = 10; // 静态变量
}

执行过程解释

  1. 加载:当执行java MyClass命令时,JVM开始加载MyClass类。
  2. 验证:JVM验证MyClass.class文件。
  3. 准备:为静态变量value分配内存,并设置初始值0(int类型的默认值)。
  4. 解析:如果有符号引用,JVM会将它们解析为直接引用。
  5. 初始化:执行<clinit>()方法,打印"MyClass static block",然后执行main方法中的代码,打印"MyClass is being loaded",接着创建MyClass实例,触发构造器的执行,打印"MyClass constructor"。

这个简单的示例演示了类从加载到初始化的完整生命周期。在实际应用中,类的生命周期可能更复杂,涉及到类的继承、接口实现等其他因素。


二、加载过程的三大任务


Java 类加载过程的三大任务是类加载机制的核心组成部分,下面我将详细解释这三大任务:


1、通过类全限定名获取定义此类的二进制字节流

通过类全限定名获取定义此类的二进制字节流,是类加载过程的第一步。

在这个任务中,Java虚拟机(JVM)需要确定类的名字(即全限定名,包括包名和类名),然后通过某种机制(如文件系统、网络、类路径等)获取到这个类的二进制字节流。这个过程可能涉及到查找.class文件、从JAR包中提取.class文件,或者通过其他自定义类加载器来获取字节流。

下面通过一个简单的示例来演示这个过程。

假设我们有一个简单的Java类MyClass,它位于com.example包中。


步骤1: 创建Java类文件

首先,我们创建MyClass.java文件,并编写如下代码:

// MyClass.java
package com.example;public class MyClass {public void sayHello() {System.out.println("Hello from MyClass!");}
}

步骤2: 编译Java类文件

使用javac命令编译这个类文件:

javac com/example/MyClass.java

这将在com/example目录下生成MyClass.class文件。


步骤3: 创建自定义类加载器

接下来,我们创建一个自定义类加载器,继承自java.lang.ClassLoader

// MyClassLoader.java
import java.io.*;public class MyClassLoader extends ClassLoader {private String path;public MyClassLoader(String path) {this.path = path;}@Overridepublic Class<?> findClass(String name) throws ClassNotFoundException {try {String fileName = path + name.replace('.', '/') + ".class";File file = new File(fileName);if (!file.exists()) {throw new ClassNotFoundException();}byte[] bytes = new byte[(int) file.length()];FileInputStream fis = new FileInputStream(file);DataInputStream dis = new DataInputStream(fis);dis.readFully(bytes);dis.close();return defineClass(name, bytes, 0, bytes.length);} catch (IOException e) {throw new ClassNotFoundException("Could not find class: " + name, e);}}
}

步骤4: 使用自定义类加载器加载类

最后,我们编写一个测试类来使用自定义类加载器加载MyClass

// TestClassLoader.java
public class TestClassLoader {public static void main(String[] args) {try {// 指定类文件所在的路径String path = "com/example/";// 创建自定义类加载器实例MyClassLoader classLoader = new MyClassLoader(path);// 通过类加载器加载MyClass类Class<?> myClass = classLoader.findClass("com.example.MyClass");// 创建MyClass的实例Object obj = myClass.newInstance();// 调用MyClass中的方法myClass.getMethod("sayHello").invoke(obj);} catch (Exception e) {e.printStackTrace();}}
}

步骤5: 编译和运行

编译MyClassLoader.javaTestClassLoader.java

javac MyClassLoader.java
javac TestClassLoader.java

运行TestClassLoader

java TestClassLoader

输出应该是:

Hello from MyClass!

这个示例演示了如何通过自定义类加载器,根据类的全限定名获取定义此类的二进制字节流,并最终加载和使用这个类。自定义类加载器允许你控制类的加载过程,这在需要动态加载类或者需要从非标准位置加载类时非常有用。


2、将静态存储结构转化为方法区数据结构

在获取到类的二进制字节流之后,JVM需要将这些静态的字节码转换成运行时的数据结构,这些数据结构存储在方法区(Method Area)。

方法区是JVM内存模型的一部分,用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。在这个任务中,JVM会进行以下操作:

  • 验证:确保字节流符合JVM规范,没有安全问题。
  • 准备:为类的静态变量分配内存,并设置默认初始值。
  • 解析:将字节码中的符号引用转换为直接引用,例如,将类、接口、字段和方法的符号引用转换为指向运行时内存中的直接指针。

这个过程是自动进行的,由JVM在类加载时处理,因此,开发者通常不需要手动进行这些操作。

不过,为了演示这个过程,我们可以创建一个简单的Java类,并在代码中展示静态变量的默认初始值是如何被设置的。然后,我们将通过反编译工具来查看JVM是如何将这些静态变量转换为运行时数据结构的。

首先,创建一个简单的Java类StaticInitialization,其中包含一些静态变量:

// StaticInitialization.java
public class StaticInitialization {public static int staticInt;public static double staticDouble;public static String staticString;static {// 静态初始化块,可以在这里给静态变量赋值staticInt = 100;staticString = "Initialized";}
}

步骤1: 编译Java类文件

使用javac命令编译这个类文件:

javac StaticInitialization.java

这将在当前目录下生成StaticInitialization.class文件。


步骤2: 使用反编译工具查看字节码

为了查看JVM是如何将静态存储结构转化为方法区数据结构的,我们可以使用Java字节码反编译工具,如javap

javap -verbose StaticInitialization

这个命令将输出StaticInitialization类的详细信息,包括字段、方法以及字节码指令等。


步骤3: 分析输出结果

javap的输出结果中,我们可以关注以下几个部分:

  • 字段(Fields):这里会列出类中的所有字段,包括静态变量,以及它们的访问标志、名称、描述符和属性。
  • 属性(Attributes):在字段属性中,可能会有ConstantValue属性,它表示静态变量的初始值。
  • 字节码指令:在<clinit>()方法的字节码中,可以看到静态初始化块中的操作,包括静态变量的赋值。

示例输出分析

假设javap的输出结果如下:

Compiled from "StaticInitialization.java"
public class StaticInitialization {public static int staticInt;// 静态变量staticInt的初始值是0(int类型的默认值)// access flags: public, static// descriptor: I// attributes:ConstantValue: int 0public static double staticDouble;// 静态变量staticDouble的初始值是0.0(double类型的默认值)// access flags: public, static// descriptor: D// attributes:ConstantValue: double 0.0public static java.lang.String staticString;// 静态变量staticString的初始值是null(引用类型的默认值)// access flags: public, static// descriptor: Ljava/lang/String;// attributes:ConstantValue: <null>static {};descriptor: ()Vflags: ACC_STATICCode:stack=2, locals=0, args_size=00: bipush        1002: ldc           #2                  // String Initialized4: putstatic     #1                  // Field staticString: Ljava/lang/String;7: return// 静态初始化块:给staticInt和staticString赋值...
}

从输出中可以看到:

  • staticIntstaticDoublestaticString字段都有默认的初始值。
  • <clinit>()方法中,JVM执行静态初始化块的代码,给staticIntstaticString赋予了新的值。

这个示例展示了JVM如何自动将静态存储结构(即类文件中的静态变量定义)转化为方法区数据结构,并在类加载的准备阶段为静态变量设置初始值。开发者通常不需要手动干预这个过程,但了解这个过程有助于更好地理解Java的类加载机制。


3、在Java堆生成类的java.lang.Class对象

最后一个任务是在Java堆中创建一个java.lang.Class对象,这个对象是类在Java程序中的一个表示。这个Class对象包含了类的完整结构信息,如类的名称、访问修饰符、字段、方法、构造函数、父类等。Java堆是JVM内存模型中用于存储对象实例和数组的部分。

创建Class对象的过程包括:

  • 初始化:执行类构造器<clinit>()方法,这是由编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并产生的。
  • 链接:将Class对象链接到JVM的运行时环境中,使其可以被Java程序访问。

下面通过一个简单的Java类来演示Class对象的生成过程。

步骤1: 创建Java类

首先,我们创建一个简单的Java类ExampleClass

// ExampleClass.java
public class ExampleClass {private String name;public ExampleClass(String name) {this.name = name;}public void sayHello() {System.out.println("Hello, my name is " + name);}public static void main(String[] args) {ExampleClass example = new ExampleClass("Kimi");example.sayHello();}
}

步骤2: 编译Java类文件

使用javac命令编译这个类文件:

javac ExampleClass.java

这将在当前目录下生成ExampleClass.class文件。


步骤3: 使用反射获取Class对象

接下来,我们编写一个测试类来使用Java反射API获取ExampleClassClass对象:

// TestClassObject.java
public class TestClassObject {public static void main(String[] args) {try {// 获取ExampleClass的Class对象Class<?> exampleClass = Class.forName("ExampleClass");// 使用Class对象创建ExampleClass的实例Object exampleInstance = exampleClass.newInstance();// 调用ExampleClass实例的方法exampleClass.getMethod("sayHello").invoke(exampleInstance);} catch (Exception e) {e.printStackTrace();}}
}

步骤4: 编译和运行

编译TestClassObject.java

javac TestClassObject.java

运行TestClassObject

java TestClassObject

输出应该是:

Hello, my name is Kimi

解释

TestClassObject类中,我们使用了Class.forName("ExampleClass")方法来获取ExampleClassClass对象。这个方法会触发ExampleClass的加载(如果它还没有被加载的话),并且创建对应的Class对象。

Class<?>Class类的泛型形式,?表示我们不关心这个Class对象所代表的具体类型。

newInstance()方法是Class类的实例方法,它创建了该类的一个新实例。这相当于调用了默认的无参构造函数。

getMethod("sayHello").invoke(exampleInstance)获取了sayHello方法的Method对象,并在创建的实例上调用了这个方法。

这个示例演示了如何通过Java反射API在Java堆中生成类的Class对象,并使用这个对象来创建类的实例和调用方法。这个过程展示了JVM如何在Java堆中为每个加载的类生成Class对象,以及如何利用这些对象进行反射操作。

这三大任务完成后,类就被成功加载到JVM中,可以被Java程序使用了。值得注意的是,类加载是懒加载的,也就是说,只有当类被主动使用时(如通过new关键字实例化对象、访问类的静态成员等),JVM才会开始加载这个类。

示例代码

public class Example {public static void main(String[] args) {System.out.println("Example class is loaded.");}
}

当你运行这个程序时,JVM会执行上述三大任务来加载Example类。程序输出"Example class is loaded."时,表示类已经被加载并初始化。


三、验证阶段-确保被加载类的正确性


验证阶段是确保加载的类的正确性,主要包括4种验证:

1、文件格式验证(是否符合Class文件规范)

文件格式验证是Java类加载过程中的一个关键步骤,它确保加载的.class文件是符合Java虚拟机规范的,没有损坏,并且不会对JVM造成安全威胁。


(1)、文件格式验证主要检查内容
  • 魔数:每个.class文件的前四个字节被称为魔数,必须为0xCAFEBABE,用于标识这是一个有效的Java类文件。
  • 版本号:紧接着魔数的四个字节表示JDK的版本号,JVM需要检查这个版本号是否支持。
  • 常量池:常量池中的常量是否有正确的结构,并且是否指向了不存在的常量类型。
  • 字段和方法:字段和方法的访问标志是否合法,以及它们的名称和描述符是否符合规范。
  • 代码:如果类中包含方法,那么方法的字节码是否合法,是否有正确的操作码和操作数等。

为了演示文件格式验证,我们将创建一个简单的Java类,然后故意修改它的字节码,以触发验证错误。


(2)、案例演示:文件格式验证

步骤1: 创建Java类

首先,我们创建一个简单的Java类ValidClass

// ValidClass.java
public class ValidClass {public void sayHello() {System.out.println("Hello, World!");}
}

步骤2: 编译Java类文件

使用javac命令编译这个类文件:

javac ValidClass.java

这将在当前目录下生成ValidClass.class文件。


步骤3: 故意损坏.class文件

为了演示验证过程,我们将使用十六进制编辑器打开ValidClass.class文件,并故意修改魔数,例如将其改为CAFED00D

hexedit ValidClass.class

在编辑器中,找到文件开头的四个字节,将它们修改为CAFED00D,然后保存文件。


步骤4: 尝试加载损坏的.class文件

现在,我们尝试加载修改后的ValidClass.class文件:

// TestLoadValidClass.java
public class TestLoadValidClass {public static void main(String[] args) {try {Class<?> clazz = Class.forName("ValidClass");// 如果没有异常,尝试创建对象并调用方法clazz.newInstance().getDeclaredMethod("sayHello").invoke(clazz.newInstance());} catch (Exception e) {e.printStackTrace();}}
}

编译并运行TestLoadValidClass

javac TestLoadValidClass.java
java TestLoadValidClass

预期输出和解释

当你运行修改后的TestLoadValidClass时,应该会抛出一个java.lang.VerifyError或其子类异常,例如java.lang.ClassFormatError,因为JVM在验证阶段检测到了损坏的.class文件。

输出示例:

java.lang.ClassFormatError: Invalid magic number in class file

这个错误表明JVM在文件格式验证阶段检测到了魔数不正确,从而拒绝了加载这个类文件。

通过这个示例,读者可以清楚地理解文件格式验证的重要性以及它是如何工作的。在实际开发中,我们通常不会手动修改.class文件,但了解这个验证过程有助于我们更好地理解JVM的类加载机制,以及在遇到类加载错误时进行调试。


2、元数据验证(对字节码信息进行语义分析)

元数据验证是Java类加载过程中的一个步骤,它发生在文件格式验证之后。元数据验证的目的是确保字节码中的元数据信息是语义上合理的,即符合Java语言规范的要求。这个过程包括对类的字段、方法、接口、继承关系等进行验证。


(1)、元数据验证的主要检查内容
  • 继承关系:确保类没有非法的继承关系,比如一个类不能继承两个具有相同名称的字段或方法的类。
  • 接口实现:如果类声明实现了一个接口,那么它必须提供接口中所有抽象方法的具体实现。
  • 字段和方法的访问:确保类中的字段和方法的访问权限是合法的,比如一个类不能访问另一个类的私有成员。
  • 方法签名:检查方法的签名是否与声明一致,包括方法名、参数列表和返回类型。
  • final类和方法:final类不能被继承,final方法不能被子类覆盖。

为了演示元数据验证,我们将创建一个Java类,其中包含一些违反Java语言规范的元数据,然后尝试加载这个类。


(2)、案例演示:元数据验证

步骤1: 创建违反规范的Java类

首先,我们创建一个违反继承规范的Java类InvalidInheritance

// InvalidInheritance.java
public class InvalidInheritance extends BaseClass1 implements BaseInterface {// 这个类试图继承两个具有相同方法的类和接口
}class BaseClass1 {public void baseMethod() {}
}interface BaseInterface {void baseMethod();
}

步骤2: 编译Java类文件

使用javac命令编译这个类文件:

javac InvalidInheritance.java

由于违反了继承规范,编译器会报错,而不是生成.class文件。


步骤3: 尝试编译并理解错误

尝试编译InvalidInheritance.java时,编译器会输出错误信息,例如:

InvalidInheritance.java:5: 错误: 类 BaseClass1 中的 baseMethod() 与 BaseInterface 中的 baseMethod() 签名相同
class BaseClass1 {^
1 个错误

这个错误表明InvalidInheritance类试图继承一个类和一个接口,它们都声明了一个具有相同签名的方法baseMethod(),这是不允许的。


步骤4: 修改代码以通过元数据验证

为了通过元数据验证,我们需要修改InvalidInheritance类,以解决继承规范的问题。例如,我们可以为BaseInterface中的方法提供一个默认实现:

// InvalidInheritanceFixed.java
public class InvalidInheritanceFixed extends BaseClass1 implements BaseInterface {@Overridepublic void baseMethod() {// 提供具体实现}
}class BaseClass1 {public void baseMethod() {}
}interface BaseInterface {default void baseMethod() {System.out.println("Method implemented in BaseInterface");}
}

现在,BaseInterface提供了一个默认方法实现,InvalidInheritanceFixed类实现了接口中的方法,这样就解决了继承规范的问题。


步骤5: 重新编译并运行

重新编译修改后的类:

javac InvalidInheritanceFixed.java

这次编译应该会成功,因为没有违反元数据验证的规则。


解释

通过这个示例,可以清楚地理解元数据验证的过程和重要性。

在实际开发中,编译器会在编译期间进行元数据验证,确保生成的.class文件符合Java语言规范。

如果违反了规范,编译器会报错,阻止不合规的类文件生成,从而避免了在运行时出现更复杂的问题。


3、字节码验证(通过数据流和控制流分析)

字节码验证是Java类加载过程中的一个关键步骤,主要目的是确保加载的字节码是安全的,不会破坏JVM的稳定运行。字节码验证通过数据流和控制流分析来检查方法的字节码,确保它们符合JVM的执行规范。


(1)、字节码验证的主要检查内容
  • 操作码合法性:确保每个字节码指令(操作码)是有效的,并且有正确的操作数。

  • 类型安全:确保字节码中的类型转换是合法的,比如不能将一个整型(int)赋值给一个对象引用。

  • 控制流完整性:确保程序的控制流(如条件分支、循环等)是合理的,比如确保所有的分支目标都是可到达的指令。

  • 栈深度检查:确保在任何给定时刻,操作数栈的深度与操作码要求的栈深度相匹配。

  • 局部变量表使用:确保对局部变量的访问在其作用域内,并且访问的变量已经被初始化。

为了演示字节码验证,我们将创建一个Java类,其中包含一些违反字节码验证规则的代码,然后尝试加载这个类。


(2)、案例演示:字节码验证

步骤1: 创建违反字节码验证规则的Java类

首先,我们创建一个Java类BytecodeVerificationExample,其中包含一个方法,该方法试图将一个未初始化的局部变量赋值给一个引用类型:

// BytecodeVerificationExample.java
public class BytecodeVerificationExample {public void riskyMethod() {Object obj; // 声明但未初始化obj = new Object(); // 正常初始化Object anotherObj = obj; // 将引用赋值给另一个变量}
}

步骤2: 编译Java类文件

使用javac命令编译这个类文件:

javac BytecodeVerificationExample.java

编译器会生成BytecodeVerificationExample.class文件。


步骤3: 故意触发字节码验证错误

为了触发字节码验证错误,我们需要修改riskyMethod,使其包含非法操作,比如访问一个未初始化的局部变量:

public void riskyMethod() {Object obj; // 声明但未初始化Object anotherObj = obj; // 这里将触发字节码验证错误,因为obj未初始化
}

步骤4: 重新编译并尝试运行

重新编译修改后的类:

javac BytecodeVerificationExample.java

尝试运行BytecodeVerificationExample

java BytecodeVerificationExample

预期输出和解释

当你尝试运行修改后的BytecodeVerificationExample时,JVM会在类加载的验证阶段抛出一个java.lang.VerifyError或其子类异常,例如java.lang.ClassFormatErrorjava.lang.VerifyError,因为JVM在字节码验证阶段检测到了非法操作。


输出示例:

java.lang.VerifyError: (class: BytecodeVerificationExample, method: riskyMethod signature: ()V) Incompatible object types for assignment

这个错误表明JVM在字节码验证阶段检测到了类型不兼容的赋值操作,即试图将一个未初始化的引用赋值给另一个引用。

通过这个示例,可以清楚地理解字节码验证的过程和重要性。

在实际开发中,JVM会自动进行字节码验证,以确保运行的字节码是安全的。如果字节码包含非法操作,JVM将拒绝加载和执行该类,从而防止潜在的安全问题和运行时错误。


4、符号引用验证(能否被正确加载和解析)

符号引用验证是Java类加载过程中的解析阶段的一部分,它发生在元数据验证之后。符号引用是类文件中的一组符号,用于间接引用类、字段、方法等。符号引用验证的目的是确保这些符号引用能够被正确地加载和解析,即它们指向的类、字段、方法等确实存在,并且可以通过全限定名找到。


(1)、符号引用验证的主要检查内容
  • 类、接口、字段和方法的存在性:确保类文件中引用的所有类、接口、字段和方法都存在于JVM中或能够被找到。

  • 访问权限:确保当前类有权限访问被引用的类、字段和方法,比如私有成员不能被不同包中的类访问。

  • 返回类型:对于方法引用,验证返回类型是否与符号引用中指定的一致。

  • 参数类型:对于方法引用,验证参数类型是否与符号引用中指定的一致。

为了演示符号引用验证,我们将创建一个Java类,其中包含对不存在的类或方法的引用,然后尝试加载这个类。


(2)、案例演示:符号引用验证

步骤1: 创建引用不存在类的Java类

首先,我们创建一个Java类SymbolReferenceVerificationExample,其中包含对一个不存在的类的引用:

// SymbolReferenceVerificationExample.java
public class SymbolReferenceVerificationExample {public void useNonExistentClass() {// 试图使用一个不存在的类new NonExistentClass();}
}

步骤2: 编译Java类文件

使用javac命令编译这个类文件:

javac SymbolReferenceVerificationExample.java

编译器会生成SymbolReferenceVerificationExample.class文件,因为符号引用验证是在类加载时由JVM执行的,而不是在编译时。


步骤3: 尝试加载和运行类

尝试运行SymbolReferenceVerificationExample

java SymbolReferenceVerificationExample

由于我们没有定义NonExistentClass类,所以这一步不会成功。


步骤4: 预期输出和解释

当你尝试运行SymbolReferenceVerificationExample时,JVM会在类加载的解析阶段抛出一个java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException异常,因为NonExistentClass无法被找到。

输出示例:

java.lang.NoClassDefFoundError: NonExistentClass

或者

java.lang.ClassNotFoundException: NonExistentClass

这个错误表明JVM在解析阶段检测到了一个无法找到的类引用,即符号引用验证失败。


解释

通过这个示例,可以清楚地理解符号引用验证的过程和重要性。

在实际开发中,如果一个类文件中包含了对不存在的类、字段或方法的引用,JVM将无法解析这些符号引用,并在运行时抛出异常。这有助于确保程序的健壮性,防止因为引用错误而导致的运行时错误。

值得注意的是,符号引用验证是在类加载的解析阶段进行的,而不是在编译时。这意味着即使编译器没有报错,如果运行时环境中缺少必要的类或资源,JVM仍然会抛出异常。


四、准备-为类变量分配内存并初始化

准备阶段是Java类加载过程的第三步,紧跟在验证阶段之后。在这个阶段,JVM为类变量分配内存,并初始化类变量的默认值。类变量,也就是静态变量,是被static关键字声明的变量,它们不属于类的任何特定实例,而是属于类本身。


1、准备阶段的主要任务
  • 内存分配:JVM为类变量在方法区分配内存空间。
  • **默认初始化:**为类变量赋予默认初始值。这些值是根据变量的数据类型决定的:
    • 整数类型byteshortintlong默认初始化为0
    • 浮点类型floatdouble默认初始化为0.0
    • 字符类型char默认初始化为\u0000(即Unicode编码中的空字符)。
    • 布尔类型boolean默认初始化为false
    • 引用类型默认初始化为null

为了演示准备阶段,我们将创建一个Java类,其中包含不同类型的静态变量,并展示它们在准备阶段的初始化。


2、案例演示:准备阶段

步骤1: 创建Java类

首先,我们创建一个Java类ClassInitialization,其中包含各种类型的静态变量:

// ClassInitialization.java
public class ClassInitialization {// 静态变量声明public static int staticInt;public static double staticDouble;public static char staticChar;public static boolean staticBoolean;public static String staticString;public static Object staticObject;public static void main(String[] args) {// 将输出类变量的初始默认值System.out.println("staticInt: " + staticInt); // 预期输出:staticInt: 0System.out.println("staticDouble: " + staticDouble); // 预期输出:staticDouble: 0.0System.out.println("staticChar: " + staticChar); // 预期输出:staticChar: System.out.println("staticBoolean: " + staticBoolean); // 预期输出:staticBoolean: falseSystem.out.println("staticString: " + staticString); // 预期输出:staticString: nullSystem.out.println("staticObject: " + staticObject); // 预期输出:staticObject: null}
}

步骤2: 编译Java类文件

使用javac命令编译这个类文件:

javac ClassInitialization.java

这将在当前目录下生成ClassInitialization.class文件。


步骤3: 运行Java程序

运行编译后的类:

java ClassInitialization

预期输出

程序运行时,将输出每个静态变量的默认初始值:

staticInt: 0
staticDouble: 0.0
staticChar: 
staticBoolean: false
staticString: null
staticObject: null

解释

这个示例展示了在类加载的准备阶段,JVM如何为静态变量分配内存并赋予默认初始值。这个过程是自动的,由JVM在类加载时执行。值得注意的是,尽管静态变量已经赋予了默认值,但此时还未执行静态初始化块(static{}块)中的代码。静态初始化块将在初始化阶段执行,可以覆盖这些默认值。

通过这个示例,可以清楚地理解类加载准备阶段的行为,以及静态变量是如何在没有显式初始化的情况下获得默认值的。


五、解析将类的符号引用转为直接引用


解析阶段会把类中的符号引用转换为直接引用,也就是将虚拟机实例在内存中存储的数据替换为直接引用指针。

解析阶段是Java类加载过程的第四个阶段,它发生在准备阶段之后。在这个阶段中,JVM将类中的符号引用转换为直接引用。符号引用是类文件中的一组符号,用于间接引用类、字段、方法等。直接引用是指向目标的指针、相对偏移量或是一个能够直接定位到目标的句柄。


1、解析的主要任务
  • 类或接口的解析:将类或接口的符号引用转换为直接引用。

  • 字段解析:将字段的符号引用转换为直接引用,并检查字段的访问权限。

  • 类方法解析:将类方法的符号引用转换为直接引用,并检查方法的访问权限。

  • 接口方法解析:将接口方法的符号引用转换为直接引用,因为接口方法默认是public的,所以不需要检查访问权限。


2、案例演示:解析阶段

为了演示解析阶段,我们将创建两个Java类,其中一个类引用了另一个类和它的成员。


步骤1: 创建被引用的Java类

首先,我们创建一个被引用的Java类ReferencedClass

// ReferencedClass.java
public class ReferencedClass {public int field = 123;public void method() {System.out.println("Method in ReferencedClass");}
}

步骤2: 创建引用其他类的Java类

然后,我们创建一个引用ReferencedClass及其成员的类ReferringClass

// ReferringClass.java
public class ReferringClass {public static final Class<?> REFERENCED_CLASS = ReferencedClass.class;public static final int REFERENCED_FIELD = ReferencedClass.field;public static final void REFERENCED_METHOD() {ReferencedClass.method();}public static void main(String[] args) {// 演示直接引用REFERENCED_CLASS.getName(); // 获取并打印类名REFERENCED_FIELD += 100; // 修改字段值REFERENCED_METHOD(); // 调用方法}
}

步骤3: 编译Java类文件

使用javac命令编译这两个类文件:

javac ReferencedClass.java
javac ReferringClass.java

步骤4: 运行Java程序

运行编译后的ReferringClass

java ReferringClass

预期输出

程序运行时,将演示直接引用的使用:

ReferencedClass
223
Method in ReferencedClass

解释

ReferringClass中,我们通过符号引用ReferencedClass.class获取了ReferencedClassClass对象的直接引用,通过ReferencedClass.field获取了字段的直接引用,并通过直接调用ReferencedClass.method()获取了方法的直接引用。

ReferringClass被加载时,JVM会解析这些符号引用,将它们转换为直接引用。这意味着JVM会查找ReferencedClass类,确保它已经被加载,然后将符号引用替换为指向实际类、字段、方法的直接引用。

这个过程是在类加载的解析阶段自动完成的,它确保了所有的引用在程序运行时都是有效的,可以直接访问目标类、字段或方法。通过这个示例,读者可以清楚地理解解析阶段的作用,以及它是如何将符号引用转换为直接引用的。


六、初始化


初始化阶段是Java类加载过程的最后一个阶段,发生在解析阶段之后。在这个阶段,JVM为类的静态变量赋予正确的初始值,并执行类定义中的静态代码块(如果存在的话)。初始化阶段是类加载过程中类从"未初始化"状态转变为"已初始化"状态的关键步骤。


1、初始化阶段的主要任务
  • 静态变量初始化:为静态变量赋予声明时指定的初始值,或者直接赋予静态初始化器中的值。

  • 静态代码块执行:执行类中定义的所有静态代码块(static{}块)。


2、案例演示:初始化阶段

为了演示初始化阶段,我们将创建一个Java类,其中包含静态变量和静态代码块。


步骤1: 创建Java类

首先,我们创建一个Java类ClassInitializationExample,其中包含静态变量、静态初始化器和静态代码块:

// ClassInitializationExample.java
public class ClassInitializationExample {// 静态变量声明public static int staticVar = 10; // 初始值为10// 静态代码块static {// 静态代码块中的代码staticVar = 20; // 静态代码块中修改静态变量的值为20System.out.println("Static block: staticVar is initialized to " + staticVar);}public static void main(String[] args) {// main方法中的代码System.out.println("staticVar in main: " + staticVar);}
}

步骤2: 编译Java类文件

使用javac命令编译这个类文件:

javac ClassInitializationExample.java

这将在当前目录下生成ClassInitializationExample.class文件。


步骤3: 运行Java程序

运行编译后的类:

java ClassInitializationExample

预期输出

程序运行时,将输出静态变量的初始值和静态代码块执行的结果:

Static block: staticVar is initialized to 20
staticVar in main: 20

解释

ClassInitializationExample类中,静态变量staticVar首先被赋予了初始值10。当JVM到达初始化阶段时,它首先执行静态代码块中的代码,将staticVar的值修改为20,并打印出相应的信息。然后,当main方法被执行时,它打印出staticVar的值,此时已经是静态代码块中修改后的值20。

这个示例展示了类加载的初始化阶段如何为静态变量赋予正确的初始值,并执行静态代码块。值得注意的是,静态变量的初始化和静态代码块的执行是在类被主动使用之前完成的,这是Java语言的一个特性,确保了类的静态状态在首次使用时已经完全初始化。通过这个示例,读者可以清楚地理解类加载初始化阶段的行为和重要性。


七、结语

类加载机制是Java程序的基石,理解它的原理将为我们编写高效、安全的程序奠定基础。除了类加载,JVM中还有哪些值得我们深入探索的机制呢?比如性能优化、内存管理、垃圾回收等,它们将在后续文章中为您一一揭晓。敬请期待!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_1052649.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux 网络】网络基础(三)(数据链路层协议:以太网协议、ARP 协议)

一、以太网 两个不同局域网的主机传递数据并不是直接传递的&#xff0c;而是通过路由器 “一跳一跳” 的传递过去。 跨网络传输的本质&#xff1a;由无数个局域网&#xff08;子网&#xff09;转发的结果。 所以&#xff0c;要理解数据跨网络转发原理就要先理解一个局域网中数…

深度解析Nginx配置文件:从全局块到upstream块的探索之旅

Nginx配置文件的简介 在浩瀚的互联网世界中&#xff0c;Nginx就如同一座大型交通枢纽&#xff0c;将访问者的请求精准地引导到正确的服务终点。而这一切&#xff0c;都离不开一个神秘而重要的角色——Nginx配置文件。这个文件&#xff0c;就像是一份详尽的路线图&#xff0c;为…

深度学习模型在OCR中的可解释性问题与提升探讨

摘要&#xff1a; 随着深度学习技术在光学字符识别&#xff08;OCR&#xff09;领域的广泛应用&#xff0c;人们对深度学习模型的可解释性问题日益关注。本文将探讨OCR中深度学习模型的可解释性概念及其作用&#xff0c;以及如何提高可解释性&#xff0c;使其在实际应用中更可…

创新力作 焕新首发丨捷顺科技·捷曜系列智慧停车新品全新上市

2024捷顺科技智慧停车全家族新品全面上市 全新外观、全新特性、全新体验 新控制机、新道闸、新超眸相机... 每款新品都有哪些功能亮点 带您一探究竟

【跟着例子学MySQL】 补充内容 -- 主外键和索引

文章目录 前言回顾主键外键索引未完待续 前言 举例子&#xff0c;是最简单有效的学习方法。本系列文章以一个贯穿始终的场景&#xff0c;结合多个实例讲解MySQL的基本用法。 ❔ 为什么要写这个系列&#xff1f; 模仿是最好的老师&#xff0c;实践是检验成果的方法。本系列以实…

机械臂与Realsense D435 相机的手眼标定ROS包

本教程主要介绍机械臂与 Realsense D435 相机手眼标定的配置及方法。 系统&#xff1a;Ubuntu 20.0.4 ◼ ROS&#xff1a;Noetic ◼ OpenCV 库&#xff1a;OpenCV 4.2.0 ◼ Realsense D435&#xff1a;librealsense sdk&#xff08;2.50.0&#xff09;、realsense-ros 功能包&…

【quarkus系列】构建可执行文件native image

目录 序言为什么选择 Quarkus Native Image&#xff1f;性能优势便捷的云原生部署 搭建项目构建可执行文件方式一&#xff1a;配置GraalVM方式二&#xff1a;容器运行错误示例构建过程分析 创建docker镜像基于可执行文件命令式构建基于dockerfile构建方式一&#xff1a;构建mic…

“提升人工智能大模型智能:策略与挑战“

文章目录 每日一句正能量前言算法创新数据质量与多样性模型架构优化后记 每日一句正能量 失败时可以称为人生财富&#xff0c;成功时可以称为财富人生。 前言 随着人工智能技术的飞速发展&#xff0c;大模型已经成为推动多个领域创新的关键力量。从自然语言处理到图像识别&…

数据集007:垃圾分类数据集(含数据集下载链接)

数据集简介 本数据拥有 训练集&#xff1a;43685张&#xff1b; 验证集&#xff1a;5363张&#xff1b; 测试集&#xff1a;5363张&#xff1b; 总类别数&#xff1a;158类。 部分代码&#xff1a; 定义数据集 class MyDataset(Dataset):def __init__(self, modetrain, …

Redis篇 数据的编码方式和单线程模型

编码方式和单线程模型 一.redis中的数据类型二. Redis中查询编码方式命令三. 单线程模型四. 经典面试题,redis为何这么快?什么是IO多路复用? 一.redis中的数据类型 在redis中,数据类型大致分为5种 1.字符串类型 2.哈希 3.列表 4.集合 5.有序集合 redis底层在实现这些数据结构…

08、SpringBoot 源码分析 - 自动配置深度分析一

SpringBoot 源码分析 - 自动配置深度分析一 refresh和自动配置大致流程如何自动配置SpringBootApplication注解EnableAutoConfiguration注解AutoConfigurationImportSelector自动配置导入选择器DeferredImportSelectorHandler的handleDeferredImportSelectorGroupingHandler的r…

【Qt】Qt框架文件处理精要:API解析与应用实例:QFile

文章目录 前言&#xff1a;1. Qt 文件概述2. 输入输出设备类3. 文件读写类3.1. 打开open3.2. 读read / readline/ readAll3.3. 写write3.4. 关闭close 4. 读写文件示例5. 文件件和目录信息类总结&#xff1a; 前言&#xff1a; 在现代软件开发中&#xff0c;文件操作是应用程序…

【git】开发提交规范(feat、fix、perf)

这段时间收到的需求很多&#xff0c;可能是临近两周一次的大版本灰度上线&#xff0c;这次产生了一个关于git的思考&#xff0c;就是各个版本之间怎么管理的问题&#xff0c;这里做出我自己的一些方法。 首先&#xff0c;既然已经明确了remote分支中的release分支为主分支&…

如何设置远程桌面连接?

远程桌面连接是一种方便快捷的远程访问工具&#xff0c;可以帮助用户在不同地区间快速组建局域网&#xff0c;解决复杂网络环境下的远程连接问题。本文将针对使用远程桌面连接的操作步骤进行详细介绍&#xff0c;以帮助大家快速上手。 步骤一&#xff1a;下载并安装远程桌面连接…

文件上传漏洞:pikachu靶场中的文件上传漏洞通关

目录 1、文件上传漏洞介绍 2、pikachu-client check 3、pikachu-MIME type 4、pikachu-getimagesize 最近在学习文件上传漏洞&#xff0c;这里使用pikachu靶场来对文件上传漏洞进行一个复习练习 废话不多说&#xff0c;开整 1、文件上传漏洞介绍 pikachu靶场是这样介绍文…

前端Vue自定义顶部搜索框:实现热门搜索与历史搜索功能

前端Vue自定义顶部搜索框&#xff1a;实现热门搜索与历史搜索功能 摘要&#xff1a; 随着前端开发复杂性的增加&#xff0c;组件化开发成为了提高效率和降低维护成本的有效手段。本文介绍了一个基于Vue的前端自定义顶部搜索框组件&#xff0c;该组件不仅具备基本的搜索功能&am…

记录一次内存取证

1.情景复现 我姐姐的电脑坏了。我们非常幸运地恢复了这个内存转储。你的工作是从系统中获取她所有的重要文件。根据我们的记忆&#xff0c;我们突然看到一个黑色的窗口弹出&#xff0c;上面有一些正在执行的东西。崩溃发生时&#xff0c;她正试图画一些东西。这就是我们从崩溃…

SQL——SELECT相关的题目(力扣难度等级:简单)

目录 197、上升的温度 577、员工奖金 586、订单最多的客户 596、超过5名学生的课 610、判断三角形 620、有趣的电影 181、超过经理收入的员工 1179、重新格式化部门表&#xff08;行转列&#xff09; 1280、学生参加各科测试的次数 1965、丢失信息的雇员 1068、产品销售分…

微信小程序基础 -- 小程序UI组件(5)

小程序UI组件 1.小程序UI组件概述 开发文档&#xff1a;https://developers.weixin.qq.com/miniprogram/dev/framework/view/component.html 什么是组件&#xff1a; 组件是视图层的基本组成单元。 组件自带一些功能与微信风格一致的样式。 一个组件通常包括 开始标签 和 结…

SEO优化,小白程序员如何做SEO优化流量从0到1

原文链接&#xff1a;SEO优化&#xff0c;小白程序员如何做SEO优化流量从0到1 1、SEO是什么&#xff1f; SEO即&#xff1a;搜索引擎优化(Search Engine Optimization)&#xff0c;是一种通过优化网站结构、内容和外部链接等因素&#xff0c;提高网站在搜索引擎中的自然排名&…