简介

.java 文件编译后的 .class 文件内容究竟是怎样的?JVM 又是如果识别并加载 .class 文件的。

环境

Win10、Nixos
JDK1.8

Class文件内容

windows 下用 Powershell 查看字节码内容(十六进制)

PS> cat Demo.java;
public class Demo{}

PS> javac.exe Demo.java; Format-Hex Demo.class

           Path: D:\tmp\java\Demo.class

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000   CA FE BA BE 00 00 00 34 00 0D 0A 00 03 00 0A 07  Êþº¾...4........
00000010   00 0B 07 00 0C 01 00 06 3C 69 6E 69 74 3E 01 00  ........<init>..
00000020   03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69  .()V...Code...Li
00000030   6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 0A  neNumberTable...
00000040   53 6F 75 72 63 65 46 69 6C 65 01 00 09 44 65 6D  SourceFile...Dem
00000050   6F 2E 6A 61 76 61 0C 00 04 00 05 01 00 04 44 65  o.java........De
00000060   6D 6F 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F  mo...java/lang/O
00000070   62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00  bject.!.........
00000080   01 00 01 00 04 00 05 00 01 00 06 00 00 00 1D 00  ................
00000090   01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01  .......*·..±....
000000A0   00 07 00 00 00 06 00 01 00 00 00 01 00 01 00 08  ................
000000B0   00 00 00 02 00 09                                ......

Linux 下命令行查看

$ cat IDemo.java
public interface IDemo{}

$ javac IDemo.java ; hexdump IDemo.class
0000000 feca beba 0000 3400 0700 0007 0705 0600
0000010 0001 530a 756f 6372 4665 6c69 0165 0a00
0000020 4449 6d65 2e6f 616a 6176 0001 4905 6544
0000030 6f6d 0001 6a10 7661 2f61 616c 676e 4f2f
0000040 6a62 6365 0674 0001 0001 0002 0000 0000
0000050 0000 0001 0003 0000 0002 0004
000005b

这里大小端是相反的。

Class文件结构

一个完整的 class 文件大致可分为10个部分

  1. 魔数
  2. 版本号
  3. 常量池
    3.1 常量池计数器
    3.2 常量池数据区
  4. 访问标志
  5. 类索引
  6. 父类索引
  7. 接口
    7.1 接口计数器
    7.2 接口信息区
  8. 字段
    8.1 字段计数器
    8.2 字段信息区
  9. 方法
    9.1 方法计数器
    9.2 方法信息区
  10. 属性
    10.1 属性计数器
    10.2 属性信息区

关于class 文件的基本数据结构在 The Java Virtual Machine Specification (Second ed.)文档有详细的说明,如下:

struct Class_File_Format {
   u4 magic_number;

   u2 minor_version;
   u2 major_version;

   u2 constant_pool_count;
   cp_info constant_pool[constant_pool_count - 1];

   u2 access_flags;
   u2 this_class;
   u2 super_class;

   u2 interfaces_count;
   u2 interfaces[interfaces_count];

   u2 fields_count;
   field_info fields[fields_count];

   u2 methods_count;
   method_info methods[methods_count];

   u2 attributes_count;
   attribute_info attributes[attributes_count];
}

u4: 指4个字节的无符号整型
u2: 指2个字节的无符号整型
cp_infofield_infomethod_infoattribute_info 表示相关数据结构,长度均不定可变

magic_number

魔数(magic_number)是指某一文件格式下前几个固定字节值,一般用于识别文件格式,class文件的魔数固定为前4个字节,即:CA FE BA BE
至于它的来源和背后的故事可以看看 Wiki - Java class file 下的 Magic Number

minor_version,major_version

minor_version,major_version统称版本号,共占用4个字节,各分为2个字节的次版本号(minor_version)占第5、6字节位以及占第7、8字节位的主版本号(major_version)。版本号的作用为标识 class 文件是在具体哪个版本的 JVM 下生成的,让运行时环境的JVM确认是否可以加载此版本的class文件。

