Hello World编译之后:深入Class文件的二进制世界

55 阅读12分钟

核心目标

通过深度解析Class文件,建立以下认知:
1. 理解JVM的"机器语言"设计哲学
2. 掌握Class文件作为Java程序载体的完整结构
3. 理解java程序运行过程,类加载过程,类加载器,双亲委派机制
4. 理解java运行时数据分布

从java到class

package classloader;

public class First {

    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

当我们写下第一行java代码通过javac编译为class文件,再由java命令运行这个过程,会经过JDK/JVM哪些环节呢?

需要注意的是 javac First.java这一步并不由JVM本身负责,而是通过JDK,也就是jdk/bin目录下的javac负责,也包括java/jar等命令, 它是独立的编译器,而 JVM 是运行时环境。这种分离设计正是 Java "一次编写,到处运行" 理念的基础。 通过javac First.java 我们得到了First.class, 先来看看First.class

cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 1b4c 636f 6d2f 6a76
6d2f 636c 6173 736c 6f61 6465 722f 4669
7273 743b 0100 046d 6169 6e01 0016 285b
4c6a 6176 612f 6c61 6e67 2f53 7472 696e
673b 2956 0100 0461 7267 7301 0013 5b4c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b01 000a 536f 7572 6365 4669 6c65 0100
0a46 6972 7374 2e6a 6176 610c 0007 0008
0700 1c0c 001d 001e 0100 0c48 656c 6c6f
2057 6f72 6c64 2107 001f 0c00 2000 2101
0019 636f 6d2f 6a76 6d2f 636c 6173 736c
6f61 6465 722f 4669 7273 7401 0010 6a61
7661 2f6c 616e 672f 4f62 6a65 6374 0100
106a 6176 612f 6c61 6e67 2f53 7973 7465
6d01 0003 6f75 7401 0015 4c6a 6176 612f
696f 2f50 7269 6e74 5374 7265 616d 3b01
0013 6a61 7661 2f69 6f2f 5072 696e 7453
7472 6561 6d01 0007 7072 696e 746c 6e01
0015 284c 6a61 7661 2f6c 616e 672f 5374
7269 6e67 3b29 5600 2100 0500 0600 0000
0000 0200 0100 0700 0800 0100 0900 0000
2f00 0100 0100 0000 052a b700 01b1 0000
0002 000a 0000 0006 0001 0000 0003 000b
0000 000c 0001 0000 0005 000c 000d 0000
0009 000e 000f 0001 0009 0000 0037 0002
0001 0000 0009 b200 0212 03b6 0004 b100
0000 0200 0a00 0000 0a00 0200 0000 0600
0800 0700 0b00 0000 0c00 0100 0000 0900
1000 1100 0000 0100 1200 0000 0200 13

简单了解一下关键部分概念

1. 魔数 (Magic Number)

cafe babe众所周知, 开头的 0x0000-0x0003 4个字节代表class文件的固定标识符,JVM在加载类时,首先会校验文件开头的四个字节,如果不是这个魔数,就会抛出ClassFormatError

2. 版本号 (Version)

0000 0034紧接着cafe babe之后的 0x0004-0x0007 4个字节分别描述了Minor Version和Major Version

前2个字节 0x0004-0x00050000 Minor Version 为0, 这是java类文件格式的早期遗留字段,曾被用来表示类文件修订版本,如今只具备向后兼容含义无其他实际意义

后2个字节 0x0006-0x00070034 Major Version 对应十进制数为52,在java官方版本映射关系中,对应的是java se 8,这告诉JVM, 这个类是由Java 8的编译器生成的

3. 常量池 (Constant Pool)

0x0008-0x000900222个字节表示常量池的计数,对应十进制数为34, 代表该常量池中有33项有效常量(因为索引0位置被预留,所以34代表是33项有效)

后续的大部分内容都是对常量池的描述, 通过十六进制很难看清楚,可以通过javap -v First.java 进行反汇编分析查看

Classfile First.class

  Last modified 2025年12月12日; size 559 bytes

