Jvm学习笔记(2)
类加载
类文件结构
ClassFile{
u4 magic; //表示是否class类型的文件 cafe babe u2 minor_version; //十六进制表示 数组数组和jdk版本有对应关系 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; attrbute_info attrbutes[attrbutes_count]; }
常量池
- constant_pool 以1为表结构 ~ constant_pool_count - 1.索引。它显示了背后有多少常量项。
- 常量池主要储存两类常量:
字面量(Literal)
和符号引用(Symbolic Refrences)
- 它包含了class文件结构及其子结构中引用的所有字符串常量、类别或接口名、字段名等常量。常量池中的每个项目都有相同的特征。第一个字节作为类型标记,用于确定项目更改的格式,称为 tag byte(标记字节,标签字节)。
类型 | 标志(或标志) | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8编码字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点字面量 |
CONSTANT_Class_info | 7 | 引用类或接口符号 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 引用字段符号 |
CONSTANT_Methodref_info | 10 | 引用类法中的符号 |
CONSTANT_InterfaceMethodref_info | 11 | 引用接口中方法的符号 |
CONSTANT_NameAndType_info | 12 | 引用字段或方法的符号 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示动态方法调用点 |
在解释这些常量之前,我们应该找出几个概念。 常量池主要储存两类常量:字面量(Literal)
和符号引用(Symbolic Refrences)
。如下表:
常量 | 具体的常量 |
---|---|
字面量 | 文本字符串 |
声明为final的常量值 | |
符号引用 | 类别和接口的全限定名 |
字段的名称和描述符 | |
方法的名称和描述符 |
字节码指令
javap工具
Oracle提供了javap反编译工具class文件
javap -v HelloWorld.class
Last modified 2022-7-8; size 559 bytes
MD5 checksum 5053c7c4ad0a79418343e5c4e0e97476
Compiled from "HelloWorld.java"
public class com.gao.demo.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/gao/demo/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/gao/demo/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/gao/demo/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.gao.demo.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/gao/demo/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
图解运行流程
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
}
}
Last modified 2022-7-8; size 475 bytes
MD5 checksum 19b70530830a66824666ba8fd66fdc33
Compiled from "Demo.java"
public class com.gao.demo.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V
#2 = Class #24 // java/lang/Short
#3 = Integer 32768
#4 = Class #25 // com/gao/demo/Demo
#5 = Class #26 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/gao/demo/Demo;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 SourceFile
#22 = Utf8 Demo.java
#23 = NameAndType #6:#7 // "<init>":()V
#24 = Utf8 java/lang/Short
#25 = Utf8 com/gao/demo/Demo
#26 = Utf8 java/lang/Object
{
public com.gao.demo.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/gao/demo/Demo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
3 8 1 a I
6 5 2 b I
10 1 3 c I
}
SourceFile: "Demo.java"
类加载器会将main方法所在类进行类加载的操作,将字节数据读取到内存里来。
运行时常量池是方法区的一部分
比较小的数字不存放在常量池中,比如int a = 10
,如果数字的范围超过了整数的最大值,就会存放在常量池中。
将方法区字节码放入方法区后,main线程开始运行,分配栈帧。
浅绿色代表局部变量表,蓝色代表操作数栈。
(stack = 2,locals = 4) 对应操作数栈有2个空间(每个空间4个字节),局部变量表中有4个槽位
bipush 10
- 将一个byte压入操作数栈(其长度会补齐四个字节),类似的命令还有sipush(将short压入操作数栈),ldc(将一个int压入操作数栈),ldc2_w,(将一个long压入操作数栈,分两次压入,因为long是8字节)
istore 1 将操作数栈的数弹出,存入局部变量表中,1代表。此时a就被赋予了10。
ldc #3
- 从常量池加载#3数据到操作数栈
- short.max_value是32767,所以32768 = short.Max + 1,这是在编译过程中就准备好的 istore2(局部变量表中的二号槽位)
给b赋值 iload1,load2
读取局部变量表1和2的数到操作数栈 iadd
弹出操作数栈的两个数,将结果存入操作数栈。
istore3
给c赋值(局部变量表中的三号槽位)
getstatic #4 invokevirtual #5
- 找到常量池 #5项
- 定位到方法区java/io/PrintStream.println(I) v方法
- 生成新的栈帧(分配locals,stack等)
- 传递参数,执行新栈帧中的字节码
控制指令
- byte,short,char 都会按int比较,因为操作数栈都是4字节
- goto 用来进行跳转到指定行号的字节码
// 从字节码角度来分析:条件判断指令
public class T04_ByteAnalyseIf {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
字节码:使用javap -v T04_ByteAnalyseIf.class,将java程序对应的字节码如下,并做了执行的注释。
0: iconst_0 // int型常量值0进操作数栈
1: istore_1 // 从操作数栈弹出数据存储局部变量表1号槽位
2: iload_1 // 从局部变量表1号槽位中加载数据到操作数栈中
3: ifne 12 // 当栈顶int型数值不等于0时跳转到12行
6: bipush 10 // 将一个byte型常量值10 推送至栈顶
8: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
9: goto 15 // 跳转到15行
12: bipush 20 // 将一个byte型常量值20 推送至栈顶
14: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
15: return // 当前方法返回void
思考:以上比较指令中没有long, float, double 的比较,那么它们要比较怎么办?
其实循环控制还是前面介绍的那些指令,例如while循环:
// 从字节码角度来分析:循环控制指令
public class T05_ByteAnalyseWhile {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
T05_ByteAnalyseWhile 字节码:使用javap -v T05_ByteAnalyseWhile.class,将java程序对应的字节码如下,并做了执行的注释。
0: iconst_0 // int型常量值0进栈
1: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
2: iload_1 // 第二个int型局部变量进栈,从0开始计数
3: bipush 10 // 将一个byte型常量值推送至栈顶
5: if_icmpge 14 // 比较栈顶两int型数值大小,当结果大于等于0时跳转到14行
8: iinc 1, 1 // 指定int型变量增加指定值,即自增1
11: goto 2 // 无条件跳转
14: return // 当前方法返回void
上述是从字节码角度分析while,下面是从字节码角度分析do while:
// 从字节码角度来分析:循环控制do while指令
public class T06_ByteAnalyseDoWhile {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}
T06_ByteAnalyseDoWhile 字节码:使用javap -v T06_ByteAnalyseDoWhile.class,将java程序对应的字节码如下,并做了执行的注释。
0: iconst_0 // int型常量值0进栈
1: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
2: iinc 1, 1 // 指定int型变量增加指定值,即自增1
5: iload_1 // 第二个int型局部变量进栈,从0开始计数
6: bipush 10 // 将一个byte型常量值推送至栈顶
8: if_icmplt 2 // 比较栈顶两int型数值大小,当结果小于0时跳转
11: return // 当前方法返回void
上述是从字节码角度分析do while,下面是从字节码角度分析 for 循环:
// 从字节码角度来分析:循环控制 for 指令
public class T07_ByteAnalyseFor {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
}
}
}
T07_ByteAnalyseFor 字节码:使用javap -v T07_ByteAnalyseFor.class,将java程序对应的字节码如下,并做了执行的注释。
0: iconst_0 // int型常量值0进栈
1: istore_1 // 将栈顶int型数值存入第二个局部变量,从0开始计数
2: iload_1 // 第二个int型局部变量进栈,从0开始计数
3: bipush 10 // 将一个byte型常量值推送至栈顶
5: if_icmpge 14 // 比较栈顶两int型数值大小,当结果大于等于0时跳转到14行
8: iinc 1, 1 // 指定int型变量增加指定值,即自增1
11: goto 2 // 无条件跳转
14: return // 当前方法返回void
注意:比较while 和 for 的字节码,会发现它们是一模一样的,殊途也能同归。所以我们编写的while 循环 与 for循环在底层是一样的执行。
构造方法
public class demo{
static int i = 10;
static{
i = 20;
}
static{
i = 30;
}
}
编译器会按从上至下的顺序,收集所有static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法()V:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #3 // Field i:I
5: bipush 20
7: putstatic #3 // Field i:I
10: bipush 30
12: putstatic #3 // Field i:I
15: return
public class Demo4 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo4(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo4 d = new Demo4("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按的顺序,收集所有 {} 代码块和成员变量赋值的代码,,但内的代码
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
//原始构造方法在最后执行
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
方法调用
public class Demo5 {
public Demo5() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo5 demo5 = new Demo5();
demo5.test1();
demo5.test2();
demo5.test3();
Demo5.test4();
}
}
不同方法在调用时,对应的虚拟机指令有所区别
- 私有、构造、被final修饰的方法,在调用时都使用指令
- 普通成员方法在调用时,使用指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
- 静态方法在调用时使用指令
字节码
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/nyima/JVM/day5/Demo5
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: invokestatic #7 // Method test4:()V
23: return
- new 是创建【对象】,给对象分配堆内存,执行成功会将【】压入操作数栈
- dup 是赋值操作数栈栈顶的内容,本例即为【】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”😦)V (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
多态原理
public class Demo2 {
public static void test(Animal animal){
animal.eat();
System.out.println(animal.toString());
}
public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}
static abstract class Animal{
public abstract void eat();
@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}
static class Dog extends Animal{
@Override
public void eat() {
System.out.println("啃骨头");
}
}
static class Cat extends Animal{
@Override
public void eat() {
System.out.println("吃鱼");
}
}
}
在jdk目录下输入下列命令
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
使用jps指令查看进程数
根据进程号在HSDB中连接
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用指令
在执行invokevirtual指令时,经历了以下几个步骤
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的Class
- Class结构中有
- 查询vtable找到方法的具体地址
- 执行方法的字节码
异常捕获
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
对应字节码指令
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
//多出来一个异常表
Exception table:
from to target type
2 5 8 Class java/lang/Exception
- 可以看到多出来一个 Exception table 的结构,[from, to) 是(也就是检测2~4行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的2号位置(为e)
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (ArithmeticException e) {
i = 20;
}catch (Exception e) {
i = 30;
}
}
}
对应的字节码
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 19
8: astore_2
9: bipush 20
11: istore_1
12: goto 19
15: astore_2
16: bipush 30
18: istore_1
19: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/Exception
- 因为异常出现时, Exception table 中,所以局部变量表 slot 2 位置
public class Demo2 {
public static void main(String[] args) {