一、Java 编译和 .class 文件生成
Java 源代码文件(.java 文件)在编译之后会生成一个或多个 .class 文件。每个 .class 文件对应一个 Java 类或接口,包含了编译后的字节码。Java 字节码是 JVM 可以理解和执行的指令集,独立于具体的硬件和操作系统。
编译过程通常由 Java 编译器 javac 完成,例如:
javac MyClass.java
这条命令会将 MyClass.java 源文件编译成 MyClass.class 文件。.class 文件是二进制文件,包含了 Java 类的定义信息,包括类的全限定名、字段、方法、常量池、字节码等。
二、JVM 内存模型
在讨论 .class 文件加载位置之前,了解 JVM 的内存模型是必要的。JVM 内存分为多个区域,每个区域用于不同类型的数据和对象:
堆(Heap):存储所有对象实例和数组。堆是 JVM 中最大的内存区域,所有线程共享。
栈(Stack):每个线程独有的内存区域,用于存储局部变量、方法调用信息、操作数栈等。
方法区(Method Area):存储类的结构信息(如类元数据、运行时常量池、静态变量、方法字节码等)。方法区是所有线程共享的区域。方法区在 Java 8 之前被称为“永久代”(PermGen),从 Java 8 开始更名为“元空间”(Metaspace)。
程序计数器(Program Counter Register):每个线程独有的小内存区域,指示当前线程执行的字节码的地址。
本地方法栈(Native Method Stack):与 JVM 栈类似,专用于本地方法(如 JNI 方法)调用时的栈帧。
三、.class 文件的加载过程
在 Java 中,.class 文件的加载过程由 JVM 类加载器(Class Loader)完成。类加载器负责从文件系统、网络、或其他来源获取 .class 文件,将其转换为 JVM 可以理解的数据结构,并存储在方法区。
1. 类加载器(Class Loader)
JVM 使用类加载器来动态加载类。类加载器有多种类型,以下是几种常见的类加载器:
启动类加载器(Bootstrap ClassLoader):用来加载 Java 核心类库,如 java.lang、java.util 包中的类。这个加载器由 JVM 内部实现,通常用 C/C++ 代码编写。扩展类加载器(Extension ClassLoader):用来加载 Java 的扩展类库。通常加载 JAVA_HOME/lib/ext 目录中的类。应用程序类加载器(Application ClassLoader):又称系统类加载器,用来加载应用程序类路径(CLASSPATH)上的类。它是最常用的类加载器,负责加载我们编写的 Java 类。
用户可以自定义类加载器,通过扩展 java.lang.ClassLoader 类来实现,通常用于实现自定义类的加载策略,如从网络加载类、加密类加载等。
2. 类加载过程的五个阶段
类的加载过程可以分为以下五个阶段:
加载(Loading):
类加载器读取 .class 文件的字节码,转换为 JVM 可以识别的数据结构,并将这些数据结构存储在方法区中。同时,在堆中创建一个代表这个类的 java.lang.Class 对象,这个对象封装了类在方法区中的数据结构,并提供访问方法。
验证(Verification):
验证类文件格式是否合法,确保它符合 JVM 规范。验证字节码没有危害性,避免恶意代码的破坏。验证包括文件格式验证、元数据验证、字节码验证和符号引用验证。
准备(Preparation):
为类的静态变量分配内存,并初始化为默认值。注意,此时仅仅是分配内存和设置默认值,而不是初始化为代码中定义的值(这将在初始化阶段进行)。
解析(Resolution):
将常量池中的符号引用(Symbolic References)转换为直接引用(Direct References)。这一步通常是懒加载,即在使用符号引用时才解析为直接引用。
初始化(Initialization):
执行类的静态初始化块(static { ... })和静态变量初始化。类的初始化是线程安全的,JVM 会确保同一个类在多线程环境下只被初始化一次。
四、.class 文件加载到方法区
当一个 .class 文件被 JVM 加载时,它的内容被放置在方法区中。方法区是 JVM 内存模型中的一部分,存储了已加载类的元数据信息。方法区中的内容包括:
类元数据:包含类的全限定名、类的修饰符、父类、接口、字段、方法描述符等。运行时常量池(Runtime Constant Pool):存储类文件中的常量信息,如字面量和符号引用。运行时常量池在类加载时被方法区的常量池存储。方法字节码(Method Bytecode):存储方法的字节码(每个方法都有一个字节码数组)。静态变量:存储类的静态变量。类初始化代码:存储类初始化的代码块(
五、元空间(Metaspace)和永久代(PermGen)的区别
从 Java 8 开始,JVM 用元空间(Metaspace)替代了永久代(PermGen)。这两个区域的主要区别在于:
永久代(PermGen):永久代是 JVM 的一部分,用于存储类的元数据。永久代的内存大小是固定的,容易导致 OutOfMemoryError(OOM)。永久代使用堆内存来存储类的元数据,并且与应用程序的堆内存共享同一个垃圾回收机制。
元空间(Metaspace):元空间也是存储类元数据,但它不使用堆内存,而是直接使用本地内存(Native Memory)。元空间的大小可以根据需要动态调整,因此更有效地避免了永久代中常见的内存问题。元空间使得 JVM 更加灵活和高效。
优点:
动态调整:元空间使用本地内存,大小可以动态调整,不会像永久代一样受到固定大小的限制。减少 OOM:通过将类元数据存储在本地内存中,元空间减少了 OutOfMemoryError 的风险。
六、.class 文件加载示例
我们来详细分析一个简单的 Java 类加载过程示例:
public class MyClass {
static {
System.out.println("Class MyClass is initialized.");
}
private static int staticVar = 10;
public static void display() {
System.out.println("Static Method");
}
}
加载过程分析:
加载:MyClass.class 文件被 JVM 类加载器加载,MyClass 的字节码被读取并存储在方法区。
验证:JVM 验证 MyClass 的字节码是否合法。
准备:JVM 为静态变量 staticVar 分配内存,并初始化为默认值 0(这是准备阶段的默认值初始化)。
解析:符号引用解析为直接引用。
初始化:执行类的静态初始化块,输出 "Class MyClass is initialized.",然后将 staticVar 初始化为 10。
public class Test {
public static void main(String[] args) {
MyClass.display();
}
}
在 Test 类的 main 方法中,第一次调用 MyClass.display(),此时 MyClass 被初始化并加载到方法区。静态初始化块和静态变量的初始化在方法区完成,display 方法的字节码也在方法区中存储。
七、总结
在 Java 中,.class 文件的加载是一个由类加载器管理的复杂过程,它将 .class 文件中的字节码转换为 JVM 可以执行的代码,并将其存储在方法区中。
在 Java 中,编译后的 .class 文件是 Java 字节码文件,它被 Java 虚拟机(JVM)加载和执行。了解 .class 文件的加载位置和过程是掌握 Java 内存模型和 JVM 工作机制的重要一部分。