  SHA-256 checksum bfd02ecdc9cbec250a03d1aedd5d9cb756167a3ad7fa314092a52db49365f976

  Compiled from "First.java"

public class com.jvm.classloader.First

  minor version: 0

  major version: 52

  flags: (0x0021) ACC_PUBLIC, ACC_SUPER

  this_class: #5                          // com/jvm/classloader/First

  super_class: #6                         // java/lang/Object

  interfaces: 0, fields: 0, methods: 2, attributes: 1

Constant pool:

   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V

   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;

   #3 = String             #23            // Hello World!

   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V

   #5 = Class              #26            // com/jvm/classloader/First

   #6 = Class              #27            // java/lang/Object

   #7 = Utf8               <init>

   #8 = Utf8               ()V

   #9 = Utf8               Code

  #10 = Utf8               LineNumberTable

  #11 = Utf8               LocalVariableTable

  #12 = Utf8               this

  #13 = Utf8               Lcom/jvm/classloader/First;

  #14 = Utf8               main

  #15 = Utf8               ([Ljava/lang/String;)V

  #16 = Utf8               args

  #17 = Utf8               [Ljava/lang/String;

  #18 = Utf8               SourceFile

  #19 = Utf8               First.java

  #20 = NameAndType        #7:#8          // "<init>":()V

  #21 = Class              #28            // java/lang/System

  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;

  #23 = Utf8               Hello World!

  #24 = Class              #31            // java/io/PrintStream

  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V

  #26 = Utf8               com/jvm/classloader/First

  #27 = Utf8               java/lang/Object

  #28 = Utf8               java/lang/System

  #29 = Utf8               out

  #30 = Utf8               Ljava/io/PrintStream;

  #31 = Utf8               java/io/PrintStream

  #32 = Utf8               println

