Java 字节码 代码编译结果从本地机器码转变为字节码,是存储格式发展的一小步,确是编程语言发展的一大步。
字节码文件剖析 我们从一段简单的代码来入手
public class MyTest01 { private int a = 0; public int getA() { return a; } public void setA(int a) { this.a = a; } }
我要要看一下 java 文件对应的 class 文件的结构,定位到工程的 out\production\classes 下边执行:
javap -c com.cuzz.jvm.bytecode.Mytest01
警告: 二进制文件com.cuzz.jvm.bytecode.Mytest01包含com.cuzz.jvm.bytecode.MyTest01 Compiled from "MyTest01.java" public class com.cuzz.jvm.bytecode.MyTest01 { public com.cuzz.jvm.bytecode.MyTest01(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_0 6: putfield #2 // Field a:I 9: return public int getA(); Code: 0: aload_0 1: getfield #2 // Field a:I 4: ireturn public void setA(int); Code: 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return }
我们如果需要获得更多信息可以使用如下命令:
javap -verbose com.cuzz.jvm.bytecode.Mytest01
警告: 二进制文件com.cuzz.jvm.bytecode.Mytest01包含com.cuzz.jvm.bytecode.MyTest01 Classfile /E:/project/learn-demo/demo-10-jvm-lecture/out/production/classes/com/cuzz/jvm/bytecode/Mytest01.class Last modified 2019-2-3; size 492 bytes MD5 checksum cceeac51ae7b6fc46c60faf834de5932 Compiled from "MyTest01.java" public class com.cuzz.jvm.bytecode.MyTest01 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // com/cuzz/jvm/bytecode/MyTest01.a:I #3 = Class #22 // com/cuzz/jvm/bytecode/MyTest01 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/cuzz/jvm/bytecode/MyTest01; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest01.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 com/cuzz/jvm/bytecode/MyTest01 #23 = Utf8 java/lang/Object { public com.cuzz.jvm.bytecode.MyTest01(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_0 6: putfield #2 // Field a:I 9: return LineNumberTable: line 8: 0 line 10: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/cuzz/jvm/bytecode/MyTest01; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 13: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/cuzz/jvm/bytecode/MyTest01; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 17: 0 line 18: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/cuzz/jvm/bytecode/MyTest01; 0 6 1 a I } SourceFile: "MyTest01.java"
我们也可以使用二进制文件查看器 查看class文件的16进制信息(winhex下载 ):
16文件查看器里边第一行的CA 就是一个字节的容量(8位bit):
使用 javap -verbos 命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法信息、类变量与成员变量等信息。
魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE (詹姆斯.高斯林设计的,蕴意:咖啡宝贝,java 的图标是咖啡。
魔数之后的4个字节为版本信息,前2个字节表示 minor versio(次版本号),后两个字节表示 major version(主版本号)。 这里的版本号为 00 00 00 34,换算成十进制,表示次版本号为0,主版本号为52。
字节常量池剖析 常量池(constant pool):紧接着主版本号之后的就是常量池入口。一个 Java 类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是 Class 文件的资源仓库,比如说 Java 类中定义的方法与变量信息,都是存储在常量池中。常量池中的主要储存两类常量:字面量与符号引用。字面量如文本字符串,Java 中声明为 final 的常量值等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
常量池的总体结构:Java 类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成。常量池数量紧跟在主版本号后面,占据 2 个字节;常量池数组紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素类型、结构都是不同的,长度当然也就不同;但是,每一种元素的第一个数据都是一个 u1 类型,该字节是一个标志位,占据 1 个字节。JVM 在解析常量池时,会根据这个 u1 类型来获取元素的具体类型。
值得注意的是,常量池数组中元素的个数 = 常量池数 - 1 (其中0暂时不使用)。对应的是 00 18 转化为十进制为24个常量,而我们看到只有23个。目的是满足某些常量池索引值的数据在特定情况下需要表达“不引用任何一个常量”的含义;根本原因在于,索引 0 也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应 null 值,所以,常量池的索引从 1 开始而不是 0 。
Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // com/cuzz/jvm/bytecode/MyTest01.a:I #3 = Class #22 // com/cuzz/jvm/bytecode/MyTest01 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/cuzz/jvm/bytecode/MyTest01; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest01.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 com/cuzz/jvm/bytecode/MyTest01 #23 = Utf8 java/lang/Object
Class 文件结构中常量池数据类型的结构表
在 JVM 规范中,每一个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回的 void 类型都是用一个大写字符来表示,对象类型则使用字符 L 加对象的全限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM 都只使用一个大写字母来表示,如下所示:B - byte,C - char,D - double,F - float,I - int,J - long,S - short,Z - boolean,V - void,L - 对象类型,如 Ljava/lang/String;
。
对于数组类型来说,没一个维度使用前置 [ 来表示,如 int []
被记录为 [I ,String[][]
被记录为 [[Ljava/lang/String;
。
用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组括号内,如方法:String getRealNameByIdAndNickName(int id, String name)
的描述符为:(I, Ljava/lang/String;) Ljava/lang/String;
我们来分析前面几个常量,如图:
我反编译出来的文件对比:
Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // com/cuzz/jvm/bytecode/MyTest01.a:I #3 = Class #22 // com/cuzz/jvm/bytecode/MyTest01 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/cuzz/jvm/bytecode/MyTest01; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest01.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 com/cuzz/jvm/bytecode/MyTest01 #23 = Utf8 java/lang/Object
0A 00 04 00 14
,如图中的标注出来,0A 对应值为10,在上表的常量中 CONSTANT_Methodref_info 中,那么后边的2个字节 00 04 (十进制4)就是 U2(第一个index),即指向声明方法的类描述符 CONSTANT_Class_info 的索引项,而第二个索引(第二个index)00 14(十进制20) 指向名称及类型描述符 CONSTANT_NameAndType_info 的索引项。类描述指向 #4 ,#4 又指向 #23,所以描述为 java/lang/Object,而名称以及类型描述符指向 #20,#20 有指向 #7 和 #8,"<init>":()V
表示为构造方法。
09 00 03 00 15
,09 是标志位对用的是 CONSTANT_Fieldref_info,第一个索引指向的是声明字段的类或接口描述符,CONSTANT_Class_info 的索引项,根上面一样分析。
07 00 16
, 00 16 十进制是22 ,07是常量 CONSTANT_CLass_info,只有一个index,指向的是指定权限定名常量项的索引, 00 16 是十进制22。
07 00 17
,07是常量 CONSTANT_CLass_info,只有一个index,指向的是指定权限定名常量项的索引,00 17 十进制是23。
01 00 01 61
,01 是 CONSTANT_Utf8_info,后面 00 01 这两个字节表示长度,最后 61 (十进制为97)的表示 ASCII 中带索引,在 ASCII 中为字母 a。
01 00 01
为 I。
等等
Java 字节码结构
Class 字节码中有两种数据类型
字节数据直接量:这是基本的数据类型,共细分为 u1、u2、u4、u8 这四种,分别代表连续的 1 个字节、2 个字节、4 个字节和8 个字节。
表(数组):表示有多个基本数据或其他表,按照既定顺序组成的大的数据集合。表示有结构的,它的结构体现在,组成表的成分所在的位置和顺序都已经严格定义好的。
访问标志 访问标志(Access_Flag)信息包括该 Class 文件是类还是接口,是否被定义成 public,是否是 abstract,如果是类,是否被声明成 final。通过上面的源代码,我们可以知道该文件是类并且是 public。
常量池之后两个字节就是访问标志,我们这个类中是 0x 00 21
,从上面来看并没有,原来它是 0x 00 20
和 0x 00 01
的并集,表示 ACC_PUBLIC 与 ACC_SUPER。
类索引、父类索引与接口索引
00 03
是类索引,指向 #3 表示是一个类,其名字为 com/cuzz/jvm/bytecode/MyTest01
00 04
是父亲索引,指向 #4 表示是一个类,其名字是 java/lang/Object
00 00
是接口,表示没有接口
字段表集合 字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但不包括方法内部声明的局部变量。
如下图
00 01 是成员变量的数量,后面接着就是 field_info 成员变量信息
field_info { u2 access_flags; // 0002 表示私有 private u2 name_index; // 0005 表示 a u2 descriptor_index; // 0006 表示 I u2 attributes_count; // 0000 没有 attribute_info attributes[attributes_count]; }
方法表 刚开始的 00 03 表示有三个方法,除了getter/setter 还有默认构造方法
methods_count { u2 access_flags; // 0001 表示 public u2 name_index; // 0007 指向常量池中 #7 的常量为 <init> u2 descriptor_index; // 0008 指向常量池中 #8 的常量为 ()V u2 attributes_count; // 0001 表示一个属性 attribute_info attributes[attributes_count]; }
方法中的属性结构
attribute_info { u2 attribute_name_index; // 0009 指向常量池中 #9 为 Code u4 attribute_length; // 0000 0038 表示长度为 0x38 为 56 长度的字节 u1 info[attribute_length]; }
Code 结构 Code attribute 的作用是保存该方法的结构,如所对应的字节码
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]; }
attribute_length 表示 attribute 所包含的字节数,不包含 attribute_name_index 和 attribute_length 字段
max_stack 表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
max_locals 表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
code_length 表示该方法所包含的字节码的字节数以及具体的指令码,具体字节码即是该方法被调用时,虚拟机所执行的字节码
exception_table 表示存放的是处理异常的信息
每个 exception_table 表由 start_pc,end_pc,handler_pc,catch_type 组成
start_pc 和 end_pc 表示在 code 数组中的从 start_pc 到 end_pc 处(包含 start_pc,不包含 end_pc)的指令抛出的异常会由这个表项来处理
handler_pc 表示处理异常的代码的开始处,catch_type 表示会被处理的异常类型,它指向常量池中的一个异常类,当 catch_type 为 0 时,表示处理所有的异常
字节码查看工具 https://github.com/ingokegel/jclasslib