注意:所有文章除特别说明外,转载请注明出处.
字节码
[toc]
反编译
1. javap 路径(.class文件) 能够反编译出Java文件的字节码
2. javap -c 路径 打印出.class文件的详细信息
3. javap -verbose .class文件的路径 反编译的文件内容
javap -verbose 命令
在使用 javap -verbose 命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。
Java字节码整体结构
ClassFile {
u4 magic;
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];
}
1. 魔数,所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE。
2. 版本号,在魔数之后的4个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号)。这里的版本号为 00 00 00 34,换算成十进制,表示次版本号为0,主版本号为52。所以该文件的版本号为:1.8.0。通过java -version命令验证。
3. 常量池(constant pool),紧接着在主版本号之后的就是常量池的入口。一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,Java中声明为final的常量值等。而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
4. 常量池的总体结构,Java类所对应的常量池主要由 常量池数量 与 常量池数组 这两部分共同构成。常量池数量紧跟在主版本号后面,占据两个字节。常量池数组则紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同元素的类型、结构都是不同的,长度当然也就不同。但是每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据一个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。
提示:常量池数组中元素的个数 = 常量池数 - 1 (其中0暂时不使用)。目的是满足某些常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池”的含义:根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,该常量就对应null值,所以常量池的索引从1而非0开始。
6. 在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。
7. 对于数组类型来说,每一个维度使用一个前置的 [ 来表示,如int[]被记录为 [I,String[][]被记录为 [[Ljava/lang/String。
8. 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()内。如方法:String getA(int id, String)的描述符是:(I, Ljava/lang/String;) Ljava/lang/String;
9. 访问修饰符,访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract。如果是类,是否被声明为final。
10. 类名称
11. 父类名称
12. 接口
13. 字段表,字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
14. 类的方法
methods_count: u2
类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count
方法表结构(前三个字段和field_info一样)
method_info{
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
方法中每个属性都是一个attribute_info结构
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
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 stack_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attribute_length];
}
1. atrribute_length 表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段
2. max_stack 表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
3. max_locals 表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
4. code_length 表示该方法所包含的字节码的字节数以及具体的指令码,具体字节码表示该方法在调用时,虚拟机所执行的字节码
5. exception_table 表示存放的是处理异常的信息
6. 每个exception_table表项是由 stack_pc | end_pc | handler_pc | catch_pc 组成
15. 当前类附加属性
提示:Class字节码中有两种数据类型:1. 字节数据直接量:这是基本的数据类型。共细分为 u1 | u2 | u4 | u8 四种,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。 2. 表(数组):表是由多个基本数据或其它表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在组成表的成分所在的位置和顺序都是已经严格定义好的。
提示:IDEA中字节码查看插件:jclasslib view 。
提示:在使用 javap -verbose 命令不能够打印出类中的私有成员方法。需要在 javap -verbose -p 后面加上-p的属性才会打印出对应的private属性方法。
分析private方法下的synchronized关键字生成的字节码
//1. 在private属性下未使用synchronized的字节码
private void setX(int);
descriptor: (I)V
flags: ACC_PRIVATE
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #4 // Field x:I
5: return
LineNumberTable:
line 12: 0
line 13: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/jvm/xidian/edu/cn/bytecode/Test2;
0 6 1 x I
//2. 在private方法中定义了synchronized的字节码
private synchronized void setX(int);
descriptor: (I)V
flags: ACC_PRIVATE, ACC_SYNCHRONIZED
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #4 // Field x:I
5: return
LineNumberTable:
line 12: 0
line 13: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/jvm/xidian/edu/cn/bytecode/Test2;
0 6 1 x I
在synchronized关键字不位于方法内部时,没有锁的释放和进入。monitorenter | monitorexit。
Class文件结构中常量池中11种数据类型的结构总表
根据上面的两个表格可以确定字节码之间的关系。
Java字节码整体结构
1. magic number 2个字节
2. version 2+2个字节
3. constant pool 2+n个字节
4. access flags 2个字节
5. this class name 2个字节
6. super class name 2个字节
7. interfaces 2+n个字节
8. fields 2+n个字节
9. methods 2+n个字节
10 attributes 2+n个字节
异常处理
提示:对于Java类中的每个实例方法(非static方法),其在编译后所生成的字节码中,方法参数的数量总是会比源代码中方法参数的数量多一个(this),它位于方法的第一个参数的位置上。这样我们可以在Java的实例方法中使用this去访问当前对象的属性以及其它方法。这个操作是在编译期间完成的,即由javac编译器在编译的时候将对this的访问转化为一个普通实例方法参数的访问,接下来在运行期间,由JVM在调用实例方法时,自动向实例方法传入该this参数。所以在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量。
Java字节码对于异常的处理方式:
1. 统一采用异常表的方式来对异常进行处理。
2. 在jdk1.4.2之前的版本中,并不是使用异常表的方式来对异常进行处理的,而是采用特定的指令的方式。
3. 当异常处理存在finally语句块时,现代化的JVM采取的处理方式是将finally语句块的字节码拼接到每一个catch块后面。即:程序中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。
栈帧(stack frame)
栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
符号引用与直接引用
1. 有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析。另外一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,体现了Java的多态性。
方法调用
1. invokeinterface 调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
2. invokestatic 调用静态方法
3. invokespecial 调用自己的私有方法、构造方法(<init>)以及父类的方法
4. invokevirtual 调用虚方法,运行期动态查找的过程
5. invokedynamic 动态调用方法
静态解析的4种情形
1. 静态方法
2. 父类方法
3. 构造方法
4. 私有方法(无法被重写)
以上四类方法称作非虚方法,它们在类加载阶段就可以将符号引用转换为直接引用。
方法的静态分派
方法重载是一种静态行为,在编译期就可以完全确定。
方法的动态分派
方法的动态分派涉及到一个重要的概念:方法接受者。invokevirtual字节码指令的多态查找流程。
方法重载与方法重写
在比较方法重载(overload)与方法重写(overwrite)我们可以得到这两者的区别:
1. 方法重载是静态的,是编译期行为。方法重写是动态的,是运行期行为。
针对方法调用动态分配的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table, vtable),针对invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interface method table, itable)。
现代JVM在执行Java程序的时候,通过都会将解释执行与编译执行二者结合起来进行。
1. 解释执行:通过解释器来读取字节码,遇到相应的指令就去执行该指令。
2. 编译执行:通过即时编译器(Just In Time,JIT)将字节码转换为本地机器码来执行。现代JVM会根据代码热点来生成相应的本地机器码。
基于栈的指令集与基于寄存器的指令集之间的关系
1. JVM执行指令时所采取的方式是基于栈的指令集。
2. 基于栈的指令集主要的操作有入栈与出栈两种方式。
3. 基于栈的指令集的优势在于它可以在不同平台之间移植,而基于寄存器的指令集是与硬件架构紧密关联的,无法做到移植。
4. 基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多。基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由CPU执行的,它是在高速缓冲区中进行执行的,速度要快很多。虽然虚拟机可以采用一些优化手段,但是总体来说,基于栈的指令集的执行速度要慢一些。