以下常见主版本号(major_version)

Java SE 17 = 61 (0x3D hex),
Java SE 16 = 60 (0x3C hex),
Java SE 15 = 59 (0x3B hex),
Java SE 14 = 58 (0x3A hex),
Java SE 13 = 57 (0x39 hex),
Java SE 12 = 56 (0x38 hex),
Java SE 11 = 55 (0x37 hex),
Java SE 10 = 54 (0x36 hex),
Java SE 9 = 53 (0x35 hex),
Java SE 8 = 52 (0x34 hex),
Java SE 7 = 51 (0x33 hex),
Java SE 6.0 = 50 (0x32 hex),
Java SE 5.0 = 49 (0x31 hex),
JDK 1.4 = 48 (0x30 hex),
JDK 1.3 = 47 (0x2F hex),
JDK 1.2 = 46 (0x2E hex),
JDK 1.1 = 45 (0x2D hex).

常量池

常量池分为两个数据字段:constant_pool_count(计数器)+constant_pool(数据结构数组)。存储了整个class文件下的字面量信息1,包括:各类型的数字、字符串、类引用、方法引用、字段引用和方法句柄等其他常量。

constant_pool_count

常量池计数器(实际并不是数量),它的值:constant_pool_count = constant_pool实体的数量 + 1,这一点与其他的池不同,constant_pool的索引在(0,constant_pool_count)之间(不包含)。总共占2个无符号字节。数值0表示为未引用任何对象。

以前面win下的例子来说:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000   CA FE BA BE 00 00 00 34 00 0D 0A 00 03 00 0A 07  Êþº¾...4........

00 0D也就是13,但常量池数量需要减1,所以是12。

constant_pool

constant_pool_count只是常量池的计数器,实际数据在constant_pool数组中。constant_pool也就是实际意义上的常量池,长度不定。数组中每一个元素数据结构为:

cp_info {
    u1 tag;
    u1 info[];
}
  • tag:常量的类型
  • info:常量信息

常量信息info是一个不确定数组,不同的tag也就是常量类型,info数组长度是不一样,每个常量类型的info都是固定的或可计算的,而对于字符串等不定长的结构,通常都是采取分为两个字段表示(实际上池也是):一个长度,一个数组。

相关的tag类型:

Table 4.4-B. Constant pool tags (by tag)

需要注意的几个结构:

因为在class文件中,大多数信息最终都会用字符串内容表示,所以字符串内容无疑是最基本的结构之一。

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

CONSTANT_Utf8_info 也就是我们常说的字符串常量(utf8编码过后的内容),它在tag之后的两个字节为无符号数值,它表示了这个字符串的UTF-8编码后的字节数组长度,即bytes的大小。这里不深入bytes存储的具体内容和方式,详细可查看原文档

然后是CONSTANT_String_info

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

CONSTANT_Utf8_info用来表示常量字符串的内容,而CONSTANT_String_info则表示常量字符串对象,即为产生的Object。对象的实际内容通过string_index指向实际的CONSTANT_Utf8_info

常量池中大多数都是依赖一个index指向实际的CONSTANT_Utf8_info来表示(数值直接通过字节表示)。

例如,类:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

name_index实际就是一个CONSTANT_Utf8_info的索引。

再比如,方法、字段等:

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_InterfaceMethodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

这里的class_index指的是constant_pool中一个具体的CONSTANT_Class_info的索引,而name_and_type_index指的其中具体某一个CONSTANT_NameAndType_info,其结构如下:

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

它包含的name_indexdescriptor_index是两个CONSTANT_Utf8_info在常量池的索引。

