1) 概述
- JVM的字节码执行引擎,功能基本就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果。
- 实现方式可能有通过解释器直接执行字节码,或者适通过及时编译器产生本地代码,也就是编译执行,当让也可能两者皆有。
- HotSpot就是两者皆有,使用频率较多的代码JIT动态编译为本地代码,频率较少的就解释执行。
2) 栈帧概述
- 栈帧是用于执行JVM进行方法调用和方法执行的数据结构。
- 栈帧随着方法调用而创建,随着方法结束而销毁。
- 栈帧里面存储了方法的 局部变量、操作数栈、动态连接、方法返回地址等信息。
3) 局部变量表
- 局部变量表:用来存放方法参数和方法内部定义的局部变量的存储空间。
- 以slot为单位,目前一个slot存放32位以内的数据类型
- 对于64位的数据占2个slot
- 对于实例方法,第0位slot存放的是this,然后从1到n,依次分配给参数列表
- 对于静态方法,则从0位开始依次分配给参数列表
- 然后根据方法体内部定义的变量顺序和作用域来分配slot
public class Test1 {
public int add(int a, int b) {
int c = a + b;
return a + b + c;
/* javap -verbose 得到slot分配情况
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/jvm/stack/Test1;
0 10 1 a I
0 10 2 b I
4 6 3 c I
*/
}
}
- slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为。
// -Xms10m -Xmx10m
public static void main(String[] args) {
{
byte[] bs1 = new byte[1024 * 1204];
byte[] bs2 = new byte[1024 * 1204];
byte[] bs3 = new byte[1024 * 1204];
/* 此时slot的情况
0 -- args -- 堆引用
1 -- bs1 -- 堆引用
2 -- bs2 -- 堆引用
3 -- bs3 -- 堆引用
*/
System.gc();
printMemory(); // freeMemory :2.98663330078125
bs1 = null;
/* 显式将bs1置为null, slot 1则空闲出来*/
}
System.gc();
printMemory(); // freeMemory :4.8661346435546875
/* 代码块结束,上述的bs1 bs2 bs3变量的作用域已经结束,
所占用的slot可以被后续定义的变量复用 */
int a = 5;
int b = 5;
/* 此时slot的使用情况
0 -- args -- 堆引用
1 -- a -- I
2 -- b -- I
3 -- bs3 -- 堆引用
*/
System.gc();
printMemory(); // freeMemory :6.8662872314453125
String c = "a";
/* 此时slot的使用情况
0 -- args -- 堆引用
1 -- a -- I
2 -- b -- I
3 -- c -- 堆引用
*/
System.gc();
printMemory(); // freeMemory :8.866722106933594
}
public static void printMemory() {
//System.out.println("totalMemory:" + Runtime.getRuntime().totalMemory()/1024.0/1024.0);
System.out.println("freeMemory :" + Runtime.getRuntime().freeMemory()/1024.0/1024.0);
//System.out.println("maxMemory :" + Runtime.getRuntime().maxMemory()/1024.0/1024.0);
}
4) 操作数栈
- 操作数栈:用来存放方法运行期间,各个指令操作的数据。
- 操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配
- 数据类型和插槽位置的数据类型必须一一对应:比如指令 iconst_1,那么插槽1位置的类型必须是I
- 虚拟机在实现栈帧的时候可能会做一些优化,让两个栈帧出现部分重叠区域,以存放公用的数据
package com.lc.sprnigcloud.stack;
/**
* @author hahadasheng
* @since 2020/12/28
*/
public class Test {
public static void main(String[] args) {
test();
}
/* LocalVariableTable:
Start Length Slot Name Signature
2 51 0 a I
16 37 1 b I
35 18 2 c I
*/
public static void test() {
int a = 1;
a = a++;
/*
0: iconst_1 -> 常数1
1: istore_0 -> 将常数1存放在局部变量表slot_0的位置,也就是a
2: iload_0 -> 将slot_0中存放a的值1 放入栈中
3: iinc 0, 1 -> slot_0中a的值自增1,1 + 1 = 2
6: istore_0 -> 将栈中的值1赋值到slot_0的位置,所以此时a的值还是为1
*/
System.out.println(a); // 1
int b = 1;
b = b++ * ++b;
/*
14: iconst_1 -> 常数1
15: istore_1 -> 将1放在slot_1的位置
16: iload_1 -> 将slot_1的值1入栈 栈:[1]
17: iinc 1, 1 -> slot_1位置的1自增1,1+1=2
20: iinc 1, 1 -> slot_1位置的2自增1,2+1=3
23: iload_1 -> 将slot_1的值3入栈 栈:[3,1]
24: imul -> 将栈中的两个数相乘 3 * 1 = 3,栈中的值为3
25: istore_1 -> 将栈中值3放在slot_1的位置
*/
System.out.println(b); // 3
int c = 1;
c = ++c * c++;
/*
33: iconst_1 -> 常数1
34: istore_2 -> 将常数1放在局部变量表slot_2的位置
35: iinc 2, 1 -> slot_2位置数自增1,1+1=2
38: iload_2 -> 将slot_2位置的2入栈 栈:[2]
39: iload_2 -> 将slot_2位置的2入栈 栈:[2,2]
40: iinc 2, 1 -> 将slot_2位置的2自增1,2+1=3
43: imul -> 将栈中的值取出来进行乘法操作 2*2=4,栈中的值变为4
44: istore_2 -> 将栈中值4存放在slot_2的位置
*/
System.out.println(c); // 4
}
}
5) 动态连接
- 动态连接:每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程中的动态连接。
- 动态连接分类:
- 静态解析:类加载的时候,符号引用就转化成直接引用
- 动态连接:运行期间转换为直接引用(动态分派)
6) 方法返回地址
- 方法返回地址:方法执行后返回的地址,
- 无论正常退出还是异常退出,都得返回到方法被调用的位置,程序才能继续执行
7) 方法调用
- 方法调用:方法调用就是确定具体调用哪一个方法,并不涉及方法内部的执行过程。
- 不一定要调用这个方法
- 部分方法是直接在类加载的解析阶段,就确定了直接引用关系
- 静态方法、私有方法、实例构造器、父类方法
- 但是对于实例方法,也称虚方法,因为重载和多态,需要运行期间动态委派
- 例如虚拟机调用此方法的指令为
invokevirtual
- 例如虚拟机调用此方法的指令为
8) 静态分派和动态分派
分派:分为静态分派和动态分派
-
静态分派:所有依赖静态类型来定位方法执行版本的分派方式
- 比如:重载方法(依据传参确定)
-
动态分派:根据运行期间的实际类型来定位方法执行版本的分派方式,
- 比如:重写覆盖方法、实现的接口等
-
单分派和多分派:就是按照分派思考的纬度,多于一个的就算多分派,只有一个的称为单分派
- 只有一个确认的,没有重载、重写、多态等可能有多个可能的就是单分派
如何执行方法中的字节码指令:JVM通过基于栈的字节码解释器引擎来执行指令,JVM的指令集也是基于栈的。