打卡学习JVM,第六天
本人学习过程中所整理的代码,源码地址
- Java字节码文件剖析
测试用例:
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(0);
in = 20;
}
private void setX(int x) {
this.x = x;
}
}
- 使用java -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息
Classfile /G:/GitHub/jvm-study/build/classes/java/main/bytecode/MyTest2.class
Last modified 2020-3-19; size 814 bytes
MD5 checksum baffcdf4a49b347cecf3122ddf8455e2
Compiled from "MyTest2.java"
public class bytecode.MyTest2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#34 // java/lang/Object."<init>":()V
#2 = String #35 // Welcome
#3 = Fieldref #5.#36 // bytecode/MyTest2.str:Ljava/lang/String;
#4 = Fieldref #5.#37 // bytecode/MyTest2.x:I
#5 = Class #38 // bytecode/MyTest2
#6 = Methodref #5.#34 // bytecode/MyTest2."<init>":()V
#7 = Methodref #5.#39 // bytecode/MyTest2.setX:(I)V
#8 = Methodref #40.#41 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#9 = Fieldref #5.#42 // bytecode/MyTest2.in:Ljava/lang/Integer;
#10 = Class #43 // java/lang/Object
#11 = Utf8 str
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 x
#14 = Utf8 I
#15 = Utf8 in
#16 = Utf8 Ljava/lang/Integer;
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 Lbytecode/MyTest2;
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 args
#27 = Utf8 [Ljava/lang/String;
#28 = Utf8 myTest2
#29 = Utf8 setX
#30 = Utf8 (I)V
#31 = Utf8 <clinit>
#32 = Utf8 SourceFile
#33 = Utf8 MyTest2.java
#34 = NameAndType #17:#18 // "<init>":()V
#35 = Utf8 Welcome
#36 = NameAndType #11:#12 // str:Ljava/lang/String;
#37 = NameAndType #13:#14 // x:I
#38 = Utf8 bytecode/MyTest2
#39 = NameAndType #29:#30 // setX:(I)V
#40 = Class #44 // java/lang/Integer
#41 = NameAndType #45:#46 // valueOf:(I)Ljava/lang/Integer;
#42 = NameAndType #15:#16 // in:Ljava/lang/Integer;
#43 = Utf8 java/lang/Object
#44 = Utf8 java/lang/Integer
#45 = Utf8 valueOf
#46 = Utf8 (I)Ljava/lang/Integer;
{
java.lang.String str;
descriptor: Ljava/lang/String;
flags:
private int x;
descriptor: I
flags: ACC_PRIVATE
public static java.lang.Integer in;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC
public bytecode.MyTest2();
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: ldc #2 // String Welcome
7: putfield #3 // Field str:Ljava/lang/String;
10: aload_0
11: iconst_5
12: putfield #4 // Field x:I
15: return
LineNumberTable:
line 7: 0
line 9: 4
line 11: 10
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lbytecode/MyTest2;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #5 // class bytecode/MyTest2
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: aload_1
9: iconst_0
10: invokespecial #7 // Method setX:(I)V
13: bipush 20
15: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
18: putstatic #9 // Field in:Ljava/lang/Integer;
21: return
LineNumberTable:
line 16: 0
line 18: 8
line 20: 13
line 21: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 args [Ljava/lang/String;
8 14 1 myTest2 Lbytecode/MyTest2;
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 24: 0
line 25: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lbytecode/MyTest2;
0 6 1 x I
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #9 // Field in:Ljava/lang/Integer;
8: return
LineNumberTable:
line 13: 0
}
SourceFile: "MyTest2.java"
- 魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE
- 魔数之后的4个字节为版本信息,其中前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号)
- 常量池(constant pool):紧接着主版本号之后的就是常量池入口。一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,比如说Java类中定义的方法与变量信息都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,Java中声明为final的常量值等; 符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
- 常量池的总体结构:Java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成。常量池紧跟在主版本后面,占据2个字节;常量池数组则紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的;但是每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据1个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。**值得注意的是,常量池数组中元素的个数=常量池数-1(索引从1开始),目的是满足某些常量池索引值的数据在特定情况下需要表达不引用任何一个常量池的含义;**根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值,所以常量池的索引从1而非0开始。
- 在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;
- 对于数组类型,每一个维度使用一个前置的[来表示
- 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:String getRealnamebyIdAndNickname(int id,String name)的描述符为:(I, Ljava/lang/String;) Ljava/lang/String
Java字节码整体结构
Class字节码中有两种数据类型:
(1)字节数据直接量:这是基本的数据类型。共细分为u1、u2、u4、u8四种,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。
(2)表/数组:表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体:组成表的成分所在的位置和顺序都是已经严格定义好的。
- 访问标识符(Access Flags)
访问标志信息包括了该class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被定义成final
- 字段表(Fields)
字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量和实例变量,但是不包括方法内部声明的局部变量
- 方法表(Methods)
方法的属性结构:
方法中的每个属性都是一个attribute_info结构:
- JVM预定义了部分attribute,但是编译器自己也可以实现自己的attribute写入class文件里,供运行时使用
- 不同的attribute通过attribute_name_index来区分
//attribute_info格式:
attribute_info{
//attribute_name_index值为code,则为Code结构Code的作用是保存该方法的结构,所对应的的字节码
u2 attribute_name_index;
//表示attribute所包含的字节数,不包含attribute_name_index和attribute_length
u4 attribute_length;
u1 info[attribute_length]
}
- 通过Java字节码分析this关键字
- 对于Java类中的每一个实例方法(非static方法),在编译后所生成的字节码当中方法参数的数量总是会比源代码种方法参数的数量多一个(this),它位于方法的第一个参数位置处;这样,我们就可以在Java的实例方法中使用this去访问当前对象的属性以及其它方法
- 这个操作是在编译期间完成的,即由javac编译器在编译的时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,由JVM在调用实例方法时,自动向实例方法传入该this参数。所以,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量
- Java字节码对于异常的处理方式
public class MyTest3 {
public void test() {
try {
FileInputStream fis = new FileInputStream("test.txt");
ServerSocket serverSocket = new ServerSocket(9999);
serverSocket.accept();
} catch (FileNotFoundException e) {
} catch (IOException e) {
} catch (Exception e) {
} finally {
System.out.println("finally");
}
}
}
- 统一采用异常表的方式来对一场进行处理
- 在jdk1.4.2之前的版本中,并不是使用异常表的方式来对异常进行处理的,而是采用特定的指令这种方式
- 当异常处理存在finally语句块中,现代化的JVM采取的处理方法是将finally语句块的字节码拼接到每一个catch块后面,换句话说,代码中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码
0 new #2 <java/io/FileInputStream>
3 dup
4 ldc #3 <test.txt>
6 invokespecial #4 <java/io/FileInputStream.<init>>
9 astore_1
10 new #5 <java/net/ServerSocket>
13 dup
14 sipush 9999
17 invokespecial #6 <java/net/ServerSocket.<init>>
20 astore_2
21 aload_2
22 invokevirtual #7 <java/net/ServerSocket.accept>
25 pop
26 getstatic #8 <java/lang/System.out>
29 ldc #9 <finally>
31 invokevirtual #10 <java/io/PrintStream.println>
34 goto 92 (+58)
37 astore_1
38 aload_1
39 invokevirtual #12 <java/io/FileNotFoundException.printStackTrace>
42 getstatic #8 <java/lang/System.out>
45 ldc #9 <finally>
47 invokevirtual #10 <java/io/PrintStream.println>
50 goto 92 (+42)
53 astore_1
54 aload_1
55 invokevirtual #14 <java/io/IOException.printStackTrace>
58 getstatic #8 <java/lang/System.out>
61 ldc #9 <finally>
63 invokevirtual #10 <java/io/PrintStream.println> //第一次出现
66 goto 92 (+26)
69 astore_1
70 getstatic #8 <java/lang/System.out>
73 ldc #9 <finally>
75 invokevirtual #10 <java/io/PrintStream.println> //第二次出现
78 goto 92 (+14)
81 astore_3
82 getstatic #8 <java/lang/System.out>
85 ldc #9 <finally>
87 invokevirtual #10 <java/io/PrintStream.println> //第三次出现
90 aload_3
91 athrow
92 return