Java 应用程序的执行生命周期

更新于 2025-12-24

或者说……当你运行一个 Java 程序时,究竟发生了什么?

César Soto Valero 发布于 2022 年 10 月 20 日

如果你正在阅读本文,很可能你已经知道如何编写 Java 代码。这非常好!我认为如今每个人都应该掌握编程(就像每个人都应该了解基本的数学运算,如 +、-、*、/,即使我们都有计算器一样)。

在之前的一篇文章中,我介绍了 Java 代码首先被“编译”为字节码,然后由 JVM 解释并执行。然而,我当时并未解释 JVM 实际上是如何执行字节码的。

本文的目的正是填补这一空白。我将回答这个问题:

当你在最喜欢的 IDE 中点击“执行”按钮时,到底发生了什么?

阅读完本文后,你将理解 Java 应用程序的执行生命周期,以及 JVM 在执行阶段所进行的各项活动。


1. 执行生命周期

Java 应用程序的执行生命周期大致可分为三个阶段:

  1. 编译(Compilation):应用程序的源代码通过 javac 编译器转换为字节码。
  2. 类加载(Class Loading):字节码被加载到内存中,并准备好必要的类文件以供执行。
  3. 字节码执行(Bytecode Execution):JVM 执行字节码,程序开始运行。

JVM 负责管理最后这个阶段,包括:

  • 加载字节码
  • 分配内存
  • 将字节码转换为本地机器码

换句话说,JVM 负责将字节码翻译成特定目标平台的机器码并执行它。这是一个复杂的过程,因为每种微处理器架构(如 x86、ARM、MIPS、PowerPC 等)都“理解”不同的指令集。

此外,JVM 还提供运行时服务,例如内存管理、线程同步和异常处理。

本文重点聚焦于字节码执行阶段。下图的活动图展示了该阶段发生的主要流程:

flowchart TB a(["开始执行"]) --> b["加载"] b --> c["链接"] c --> d["初始化"] d --> e["实例化"] e --> f["终结"] f --> q{是否卸载?} q -- 是 --> x(["程序退出"]) q -- 否 --> d

以下各节将详细说明字节码执行阶段中每个活动的具体内容。


1.1 加载(Loading)

加载是指根据特定名称查找类或接口的二进制形式(即 class 文件格式),并从中构造出一个 Class 对象的过程。

JVM 使用 ClassLoader 来查找 Main 类的二进制表示。ClassLoader 类及其子类实现了加载过程,其中 defineClass 方法用于从 class 文件的二进制数据构造 Class 对象。

JVM 提供两类内置类加载器:

  • 引导类加载器(Bootstrap Class Loader):加载来自 rt.jar 的核心 Java 类。
  • 扩展类加载器(Extension Class Loader):从 ext 目录加载类。

此外,还可以使用应用程序类加载器从其他位置(如 classpath 或远程服务器)加载类。这些通常是 ClassLoader 的自定义子类,可通过 java.lang.Class 实例动态加载类。

下面是一个自定义类加载器的示例:

public class CustomClassLoader extends ClassLoader {
    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }

    // 根据类名加载类的方法
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (!name.startsWith("com.example")) {
            // 若不是 com.example 开头,则委托给父加载器
            return super.loadClass(name);
        }

        // 构造文件名
        String fileName = name.substring(name.lastIndexOf('.') + 1) + ".class";

        // 尝试打开输入流
        InputStream inputStream = getClass().getResourceAsStream(fileName);
        if (inputStream == null) {
            throw new ClassNotFoundException();
        }

        try {
            byte[] bytes = new byte[inputStream.available()];
            inputStream.read(bytes);
            // 使用 defineClass 定义类
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }
    }
}

该类继承 ClassLoader 并重写了 loadClass 方法:

  • 如果类名不以 com.example 开头,就委托给父加载器;
  • 否则,尝试从资源路径加载 .class 文件,读取字节并调用 defineClass

总结:类加载过程执行以下三项功能:

  1. 从 class 文件创建二进制数据流
  2. 按照内部数据结构解析该二进制数据
  3. 创建 java.lang.Class 的实例

完成后,该 Class 实例即可进入链接阶段。


1.2 链接(Linking)

链接是指将类或接口的二进制形式整合到 JVM 的运行时状态中,使其可以被执行。链接包含三个步骤:

1. 验证(Verification)

检查加载的类表示是否结构良好,符号表是否正确。同时验证类的字节码是否符合 Java 语言和 JVM 的语义要求,例如:

  • 每条指令的操作码是否有效
  • 所有跳转指令是否指向另一条指令的起始位置(而非中间)
  • 每个方法是否有正确的签名

2. 准备(Preparation)

为类或接口创建静态字段(类变量和常量),并将其初始化为默认值(如 int 默认为 0)。此过程会分配静态存储空间,并构建 JVM 内部所需的数据结构(如方法表[^1])。

3. 解析(Resolution,可选)

解析类中对其他类或接口的符号引用。通过加载被引用的类并验证引用是否合法来完成。

对比 C 语言:在简单的 C 语言静态链接中,编译后的程序(如 a.out)包含所有已完全链接的库函数副本。
而在 Java 中,符号引用采用惰性解析(lazy resolution)——仅在实际使用时才解析。例如,如果一个类包含多个对另一个类的引用,这些引用可能逐个解析,甚至可能完全不解析(如果程序从未使用它们)。

总结:链接包含三个阶段

  • 验证
  • 准备
  • 解析(可选)

