深入理解JVM-字节码
字节码
- Class文件
- Java字节码整体结构
- Java字节码结构
- 常量池中数据类型的结构总表
- 访问标志
- 字段表集合
- 方法表
- 方法的属性结构
- JVM规范预定义的attribute
- Code结构
- 附加属性
- 代码示例
- 示例1
- 示例2
- 示例3
Class文件
Java源程序被编译器编译后称为class文件,而Class文件则由自己的格式,其采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型,无符号数和表。正是因为class文件拥有自己的格式才使得Java程序可以跨平台执行。
Java字节码整体结构
字节数 | 名称 | 含义 |
---|---|---|
4 | Magic Number | 魔数,值为0xCAFEBABE |
2+2 | Version | 包括minor version和major version |
2 + n | Constant Pool | 字符串常量池 |
2 | Access Flags | 权限 |
2 | This Class Name | 当前类的全限定类名,指向常量池 |
2 | Super Class Name | 父类的全限定类名,指向常量池 |
2+n | Interfaces | 所实现的接口以及接口表 |
2+n | Fields | 拥有的字段以及字段表 |
2+n | Methods | 拥有的方法以及方法表 |
2+n | Attributes | 拥有的属性以及属性表 |
Java字节码结构
Class字节码中有两种数据类型:
- 字节数据直接量:这是基本的数据类型。共分为u1、u2、u4、u8四种,分别代表连续的一个字节、2个字节、4个字节、8个字节组成的整体数据。
- 表(数组):表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。
常量池中数据类型的结构总表
上面的表中描述了11中常用的数据类型,jdk1.7之后又增加了三种,CONSTANT\_MethodHandle\_info、CONSTANT\_MethodType\_info以及CONSTANT\_MethodDynamic\_info。
访问标志
如果一个类是public以及final的,那么其标志值为分别对应的标志值相或,所以其标志值为0x0011。
字段表集合
字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部的局部变量。
fields_count: u2 (表示字段的个数)
方法表
method_count:u2 (表示方法的个数)
方法的属性结构
方法中的每个属性都是一个attribute_info结构
JVM规范预定义的attribute
- JVM预定义了部分attribute,但是编译器自己也可以实现自己的attribute写入class文件里,供运行时使用。
- 不同的attribute通过attribute_name_index来区分。
Code结构
Code attribute的作用是保存该方法的结构
- attribute_length 表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。
- max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
- max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
- code_length表示该方法所包含的字节码的字节数以及具体的指令码
- 具体字节码即是该方法被调用时,虚拟机所指向的字节码
- exception_table,异常表,这里存放的是处理异常的信息
- 每个exception_table表项由start_pc,end_pc,handle_pc,catch_type组成
- start_pc和end_pc表示在code数组中的从start_pc和end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常由这个表项来处理
- handler_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有异常
附加属性
方法的附加属性:
- LineNumberTable:这个属性用来表示code数组·中的字节码和Java代码行数之间的关系。这个属性可以用来在调试的时候定位代码执行的行数。
代码示例
示例1
public class MyTest1 {
private int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
E:\IdeaProjects\jvm_lecture\target\classes>javap -verbose com/wangzhao/jvm/bytecode/MyTest1
Classfile /E:/IdeaProjects/jvm_lecture/target/classes/com/wangzhao/jvm/bytecode/MyTest1.class
Last modified 2019-7-21; size 497 bytes
MD5 checksum 5fea812ebd710c6be8d438b7e0959a03
Compiled from "MyTest1.java"
public class com.wangzhao.jvm.bytecode.MyTest1
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/wangzhao/jvm/bytecode/MyTest1.a:I
#3 = Class #22 // com/wangzhao/jvm/bytecode/MyTest1
#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/wangzhao/jvm/bytecode/MyTest1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest1.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/wangzhao/jvm/bytecode/MyTest1
#23 = Utf8 java/lang/Object
{
public com.wangzhao.jvm.bytecode.MyTest1();
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_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 7: 0
line 10: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/wangzhao/jvm/bytecode/MyTest1;
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/wangzhao/jvm/bytecode/MyTest1;
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 18: 0
line 19: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/wangzhao/jvm/bytecode/MyTest1;
0 6 1 a I
}
SourceFile: "MyTest1.java"
通过javap命令,我们对MyTest1的class文件解析可得到如下结果:
通过常量池,我们可以找到这个类拥有哪些字段,方法以及父类和接口的全限定类名。需要注意的是,常量池中的索引是从1开始,并没有使用0是因为0表示不引用任何一个常量。
可以看到==对于类中实例变量的赋值,是在构造方法中完成的。==同时编译器为我们自动增加了一个构造方法。
对于getA()方法,我们并没有传入参数,但是参数个数却为1,这是因为对于Java类中的每一个实例方法(非static)方法,其在编译后所生成的字节码当中,方法参数的数量总是会比源代码中方法参数的数量多一个(this),它位于方法的参数的第一个参数位置处;这样,我们就可以在java的实例方法中使用this来去访问当前对象的属性以及其他方法。
示例2
public class MyTest2 {
String str = "Welcome";
private int x = 5;
public static Integer in = 10;
public static void main(String[] args) {
MyTest2 myTest2 = new MyTest2();
myTest2.setX(8);
in = 20;
}
public void setX(int x) {
this.x = x;
}
private static void test(String str){
synchronized (str){
System.out.println("hello world");
}
}
}
可以看到synchronized与两个指令相关联,分别为minitorenter以及monitorexit。
minortorenter的作用是:执行该指令的线程尝试获取相关对象的监视器的所有权,如果监视器的计数为0,则线程进入监视器并将其计数设置为1。如果该线程已经拥有该对象的所有权,则监视器的计数加1(可重入)。而其他线程将阻塞,直到该对象的监视器计数为1。
minortorexit的作用是:拥有当前对象监视器所有权的线程递减关联对象的监视器计数,直到为0,当前线程退出监视器。
对于静态的字段,编译器会生成一个静态构造代码块,完成静态成员变量的赋值。
示例3
public class MyTest3 {
public void test(){
try {
InputStream is = new FileInputStream("test.txt");
ServerSocket serverSocket = new ServerSocket(9999);
}catch (FileNotFoundException ex){
}catch (IOException ex){
}catch (Exception ex){
}finally {
System.out.println("finally");
}
}
}
可以看到对于异常的处理流程是通过异常表,罗列出所有可能的路径。
当异常处理存在finally语句块时,现代化的JVM采取的处理方式是将finally语句块的字节码拼接到每一个catch块后面,换句话说,程序中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。
还没有评论,来说两句吧...