  #33 = Utf8               (Ljava/lang/String;)V

{

  public com.jvm.classloader.First();

    descriptor: ()V

    flags: (0x0001) ACC_PUBLIC

    Code:

      stack=1, locals=1, args_size=1

         0: aload_0

         1: invokespecial #1                  // Method java/lang/Object."<init>":()V

         4: return

      LineNumberTable:

        line 3: 0

      LocalVariableTable:

        Start  Length  Slot  Name   Signature

            0       5     0  this   Lcom/jvm/classloader/First;

  


  public static void main(java.lang.String[]);

    descriptor: ([Ljava/lang/String;)V

    flags: (0x0009) ACC_PUBLIC, ACC_STATIC

    Code:

      stack=2, locals=1, args_size=1

         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

         3: ldc           #3                  // String Hello World!

         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

         8: return

      LineNumberTable:

        line 6: 0

        line 7: 8

      LocalVariableTable:

        Start  Length  Slot  Name   Signature

            0       9     0  args   [Ljava/lang/String;

}

SourceFile: "First.java"

这里还要结合一个概念,那就是常量池tag类型,java8版本的常量池tag类型共有16种

Tag值十六进制类型常量名内容结构长度(字节)说明
10x01CONSTANT_Utf8tag(1) + length(2) + bytes3 + nUTF-8编码字符串
30x03CONSTANT_Integertag(1) + bytes(4)5int字面值
40x04CONSTANT_Floattag(1) + bytes(4)5float字面值
50x05CONSTANT_Longtag(1) + high_bytes(4) + low_bytes(4)9long字面值,占2个常量池索引
60x06CONSTANT_Doubletag(1) + high_bytes(4) + low_bytes(4)9double字面值,占2个常量池索引
70x07CONSTANT_Classtag(1) + name_index(2)3类或接口符号引用
80x08CONSTANT_Stringtag(1) + string_index(2)3字符串字面值引用
90x09CONSTANT_Fieldreftag(1) + class_index(2) + name_and_type_index(2)5字段符号引用
100x0ACONSTANT_Methodreftag(1) + class_index(2) + name_and_type_index(2)5普通类方法符号引用
110x0BCONSTANT_InterfaceMethodreftag(1) + class_index(2) + name_and_type_index(2)5接口方法符号引用
120x0CCONSTANT_NameAndTypetag(1) + name_index(2) + descriptor_index(2)5名称和类型描述符
150x0FCONSTANT_MethodHandletag(1) + reference_kind(1) + reference_index(2)4方法句柄(Java 7引入)
160x10CONSTANT_MethodTypetag(1) + descriptor_index(2)3方法类型(Java 7引入)

结合三者之后,再回头来拆解class文件

image.png

  • #1 0aCONSTANT_Methodref 长度5字节tag(1)+class_index(2)+name_and_type_index(2) 0a00 0600 14即引用#6.#20 Object.init()V 即Object类默认的无参构造函数
  • #2 09CONSTANT_Fieldref 长度5字节 tag(1)+class_index(2)+name_and_type_index(2) 09 0015 0016即引用#21.#22 java/lang/System.out:Ljava/io/PrintStream; 即对应main方法中System.out的引用

这便是常量池定义的基础规则,需要注意的是,唯有CONSTANT_Utf8是变长的

例如”Hello World!“对应class内容开头即应为: 01 00 0c,用2位存储他的长度,那么也就限制了他的最大总长度为:0xffff = 65535字节 实际可用: 65535-1 ,如果超出这个范围,编译则会报错 constant string too long,相信大家都遇到过.

4. 字节码指令集

真正表达给JVM的执行指令在这个First.class中实际只有两小段,分别对应默认构造函数和main方法,而在此次示例中 构造函数并不会被执行,因为执行的是static main 方法.

但不妨碍我们一起解读:

首先默认构造函数的字节码2a b7 00 01 b1和main方法 b2 00 02 12 03 b6 00 04 b1关于指令集的映射关系我们可以从官方文档中查看 java se 8 doc地址

构造函数

2a aload_0 Load reference from local variable

这里涉及栈帧的相关概念暂不展开, aload_0的含义是将局部变量表索引0位置的变量压入操作数栈, 而局部变量表索引0位置是固定存放this指针的, 这正是我们能够在一个非静态方法(或构造方法)中直接使用this.的原因

b7 invokespecial Invoke instance method; special handling for superclass, private, and instance initialization method invocations

invokespecial的含义是调用实例方法对超类(super) 私有方法及实例初始化方法的调用进行特殊处理,这里的特殊处理,结合完整指令b7 00 01回顾常量池#1位置,实际就是通过super();调用Object类的默认构造函数.

b1 return Return void from method

顾名思义: return;结束方法执行从方法中返回.

小结

2a b7 00 01 b1通过指令解释就是

  • aload_0
  • invokespecial #1 Object.init()V
  • return;

翻译成java代码就是:

public First() {
    super();
}

main方法

b2 getstatic Get static field from class

getstatic的含义是从类中获取静态字段, 结合完整指令b2 00 02回顾常量池#2位置,实际就是获取System.out静态字段

12 ldc Push item from run-time constan pool

将字符串从运行时常量池压入操作数栈,结合完整指令12 03常量池#3位置是String类型的引用指向#23, 而#23则是CONSTANT_Utf8 “Hello World!” 那么含义就清晰了,即将“Hello World!”压入操作数栈

b6 invokevirtual Invoke instance method; dispatch based on class

调用实例方法, 结合完整指令b6 00 04常量池#4位置是MethodRef 指向PrintStream.println(String)V, 这一步会将前两步压入的out和Hello World!弹出, 通过out.println为println方法创建新的栈帧,把Hello World!设置到println栈帧的局部变量表中去, println执行完成后退出回到main方法,内层调用暂不展开,原理是一致的

b1 return
小结

b2 00 02 12 03 b6 00 04 b1通过指令解释就是

  • getstatic #2
  • ldc #3
  • invokevirtual #4
  • return

翻译成java代码就是:

public static void main(String[] args) {
    System.out.println("Hello World!");
}

总结

至此, 我们通过Hello World 认识了class,从java文件走向了class文件, 接下来就往JVM深处走去, 从运行First开始...