完成后,类即可进入初始化阶段。


1.3 初始化(Initializing)

类的初始化是指执行其静态初始化器静态字段的初始化表达式。这些代码按其在源文件中出现的顺序依次执行。

考虑以下示例:

class Main extends Object {
    static int x = 1;          // 静态字段初始化器(先执行)
    static int y;              // 无初始化器

    static {                   // 静态初始化块(其次执行)
        y = x + 1;
    }

    static int z = x + y;      // 静态字段初始化器(最后执行)

    public static void main(String[] args) {
        // main 方法在类初始化完成后才执行
    }
}

当 JVM 初始化 Main 类时:

  1. 首先递归初始化其所有超类(从 Object 开始)
  2. 然后按源码顺序执行 Main 中的静态字段初始化器和静态块

重要规则:在 Java 中,超类总是在子类之前初始化

触发类 T 初始化的常见情形包括:

  • 创建 T 的实例
  • 调用 T 的静态方法
  • 给 T 的静态字段赋值
  • 使用 T 的非常量静态字段
  • 通过反射调用 T 的方法

一旦所有类完成初始化,JVM 就会开始实例化对象。


1.4 实例化(Instantiating)

显式创建新实例通常通过 new 表达式完成,例如:

Point magicPoint = new Point(42, 42);

但实例也可能隐式创建,例如:

  • 加载包含字符串字面量或文本块的类时,可能创建新的 String 对象
  • 执行装箱转换(如 intInteger)时,可能创建包装类对象
  • 字符串拼接操作可能创建新的 String 对象
  • 方法引用或 Lambda 表达式求值可能创建函数式接口的实例

实例化过程中执行以下步骤:

  1. 在堆上分配内存以容纳新对象
  2. 调用类的构造函数初始化对象
  3. 返回新对象的引用

1.5 终结(Finalizing)

终结是指在对象被垃圾回收前清理其所持有的资源。Object 类定义了一个 finalize() 方法,垃圾回收器在回收对象前会调用它。

子类可重写 finalize() 以执行必要的清理操作。例如:

public class TempFile {
    private File file;

    public TempFile(String filename) {
        file = new File(filename);
    }

    @Override
    protected void finalize() throws Throwable {
        file.delete();          // 删除临时文件
        super.finalize();
    }
}

⚠️ 注意finalize() 不保证会被调用,不应依赖它执行关键任务。它仅作为对象回收前的辅助清理机制。


1.6 卸载(Unloading)

卸载是指从 JVM 运行时状态中移除类或接口(例如,当定义该类的类加载器被垃圾回收时)。类卸载可减少内存占用,因此主要对动态加载大量类并在之后不再使用的应用有意义。

以下示例演示了用户定义的垃圾回收场景:

import java.lang.ref.WeakReference;

public class LargeClass {
    private int[] data = new int[Integer.MAX_VALUE]; // 占用大量内存

    public static void main(String[] args) {
        LargeClass largeObject = new LargeClass();
        WeakReference<LargeClass> weakRef = new WeakReference<>(largeObject);
        largeObject = null; // 不再强引用

        System.gc(); // 建议执行 GC

        if (weakRef.get() == null) {
            System.out.println("LargeClass 对象已被回收");
        } else {
            System.out.println("LargeClass 对象未被回收");
        }
    }
}

注意:由引导类加载器加载的类永远不会被卸载。因此,在典型的独立应用中,由于系统类加载器在整个程序生命周期内都处于活跃状态,类卸载并不常见。

与对象垃圾回收不同,类卸载指的是移除类的定义(即 Class 对象)及其元数据。这通常发生在加载该类的类加载器本身被垃圾回收时。

以下示例更可能触发类卸载:

import java.lang.ref.WeakReference;
import java.net.URL;
import java.net.URLClassLoader;

public class ClassUnloadingExample {
    public static void main(String[] args) throws Exception {
        URL classUrl = new URL("file:///path/to/LargeClass.class");
        URLClassLoader customClassLoader = new URLClassLoader(new URL[]{classUrl});

        Class<?> largeClass = Class.forName("LargeClass", true, customClassLoader);

        WeakReference<ClassLoader> weakClassLoaderRef = new WeakReference<>(customClassLoader);

        customClassLoader = null;
        largeClass = null;

        System.gc();
        Thread.sleep(1000); // 等待 GC

        if (weakClassLoaderRef.get() == null) {
            System.out.println("自定义类加载器已被回收,表明 LargeClass 可能已被卸载");
        } else {
            System.out.println("自定义类加载器仍在内存中");
        }
    }
}

在此例中,通过自定义 URLClassLoader 加载类,随后清除所有强引用。若该加载器被回收,则其加载的类很可能也被卸载[^3]。


1.7 程序退出(Program Exit)

程序退出指终止程序的执行。这发生在:

  • 所有非守护线程终止,
  • 某线程调用了 Runtime.exit() 方法

exit() 会停止 JVM 并返回指定的退出码。但若存在安全管理器且不允许退出,则会抛出 SecurityException


2. 结论

本文深入探讨了 Java 应用程序的执行生命周期。正如所见,在 main 方法真正执行之前,JVM 已经完成了大量复杂的工作——从类加载、链接、初始化,到实例化、终结,甚至可能的卸载。

理解这些机制对开发者至关重要,它有助于我们:

  • 更深入地掌握 JVM 工作原理
  • 编写出更高效、更可靠的 Java 应用

希望你喜欢这篇文章,并有所收获 😎。


3. 参考资料