u1、u2、u4分别代表1字节、2字节、4字节
一. 初识字节码文件
整体结构
1. Class 字节码中有两种数据类型
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表:
- 无符号数属于基本的数据类型,以u1,u2,u4,u8四种,分别连续代表的1个字节、2个字节、4个字节、8个字节组成的整体数据,无符号数可以用来描述文字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯的以“_info” 结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
表(数组):表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。
2. 编写Java测试类
package com.java.jvm.bytecode;
/**
* @author xuweizhi
* @date 2019/03/03 12:57
*
*
*/
public class ByteCode1 {
private int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
3. 查找编译过后的class文件
由于 Idea j 反编译工具的存在,查看的字节码文件是已经经过反编译后的格式,因此我们要借助JDK自带的JavaP工具查看字节码文件。
进入编译后的classes 目录 D:\root\JavaPlus\jvm\out\production\classes>,输入javap com.java.jvm.bytecode.ByteCode1命令,打印字节码数据
D:\root\JavaPlus\jvm\out\production\classes>javap com.java.jvm.bytecode.ByteCode1
Compiled from "ByteCode1.java"
public class com.java.jvm.bytecode.ByteCode1 {
public com.java.jvm.bytecode.ByteCode1();
public int getA();
public void setA(int);
}
4. javap -c com.java.jvm.bytecode.ByteCode1
更详细的的方式打印字节码文件信息
D:\root\JavaPlus\jvm\out\production\classes>javap -c com.java.jvm.bytecode.ByteCode1
Compiled from "ByteCode1.java"
public class com.java.jvm.bytecode.ByteCode1 {
public com.java.jvm.bytecode.ByteCode1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
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
}
5. javap -verbose com.java.jvm.bytecode.ByteCode1
D:\root\JavaPlus\jvm\out\production\classes>javap -verbose com.java.jvm.bytecode.ByteCode1
Classfile /D:/root/JavaPlus/jvm/out/production/classes/com/java/jvm/bytecode/ByteCode1.class
Last modified 2019年3月3日; size 495 bytes
MD5 checksum 681ff27fa74e0f13311cfb52a14b3fb0
Compiled from "ByteCode1.java"
public class com.java.jvm.bytecode.ByteCode1
minor version: 0 #次版本号
major version: 52 #主版本号
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/java/jvm/bytecode/ByteCode1
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/java/jvm/bytecode/ByteCode1.a:I
#3 = Class #22 // com/java/jvm/bytecode/ByteCode1
#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/java/jvm/bytecode/ByteCode1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile //编译的源文件
#19 = Utf8 ByteCode1.java
#20 = NameAndType #7:#8 // "<init>":()V # 指向常量池中索引值
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/java/jvm/bytecode/ByteCode1
#23 = Utf8 java/lang/Object
{
public com.java.jvm.bytecode.ByteCode1();
descriptor: ()V
flags: (0x0001) 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_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 9: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/java/jvm/bytecode/ByteCode1;
public int getA();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/java/jvm/bytecode/ByteCode1;
public void setA(int);
descriptor: (I)V
flags: (0x0001) 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 18: 0
line 19: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/java/jvm/bytecode/ByteCode1;
0 6 1 a I
}
SourceFile: "ByteCode1.java"
subline 或者 Notepad++ 插件 Hex Editor 或者 Winhex 查看16进制文件
二. Javap 命令
- 使用javap -verbose 命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。
三. .class二进制文件
Java .class 文件是以最小单位为两个16位字母构成的16进制文件,由魔数来看,前八个字母构成了4个字节。
3.1 魔数
所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCA FE BA BE(咖啡宝贝)
3.2 版本号
魔数之后的四个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号),换算成十进制,表示次版本号为0,主版本号为52.所以该版本为:1.8.0,通过java -version 验证 1.8.0_181
表示.class是由哪个具体版本的JDK编译而成的十六进制文件。
次版本号:第 5、6 个字节 00 00
主版本号:第 7、8 个字节 00 34
3.3 常量池(constant pool)
3.3.1 常量池概念
紧接着主版本号之后的就是常量池入口
一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作Class文件的资源仓库,比如Java类中定义的方法与变量信息,都是存储在常量池中。
常量池主要存储两类常量:字面量与符号引用
- 字面量:如文本字符串,Java中声明为final的常量值等
字面量是指由字母,数字等构成的字符串或者数值,它只能作为右值出现,所谓右值是指等号右边的值,如:int a=123这里的a为左值,123为右值。
常量和变量都属于变量,只不过常量是赋过值后不能再改变的变量,而普通的变量可以再进行赋值操作。
例:
int a;//a变量
const int b=10;//b为常量,10为字面量
string str="hello world";//str为变量,hello world为也字面量
- 符号引用:如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。
3.3.2 常量池的总体结构
Java类所对应的常量池主要由常量池数量与**常量池数组(常量表)**这两部分共同构成。
- 常量池数量紧跟在主版本号后面,占据两个字节
- 常量池数组(常量表)则紧着在常量池数量之后,常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同;但是,每一种元素的第一个数据都是u1类型,该字节是个标志位,占据1个字节。JVM在解析常量池时,会根据u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池数 - 1 (其中0暂时不可用),在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的是满足某些常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池”的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以,常量池的索引从1而非0开始。在整个class文件中,只用常量池的索引从1开始,其它是从0开始。
3.3.4 常量池常量项的结构总表
tag:常量池类型的索引值,用u1(一个字节)表示
3.3.5 常量池信息
cafe babe 0000 0034 0018
0018:表示常量池的数量为1*16+8=24个,为什么java -verbose反编译的文件只有23个常量呢?
值得注意的是,常量池数组中元素的个数 = 常量池数 - 1 (其中0暂时不可用)。
目的是满足某些常量池索引值的数据在特定情况下需要表达不引用任何一个常量池的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以,常量池的索引从1而非0开始
3.3.6 字节码文件分析
在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示,如下所示:
- B - byte
- C - char
- D - double
- I - int
- F - float
- J - long
- S - short
- Z - boolean
- v - void
- L - 对象类型 如: Ljava/lang/String;
对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]被记录为[I,String[][]被记录为[[java.lang.String
用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:String getRealnamebyIdAndNickname(int id , String name)的描述符为:(I,Ljava/lang/String;)Ljava/lang/String;
第一个常量分析 0a 00 04 00 14 (5个常量)
CONSTANT_Methodref_info
tag(u1): 0a(10) 对应tag值的表格类型为CONSTANT_Methodref_info
index: 00 04
idnex: 00 14
cafe babe 0000 0034 0018 0a00 0400 1409
0003 0015 0700 1607 0017 0100 0161
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/java/jvm/bytecode/ByteCode1.a:I
#3 = Class #22 // com/java/jvm/bytecode/ByteCode1
#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/java/jvm/bytecode/ByteCode1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 ByteCode1.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/java/jvm/bytecode/ByteCode1
#23 = Utf8 java/lang/Object
3.4 访问标志
访问标志信息包括Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,我们是否被声明成final.通过上面的源代码,我们知道该文件是类还是public,占据两个字节。
0x 0021:是 0x 0020 和 0x 0001 并集,表示ACC_PUBLIC与ACC_SUPER
3.5 class类名
占据两个字节,表示常量池所在数据的索引值,如0x 00 03 表示常量池中所在的索引对应的数据
#3 = Class #22 // com/java/jvm/bytecode/ByteCode1
3.6 父类
占据两个字节,一样表示父接口指向常量池所处的索引值
3.7 接口
占据两个字节,表示接口的数量。若接口的数量等于0,则表示没有接口,其后面的接口字节码文件没有数据。
3.8 字段表集合
字段表用于描述类和接口中声明的变量,这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
两个字节表示字段数量,字段表的结构
field_info{
u2 access_flags; 0002 # 表示字段的修饰符
u2 name_index; 0005 # 表示字段的简单名称
u2 decriptor_index; 0006 # 表示字段和方法的描述符
u2 attributes_count; 0000
attribute_info attributes[attributes_count];
}
- 全限定名:以"org/fenixsoft/calzz/TestClass"是这个类的全限定名,仅仅是把类全名中的".“替换成了”/"而已,为了使连续的多个全限定名之间不产生混淆,在使用最后一般会加入一个“;”表示全限定名结束。
- 简单名称:指没有类型和参数修饰的方法或者字段名称,这个类中inc()方法和m字段的简单名称分别为是"inc"和"m".
- 描述符:描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述规则,基本数据类型为(byte、char、int、long、long、short、boolean)以及所表示无返回值的void类型都用一个大写字符来表示,而对象类型则是用符号L加加对象的全限定名来表示,详情见下图:
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如定义一个为“java.lang.String[]”,类型的二维数组,将被记录为:"[[Ljava/lang/String",一个整型数组"int[]“将被标记为”[I".
用描述符来描述方法,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号内“()”之内。如方法void inc()的描述符“()V”,方法java.lang.String.toString()?)Ljava/lang/String;
00 01 00 02 00 05 00 06 00 00
- 00 01:表示字段数量
- 00 02:访问修饰符 private
- 00 05:字段名称索引 a
- 00 06:描述符索引,对应常量池数据 I => int
- 00 00:
3.9 方法
u2:占据两个字节表示方法的数量
方法表结构
3.9.1 参照方法表结构进行分析
method_info {
u2 access_flags; #访问标识符
u2 name_index; #属性名索引
u2 description_index; #描述符对应java类型
u2 attributes_count; #属性数量
attribute_info attributes[attributes_count];
}
00 03 00 01 00 07 00 08 00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 09 00 04 00 0B 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C
- 00 03:表示三个方法,默认的构造函数
- 00 01:权限修饰符 ACC_PUBLIC
- 00 07: 指向常量池索引
- 00 08:指向常量池索引()v
- 00 01: 表示有一个attribute_info信息,一下是attribute_info数据结构
attribute_info {
u2 attribute_name-index; #属性名的 索引值
u4 attribute_length; #属性的长度
u1 info[attribute_length]; #属性的具体数据
}
- 00 09:表示指向常量池索引值为 Code 表示方法的代码
- 00 00 00 38:表示代码的长度为56字节
- 00 02:max_stack = 2
- 00 01:max_locals = 1
- 00 00 00 0A:code_length 真正执行的指令码,也可以称之为助记符
- 2A B7 00 01 2A 04 B5 00 02 B1:
- 2A : load_0 Load reference from local variable
- B7 : invokespecial 00 01 常量池索引
- 2A :
- 04 : count iconst_1 = 4 (0x4) 复制
- B5 : putfield 赋值 00 02 对应常量池索引值为 com/java/jvm/bytecode/ByteCode1.a:I
- B1 :return
- 00 00 : 异常数量为0
- 00 02 :属性数量为2
- 00 0A :LineNumberTable 字节码与本地Java文件对应的行号关系
- 00 00 00 0A: 属性字节长度
- 00 02 00 00 00 09 00 04 00 0B :
- 00 02: 表示有两对映射
- 00 00 00 09:字节码为0 Java代码偏移量为9
- 00 04 00 08:字节码为4 Java代码偏移量为8
- 00 0B 00 00 00 0C
- 00 0B: LocalVariableTable 局部变量表
- 00 00 00 0C:12个长度
- 00 01 00 00 00 0A 00 0C 00 0D 00 00
- 00 01 :局部变量个数
- 00 00 00 0A: 开始位置0,结束位置10
- 00:索引0
- 0C: 常量池中对应的索引 this 当前对象
- 00 0D:局部变量的描述 Lcom/java/jvm/bytecode/ByteCode1
- 00 00:jdk1.6加入,动态检查
00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00
对于Java文件来讲,默认的无惨方法会隐式传入this参数,用来调用成员变量,但是对于class文件来说,this这个参数不可被忽略,因此this这个参数代表着那个啥呢?局部变量被方法隐式的加入到了第一个局部变量
对于非静态方法来讲,至少有一个参数传入
3.9.2 方法的Code表结构
- JVM预定义了部分attribute,但是编译器自己也可以实现自己的attribute写入class文件,供运行时使用
- 不同的attribute通过attribute_name_index来区分
3.9.3 Code 结构
Code Attribute 的作用是保存该方法的结构,如所对应的字节码
Code_attribute {
u2 attribute_name_index; ## 方法一致为Code
u2 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,这里存放的是处理异常的信息
- 每个exception_table表项由start_pc,end_pc,handler-pc,catch_pc组成
- start_pc和end_pc表示在code数组中的从start_pc到end_pc(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理
- handler_pc表示处理异常的代码的开始处,catch_type表示会被处理的异常类型,它值常量池的一个异常类。当catch_type为0时,表示处理所有的异常
附加属性
接下来是该方法的附加属性
LineNumberTable:这个属性用来表示code数组中的字节码和Java代码行数之间的关系,这个属性可以用来在调试的时候定位代码执行的行数
LineNumberTable结构
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
ideaj jclasslib插件
https://github.com/ingokegel/jclasslib
jclasslib 插件