1. 概述

JVM 在初始化对象实例和类时,使用了两种特殊的内部方法:<init><clinit>
这两个方法由编译器自动生成,并在运行时由 JVM 调用,是理解 Java 初始化机制的关键。

本文将深入剖析这两个“隐藏方法”的作用、生成时机以及它们在字节码层面的表现形式,帮助你避开一些常见的初始化“坑”。


2. 实例初始化方法(

我们从一个简单的对象创建开始:

Object obj = new Object();

编译后通过 javap -c 查看其字节码:

0: new           #2      // class java/lang/Object
3: dup
4: invokespecial #1      // Method java/lang/Object."<init>":()V
7: astore_1

可以看到,new 指令分配内存后,JVM 会调用一个名为 <init> 的特殊方法来完成对象的初始化。

什么是 <init>
在 JVM 规范中,这被称为 实例初始化方法(Instance Initialization Method)。一个方法要成为 <init>,必须满足以下条件:

  • 定义在类中(非接口)
  • 方法名严格为 <init>
  • 返回类型为 void

⚠️ 每个类可以有零个或多个 <init> 方法 —— 它们对应 Java 中的构造器(constructors)。

2.1 构造器与实例初始化块

为了更清楚地看到编译器如何处理构造器和初始化块,来看这个例子:

public class Person {
    
    private String firstName = "Foo"; // 实例变量初始化
    private String lastName = "Bar"; // 实例变量初始化
    
    // 实例初始化块
    {
        System.out.println("Initializing...");
    }

    // 构造函数1
    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    // 构造函数2
    public Person() {
    }
}

使用 javap -c Person 查看字节码(以带参构造器为例):

public Person(java.lang.String, java.lang.String);
  Code:
     0: aload_0
     1: invokespecial #1       // Method java/lang/Object."<init>":()V
     4: aload_0
     5: ldc           #7       // String Foo
     7: putfield      #9       // Field firstName:Ljava/lang/String;
    10: aload_0
    11: ldc           #15      // String Bar
    13: putfield      #17      // Field lastName:Ljava/lang/String;
    16: getstatic     #20      // Field java/lang/System.out:Ljava/io/PrintStream;
    19: ldc           #26      // String Initializing...
    21: invokevirtual #28      // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    24: aload_0
    25: aload_1
    26: putfield      #9       // Field firstName:Ljava/lang/String;
    29: aload_0
    30: aload_2
    31: putfield      #17      // Field lastName:Ljava/lang/String;
    34: return

🔍 关键观察点:

  • 实例变量的默认值赋值(firstName = "Foo")和实例初始化块 { ... } 都被合并到了 <init> 方法中
  • 执行顺序为:
    1. 调用父类 <init>invokespecial 调用 Object.<init>
    2. 执行字段默认初始化
    3. 执行实例初始化块
    4. 执行构造器中的自定义逻辑

也就是说,在字节码层面,构造器代码和实例初始化块是“平级”的,都被打包进同一个 <init> 方法

再看创建实例的调用方字节码:

Person person = new Person("Brian", "Goetz");

对应的字节码为:

0: new           #7        // class Person
3: dup
4: ldc           #9        // String Brian
6: ldc           #11       // String Goetz
8: invokespecial #13       // Method Person."<init>":(Ljava/lang/String;Ljava/lang/String;)V
11: astore_1

这里 invokespecial 调用了匹配签名的 <init> 方法 —— 即 Person(String, String) 对应的那个。

核心结论:

  • Java 中的每一个构造器都会生成一个对应的 <init> 方法
  • 实例初始化块、字段直接赋值、构造器逻辑都会被编译进 <init>
  • <init> 是 JVM 层面的对象初始化入口

3. 类初始化方法(

与实例初始化不同,类级别的静态资源需要在类加载过程中初始化。这时 JVM 使用的是另一个特殊方法:<clinit>

来看一个典型场景:

public class Person {

    private static final Logger LOGGER = LoggerFactory.getLogger(Person.class); // 静态字段初始化

    static {
        System.out.println("Static Initializing...");
    }

    // 其他成员省略
}

编译后,JVM 会生成一个名为 <clinit> 的类初始化方法。

什么是 <clinit>
根据 JVM 规范,满足以下条件的方法即为类初始化方法:

  • 方法名为 <clinit>
  • 返回类型为 void

⚠️ Java 中只有两种方式能触发 <clinit> 的生成:

  • 定义静态字段并赋初始值
  • 使用 static { } 初始化块

执行时机

<clinit> 方法由 JVM 在首次主动使用该类时自动调用一次,常见触发场景包括:

  • 创建该类的实例(new Person()
  • 调用其静态方法
  • 访问其静态字段(非编译期常量)
  • 通过反射加载类
  • 初始化其子类(父类先初始化)

❌ 注意:你不会在任何字节码中看到对 <clinit> 的显式调用指令,因为它是 JVM 内部自动触发的。

例如,当我们执行:

Person person = new Person("Alice", "Smith");

JVM 会在真正执行 <init> 前,确保 <clinit> 已被执行(且仅执行一次)。输出结果会是:

Static Initializing...
Initializing...

这说明:

  1. 类初始化(<clinit>)先于实例初始化(<init>
  2. 静态块只执行一次,无论创建多少个对象

4. 总结

对比项 <init> <clinit>
✅ 作用 初始化对象实例 初始化类本身
✅ 触发时机 每次 new 对象时 类首次被主动使用时
✅ 调用方式 字节码中可见 invokespecial 调用 JVM 自动调用,字节码不可见
✅ 来源 构造器、实例初始化块、实例字段赋值 静态字段初始化、静态块
✅ 执行次数 每次创建实例都执行 整个生命周期只执行一次
✅ 是否可重载 是(对应不同构造器) 否(每个类最多一个)

🎯 关键要点回顾:

  • <init> 是每个对象诞生时必经的“出生仪式”,由构造器等内容构成
  • <clinit> 是类的“成年礼”,只举行一次,负责静态资源初始化
  • 二者均由编译器生成,开发者不可直接调用或重写
  • 理解它们有助于排查类加载、静态初始化顺序等问题(比如单例模式踩坑)

📘 想彻底掌握初始化流程?强烈建议阅读官方 JVM 规范第5.5节:Initialization
那里才是真正的“真相源头”。


原始标题:and Methods in the JVM