简介
.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个部分
- 魔数
- 版本号
- 常量池
3.1 常量池计数器
3.2 常量池数据区 - 访问标志
- 类索引
- 父类索引
- 接口
7.1 接口计数器
7.2 接口信息区 - 字段
8.1 字段计数器
8.2 字段信息区 - 方法
9.1 方法计数器
9.2 方法信息区 - 属性
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_info
、field_info
、method_info
、attribute_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
类型:
需要注意的几个结构:
因为在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_index
和descriptor_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 0A
,tag
:0A
(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文件基本组织结构。
以前面的例子来看:
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_count
和fields
。
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_index
和descriptor_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_count
和methods
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_index
和descriptor_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_count
和attributes
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
详细属性可在官网了解,这里只简单说明下
Code
是method_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
中作为常量。
延伸
一般描述为固定不变的值 ↩︎