我们从头到尾将Class类文件的结构过一遍。
注意:该结构没有任何分隔符,按照严格的顺序限定,由1到n,由n到m,都有其定义。
-
魔术Magic
类型 | 位置 | 数量 | 值(十六进制) |
---|---|---|---|
u4 | 1-4字节 | 1 | 0xCAFEBABE |
据Java开发小组最初的关键成员Patrick Naughton所说“选择0xCAFEBABE是因为它象征着著名咖啡品牌Peet's Coffee中深受欢迎的Baristas咖啡”,这个魔术似乎预示着日后“Java”商标的出现。
-
次版本号Minor Version
类型 | 位置 | 数量 | 值(十六进制) |
---|---|---|---|
u2 | 5-6字节 | 1 | 非固定值 |
-
主版本号Major Version
类型 | 位置 | 数量 | 值(十六进制) |
---|---|---|---|
u2 | 7-8字节 | 1 | 非固定值 |
每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件。虚拟机必须拒绝执行超过其版本号的Class文件。
例如,JDK1.1支持45.0至45.65535的Class文件,JDK1.2也能支持45.0至46.65535的Class文件。
为了方便理解,举个例子:
创建一个Test.java文件
class Test {
public static void main(String[] args) {
System.out.println("test");
}
}
使用的javac版本号
$ javac -version
javac 1.8.0_161
编译该文件,生成Test.class文件,vim打开Test.class文件
$ javac Test.java
$ vim -b Test.class
^@^F^@^O ^@^P^@^Q^H^@^R
^@^S^@^T^G^@^U^G^@^V^A^@^F^A^@^C()V^A^@^DCode^A^@^OLineNumberTable^A^@^Dmain^A^@^V([Ljava/lang/String;)V^A^@
SourceFile^A^@ Test.java^L^@^G^@^H^G^@^W^L^@^X^@^Y^A^@^Dtest^G^@^Z^L^@^[^@^\^A^@^DTest^A^@^Pjava/lang/Object^A^@^Pjava/lang/System^A^@^Cout^A^@^ULjava/io/PrintStream;^A^@^Sjava/io/PrintStream^A^@^Gprintln^A^@^U(Ljava/lang/String;)V^@ ^@^E^@^F^@^@^@^@^@^B^@^@^@^G^@^H^@^A^@ ^@^@^@^]^@^A^@^A^@^@^@^E*·^@^A±^@^@^@^A^@
^@^@^@^F^@^A^@^@^@^A^@ ^@^K^@^L^@^A^@ ^@^@^@%^@^B^@^A^@^@^@ ²^@^B^R^C¶^@^D±^@^@^@^A^@
^@^@^@
^@^B^@^@^@^C^@^H^@^D^@^A^@^M^@^@^@^B^@^N
命令模式下输入
:%!xxd
00000000: ca fe ba be 00 00 00 34 00 1d 0a 00 06 00 0f 09 .......4........
00000010: 00 10 00 11 08 00 12 0a 00 13 00 14 07 00 15 07 ................
00000020: 00 16 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 ........()
00000030: 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e V...Code...LineN
00000040: 75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69 umberTable...mai
00000050: 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 n...([Ljava/lang
00000060: 2f 53 74 72 69 6e 67 3b 29 56 01 00 0a 53 6f 75 /String;)V...Sou
00000070: 72 63 65 46 69 6c 65 01 00 09 54 65 73 74 2e 6a rceFile...Test.j
00000080: 61 76 61 0c 00 07 00 08 07 00 17 0c 00 18 00 19 ava.............
00000090: 01 00 04 74 65 73 74 07 00 1a 0c 00 1b 00 1c 01 ...test.........
000000a0: 00 04 54 65 73 74 01 00 10 6a 61 76 61 2f 6c 61 ..Test...java/la
000000b0: 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 ng/Object...java
000000c0: 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f /lang/System...o
000000d0: 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 ut...Ljava/io/Pr
000000e0: 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 intStream;...jav
000000f0: 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d a/io/PrintStream
00000100: 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a ...println...(Lj
00000110: 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b ava/lang/String;
00000120: 29 56 00 20 00 05 00 06 00 00 00 00 00 02 00 00 )V. ............
00000130: 00 07 00 08 00 01 00 09 00 00 00 1d 00 01 00 01 ................
00000140: 00 00 00 05 2a b7 00 01 b1 00 00 00 01 00 0a 00 ....*...........
00000150: 00 00 06 00 01 00 00 00 01 00 09 00 0b 00 0c 00 ................
00000160: 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 b2 ......%.........
00000170: 00 02 12 03 b6 00 04 b1 00 00 00 01 00 0a 00 00 ................
00000180: 00 0a 00 02 00 00 00 03 00 08 00 04 00 01 00 0d ................
00000190: 00 00 00 02 00 0e 0a .......
我们观察到,第一行的前8个字节是:ca fe ba be 00 00 00 34
1-4字节是Magic,5-6字节是Minor Version,7-8字节是Major Version,主版本号0x0034的十进制是52,JDK1.8及以上支持52的Class文件。不同的JDK版本,编译生成的主次版本号不同(JDK版本对应支持Class版本可上网搜索)
-
常量池
9-10字节代表常量池容量计数值(constant_pool_count)
我们看到,跟在ca fe ba be 00 00 00 34之后的是00 1d 0a 00 06 00 0f 09,0x001d的十进制是29,表示常量池中有28项常量,索引值1~28。注意:跟数组从0开始的习惯不同,0空出来的目的在于满足后面某些指向常量池的索引值的数据在特定情况下表示“不引用任何一个常量池项目”。
类型 | 标识 | 描述 |
---|---|---|
Utf8 | 1 | UTF-8编码字符串 |
Integer | 3 | 整型字面量 |
Float | 4 | 浮点型字面量 |
Long | 5 | 长整型字面量 |
Double | 6 | 双精度浮点型字面量 |
Class | 7 | 类或接口的符号引用 |
String | 8 | 字符串字面量 |
Fieldref | 9 | 字段的符号引用 |
Methodref | 10 | 类中方法的符合引用 |
InterfaceMethodref | 11 | 接口中方法的引用 |
NameAndType | 12 | 字段或方法部分符合引用 |
MethodHandle | 15 | 方法句柄 |
MethodType | 16 | 标识方法类型 |
InvokeDynamic | 18 | 动态方法调用 |
-
String
type | descriptor |
---|---|
u1 | tag |
u2 | string_index |
-
Methodref
type | descriptor |
---|---|
u1 | tag |
u2 | class_index |
u2 | name_and_type_index |
-
字段表集合
字段表(field_info)包括的信息:
字段的作用域修饰符:public、protected、private
实例变量or类变量修饰符:static
变量可变性修饰符:final
并发可见性修饰符:volatile
可否被序列化修饰符:transient
字段数据类型:基本类型、对象、数组
字段名称
上面的信息中,修饰符都是boolean值,要么0,要么1。
字段数据类型与字段名称就无法固定长度去表示。
Java中,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称。但是对字节码没这个要求,两个字段的描述符不一致,字段重名就是合法的。
-
方法表集合
方法修饰符中,不包括volatile和transient。
相对的,新增synchronized、native、strictfp和abstract
-
属性表集合
- Code属性
- Exception属性
- LineNumberTable属性
- LocalVariableTable属性
- LocalVariableTypeTable属性(引入泛型时新增的属性)
- SourceFile属性
- ConstantValue属性
- InnerClasses属性
- Deprecated及Synthetic属性
- StackMapTable属性
- Signature属性
- BootstrapMethods属性
-
字节码指令简介
- 字节码与数据类型
在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息,例如iload指令用于从局部变量中加载int型的数据到操作数栈中。
在指令集中,load有操作int的指令iload,但对byte、char、short和boolean,没有对应的指令,在处理它们时,会先转换成int类型,然后使用iload进行操作。 - 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
将一个局部变量加载到操作栈:iload、iload_、lload、lload_ 、fload、fload_ 、dload、dload_ 、aload、aload_
将一个数值从操作数栈存储到局部变量:istore、istore_、lstore、lstore_ 、fstore、fstore_ 、dstore、dstore_ 、astore、astore_
将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_、lconst、fconst_ 、dconst
扩充局部变量表的访问索引的指令:wide - 运算指令
加法指令:iadd、ladd、fadd、dadd
减法指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem
取反指令:ineg、lneg、fneg、dneg
位移指令:ishl、ishr、iushr、lshl、lshr、lushr
按位或指令:ior、lor
按位与指令:iand、land
按位异或指令:ixor、lxor
局部变量自增指令:iinc
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp - 类型转换指令
宽化类型转换:
int类型到long、float或者double类型
long类型到float、double类型
float类型到double类型
窄化类型转换:
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f - 对象创建于访问指令
创建类实例指令:new
创建数组指令:newarray、anewarray、multianewarray
访问类字段指令:getfield、putfield、getstatic、putstatic
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
取数组长度的指令:arraylength
检查类实例类型的指令:instanceof、checkcast - 操作数栈管理指令
将操作数栈的栈顶一个或两个元素出栈:pop、pop2
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
将栈最顶端的两个数值交换:swap - 控制转移指令
条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
复合条件分支:tableswitch、lookupswitch
无条件分支:goto、goto_w、jsr、jsr_w、ret - 方法调用和返回指令
invokevirtual指令用于调用对象的实例方法
invokeinterface指令用于调用接口方法
invokespecial指令用于调用一些需要特殊处理的实例方法
invokestatic指令用于调用类方法
invokedynamic指令用于在运动时动态解析出调用点限定符所引用的方法
注:前四条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 - 异常处理指令
显式抛出异常指令athrow - 同步指令
Java虚拟机支持方法级同步和方法内部一段指令序列的同步,都是通过管程(Monitor)来支持的。
指令:monitorenter、monitorexit