以前面win下的例子来看:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000   CA FE BA BE 00 00 00 34 00 0D 0A 00 03 00 0A 07  Êþº¾...4........
00000010   00 0B 07 00 0C 01 00 06 3C 69 6E 69 74 3E 01 00  ........<init>..
00000020   03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69  .()V...Code...Li
00000030   6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 0A  neNumberTable...
00000040   53 6F 75 72 63 65 46 69 6C 65 01 00 09 44 65 6D  SourceFile...Dem
00000050   6F 2E 6A 61 76 61 0C 00 04 00 05 01 00 04 44 65  o.java........De
00000060   6D 6F 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F  mo...java/lang/O
00000070   62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00  bject.!.........

共计12个常量,简单说明几个:

第1个:0A 00 03 00 0Atag0A(CONSTANT_Methodref_info),info[4]00 03 00 0A

CONSTANT_Methodref_info {
    u1 tag; // `0A`
    u2 class_index; // `00 03`
    u2 name_and_type_index; // `00 0A`
}

第3个:07 00 0C

CONSTANT_Class_info {
    u1 tag; // 07
    u2 name_index; // 00 0C
}

第10个:0C 00 04 00 05

CONSTANT_NameAndType_info {
    u1 tag; // 0C
    u2 name_index; // 00 04
    u2 descriptor_index; //00 05
}

第12个:01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74

CONSTANT_Utf8_info {
    u1 tag; // 01
    u2 length; // 00 10
    u1 bytes[length]; // 6A 61 76 61 2F 6C 61 6E 67 2Fj 4F 62 6A 65 63 74
}

这里直接通过 javap 查看class文件的编译字节信息

PS> javap -v Demo.class
Classfile D:\tmp\java\Demo.class
  Last modified 2021-6-19; size 182 bytes
  MD5 checksum 626255e824a12d768af074fcb588b1d3
  Compiled from "Demo.java"
public class Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#10         // java/lang/Object."<init>":()V
   #2 = Class              #11            // Demo
   #3 = Class              #12            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               SourceFile
   #9 = Utf8               Demo.java
  #10 = NameAndType        #4:#5          // "<init>":()V
  #11 = Utf8               Demo
  #12 = Utf8               java/lang/Object
{
  public Demo();
    descriptor: ()V
    flags: 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 1: 0
}
SourceFile: "Demo.java"

第12个常量对应java/lang/Object,第4和5个常量为<init>()V,所以第一个变量是java/lang/Object."<init>":()V,实际指向类Object编译的初始化方法<init>

接口常量就更加少了,这里不再继续说明了。

access_flags

access_flags为访问标志,共2个字节,用于表示某个类或者接口的访问权限及基础属性。
详细内容可查看《Java虚拟机原理图解》 1.1、class文件基本组织结构

P2

以前面的例子来看:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000070   62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00  bject.!.........

即:ACC_SUPER+ACC_PUBLIC

this_class

当前类索引,即当前类或者接口在constant_pool中的位置索引。

以之前的例子来看:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000070   62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00  bject.!.........

00 02即是第2个,参见上文

super_class

父类索引,即当前类的父类在constant_pool中的索引。

以之前的例子来看:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000070   62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00  bject.!.........

父类为第3个常量class,也就是Object
对于Object(没有父类),则索引为0。
对于接口:

00000040   65 63 74 06 01 00 01 00 02 00 00 00 00 00 00 00  ect.............
// javap -v Demo.class
  #2 = Class              #6              // java/lang/Object

只能是Object

接口表

interfaces_count

接口计数器,占两个字节,interfaces_count的值表示当前类或接口的直接父接口数量。可为0。

interfaces

接口数组,长度为interfaces_count。数组中每一个元素必须是constant_pool中的一个CONSANT_Class_info常量。存储顺序与代码中提供的接口顺序相同。

上文无接口时,表示为

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000070   62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00  bject.!.........

只有接口计数器的值00 00

字段表

字段表同样由两个部分组成:fields_countfields

fields_count

字段计数器,共2个字节,表示当前Class文件fields数组的长度。可以为0。

fields

字段数组,字节大小不定,元素结构为field_info,长度为fields_count。只包含当前class文件定义的字段,继承的部分不在其中。

field_info结构:

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

其中name_indexdescriptor_index都是constant_pool的一个CONSTANT_Utf8_info索引。
attributes结构与class文件的attributes结构一致,后文再讲。
fields_count为0时,和接口表一样不会有数据区fields:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000070   62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00  bject.!.........

方法表

方法表同样分为两个部分:methods_countmethods

methods_count

方法计数器,共占用两个字节,从0开始,表示当前class文件定义的方法数量,不包含继承的部分。

上文的例子中表示的位置:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000070   62 6A 65 63 74 00 21 00 02 00 03 00 00 00 00 00  bject.!.........
00000080   01 00 01 00 04 00 05 00 01 00 06 00 00 00 1D 00  ................

只有一个方法。

methods

方法数组,长度不定,每个元素均为method_info

method_info数据结构:

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

和字段表元素结构类似,name_indexdescriptor_index均是constant_pool中的一个CONSTANT_Utf8_info常量的索引。

以上文的例子数据来看:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000080   01 00 01 00 04 00 05 00 01 00 06 00 00 00 1D 00  ................

这里共8个字节,不包含attributes,相关常量查看上文:

  • 00 01: access_flags,访问标志:ACC_PUBLIC
  • 00 04: name_index,方法名,常量表里的 <init>
  • 00 05: descriptor_index,方法描述,常量表里的 ()V
  • 00 01: attributes_count,方法属性个数,1个

方法的属性同类属性一起。

属性表

属性表同样也是两个部分组成:attributes_countattributes

attributes_count

属性计数器,共占2个字节,表示attributes数组元素的个数。

attributes

属性数组,大小不定,长度为attributes_count,每个元素的结构都是attribute_info,字段、方法、class文件都会用到。

attribute_info数据结构:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

其中attribute_name_index表示的是属性名,其值为常量池constant_pool中的一个CONSTANT_Utf8_info常量的索引,属性内容长度用了4个字节表示。

class文件五个非常重要的属性:

  • ConstantValue
  • Code
  • StackMapTable
  • Exceptions
  • BootstrapMethods

详细属性可在官网了解,这里只简单说明下

Codemethod_info的一个属性,它包含方法体内容,其结构为

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

方法体为code_length+code[code_length],其他不再细说。

根据上文仅有的方法来看,它只有一个属性,也就是

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000080   01 00 01 00 04 00 05 00 01 00 06 00 00 00 1D 00  ................
00000090   01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01  .......*·..±....
000000A0   00 07 00 00 00 06 00 01 00 00 00 01 00 01 00 08  ................
000000B0   00 00 00 02 00 09                                ......

常量表见上文

  • 00 06: 属性名索引,值为Code
  • 00 00 00 00 1D: 属性内容长度,值为30个字节

内容根据Code_attribute的数据结构解析

max_stack:00 01
max_locals:00 01
code_length:00 00 00 05
code[code_length]:2A B7 00 01 B1
exception_table_length:00 00
exception_table: 无
attributes_count:00 01
attributes:00 07 00 00 00 06 00 01 00 00 00 01 00

Code中的attributes还有一个00 07表示的一个LineNumberTable,这里不做说明

剩下的字节也就是class文件的属性:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

000000A0   00 07 00 00 00 06 00 01 00 00 00 01 00 01 00 08  ................
000000B0   00 00 00 02 00 09                                ......

这里表示的是SourceFile属性,其值为Demo.java

思考

  • Q:导入的静态方法是怎么表示的?
    如果代码里没有用到,编译后会被优化。import的类和方法都会放入constant_pool中作为常量。

延伸

  1. Class 文件基本组织结构
  2. Java class file - Wiki
  3. The class File Format

  1. 一般描述为固定不变的值 ↩︎