第 5 章 虚拟机栈
1、虚拟机栈概述
微信搜一搜: 全栈小刘,获取文章全套 pdf 版
1.1、虚拟机栈的呈现背景
文档网址
https://docs.oracle.com/javas…
虚拟机栈呈现的背景
- 因为跨平台性的设计,Java 的指令都是依据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的。
- 长处是跨平台,指令集小,编译器容易实现,毛病是性能降落,实现同样的性能须要更多的指令。
内存中的栈与堆
首先栈是运行时的单位,而堆是存储的单位
- 栈解决程序的运行问题,即程序如何执行,或者说如何解决数据。
- 堆解决的是数据存储的问题,即数据怎么放,放哪里
1.2、虚拟机栈的存储内容
虚拟机栈的根本内容
Java 虚拟机栈是什么?
Java 虚拟机栈(Java Virtual Machine Stack),晚期也叫 Java 栈。每个线程在创立时都会创立一个虚拟机栈,其外部保留一个个的栈帧(Stack Frame),对应着一次次的 Java 办法调用,栈是线程公有的
虚拟机栈的生命周期
生命周期和线程统一,也就是线程完结了,该虚拟机栈也销毁了
虚拟机栈的作用
主管 Java 程序的运行 ,它 保留办法的局部变量(8 种根本数据类型、对象的援用地址)、局部后果,并参加办法的调用和返回。
- 局部变量,它是相比于成员变量来说的(或属性)
- 根本数据类型变量 VS 援用类型变量(类、数组、接口)
1.3、虚拟机栈的特点
栈的特点
栈是一种疾速无效的调配存储形式,访问速度仅次于程序计数器。JVM 间接对 Java 栈的操作只有两个:
- 每个办法执行,随同着 进栈(入栈、压栈)
- 执行完结后的 出栈 工作
对于栈来说不存在垃圾回收问题(栈存在溢出的状况)
1.4、虚拟机栈的异样
栈中可能呈现的异样
面试题:栈中可能呈现的异样
- Java 虚拟机标准容许 Java 栈的大小 是动静的或者是固定不变的。
- 如果采纳固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量能够在线程创立的时候独立选定。
- 如果线程申请调配的栈容量超过 Java 虚拟机栈容许的最大容量,Java 虚拟机将会抛出一个 StackoverflowError 异样。
- 如果 Java 虚拟机栈能够动静扩大,并且在尝试扩大的时候无奈申请到足够的内存,或者在创立新的线程时没有足够的内存去创立对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutofMemoryError 异样。
栈异样演示
- 代码
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {System.out.println(count);
count++;
main(args);
}
}
- 递归调用 11418 次后,呈现栈内存溢出
1.5、设置栈内存大小
设置栈内存的大小
- 咱们能够应用参数 -Xss 选项来设置线程的最大栈空间,栈的大小间接决定了函数调用的最大可达深度。
-Xss1024m // 栈内存为 1024MBS
-Xss1024k // 栈内存为 1024KB
- 设置线程的最大栈空间:256KB
- 代码测试
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {System.out.println(count);
count++;
main(args);
}
}
- 递归 2471 次,栈内存溢出
2、栈的存储单位
2.1、栈的运行原理
栈存储什么?
- 每个线程都有本人的栈,栈中的数据都是以 栈帧(Stack Frame)的格局存在
- 在这个线程上 正在执行的每个办法都各自对应一个栈帧(Stack Frame)。
- 栈帧是一个内存区块,是一个数据集,维系着 办法执行 过程中的各种数据信息。
栈的运行原理
- JVM 间接对 Java 栈的操作只有两个,就是对栈帧的 压栈和出栈,遵循先进后出(后进先出)准则
-
在一条流动线程中,一个工夫点上,只会有一个流动的栈帧。即 只有以后正在执行的办法的栈帧(栈顶栈帧)是无效的
- 这个栈帧被称为 以后栈帧(Current Frame)
- 与以后栈帧绝对应的办法就是 以后办法(Current Method)
- 定义这个办法的类就是 以后类(Current Class)
- 执行引擎运行的所有字节码指令只针对以后栈帧进行操作。
- 如果在该办法中调用了其余办法,对应的新的栈帧会被创立进去,放在栈的顶端,成为新的以后帧。
- 不同线程中所蕴含的栈帧是不容许存在互相援用的,即不可能在一个栈帧之中援用另外一个线程的栈帧。
- 如果以后办法调用了其余办法,办法返回之际,以后栈帧会传回此办法的执行后果给前一个栈帧,接着,虚构机会抛弃以后栈帧,使得前一个栈帧从新成为以后栈帧。
-
Java 办法有两种返回函数的形式,但不论应用哪种形式,都会导致栈帧被弹出
- 一种是 失常的函数返回,应用 return 指令
- 另外一种是 抛出异样
代码示例:
- 代码
public class StackFrameTest {public static void main(String[] args) {StackFrameTest test = new StackFrameTest();
test.method1();}
public void method1() {System.out.println("method1()开始执行...");
method2();
System.out.println("method1()执行完结...");
}
public int method2() {System.out.println("method2()开始执行...");
int i = 10;
int m = (int) method3();
System.out.println("method2()行将完结...");
return i + m;
}
public double method3() {System.out.println("method3()开始执行...");
double j = 20.0;
System.out.println("method3()行将完结...");
return j;
}
}
- 先执行的函数,最初执行完结
method1()开始执行...
method2()开始执行...
method3()开始执行...
method3()即将结束...
method2()即将结束...
method1()执行结束...
- 反编译,能够看到每个办法前面都带有 return 语句或者 ireturn 语句
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String method1()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #8 // Method method2:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #9 // String method1()执行结束...
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
line 19: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/atguigu/java1/StackFrameTest;
public int method2();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String method2()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: bipush 10
10: istore_1
11: aload_0
12: invokevirtual #11 // Method method3:()D
15: d2i
16: istore_2
17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #12 // String method2()即将结束...
22: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: iload_1
26: iload_2
27: iadd
28: ireturn
LineNumberTable:
line 22: 0
line 23: 8
line 24: 11
line 25: 17
line 26: 25
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/atguigu/java1/StackFrameTest;
11 18 1 i I
17 12 2 m I
public double method3();
descriptor: ()D
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String method3()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: ldc2_w #14 // double 20.0d
11: dstore_1
12: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #16 // String method3()即将结束...
17: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: dload_1
21: dreturn
LineNumberTable:
line 30: 0
line 31: 8
line 32: 12
line 33: 20
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/atguigu/java1/StackFrameTest;
12 10 1 j D
2.2、栈的内部结构
栈帧内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动静链接(Dynamic Linking)(或指向运行时常量池的办法援用)
- 办法返回地址(Return Address)(或办法失常退出或者异样退出的定义)
- 一些附加信息
并行每个线程下的栈都是公有的,因而每个线程都有本人各自的栈,并且每个栈外面都有很多栈帧,栈帧的大小次要由局部变量表 和 操作数栈决定的
3、局部变量表
3.1、意识局部变量表
意识局部变量表
- 局部变量表:Local Variables,被称之为局部变量数组或本地变量表
- 定义为一个 数字数组 ,次要用于 存储办法参数和定义在办法体内的局部变量,这些数据类型包含各类根本数据类型、对象援用(reference),以及 returnAddress 类型。
- 因为局部变量表是建设在线程的栈上,是线程的公有数据,因而 不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来的 ,并保留在办法的 Code 属性的 maximum local variables 数据项中。在办法运行期间是不会扭转局部变量表的大小的。
-
办法嵌套调用的次数由栈的大小决定。一般来说,栈越大,办法嵌套调用次数越多。
- 对一个函数而言,它的参数和局部变量越多,使得局部变量表收缩,它的栈帧就越大,以满足办法调用所需传递的信息增大的需要。
- 进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会缩小。
-
局部变量表中的变量只在以后办法调用中无效。
- 在办法执行时,虚拟机通过应用局部变量表实现参数值到参数变量列表的传递过程。
- 当办法调用完结后,随着办法栈帧的销毁,局部变量表也会随之销毁。
局部变量表所需的容量大小是在编译期确定下来的
- 代码
public class LocalVariablesTest {
private int count = 0;
public static void main(String[] args) {LocalVariablesTest test = new LocalVariablesTest();
int num = 10;
test.test1();}
public void test1() {Date date = new Date();
String name1 = "atguigu.com";
test2(date, name1);
System.out.println(date + name1);
}
public String test2(Date dateP, String name2) {
dateP = null;
name2 = "songhongkang";
double weight = 130.5;
char gender = '男';
return dateP + name2;
}
}
-
反编译后,可得论断:
- 在编译期间,局部变量的个数、每个局部变量的大小都曾经被记录下来
- 所以局部变量表所需的容量大小是在编译期确定下来的
- 利用 JClassLib 也能够查看局部变量的个数
思考:
- 代码
public static void main(String[] args) {if(args == null){LocalVariablesTest test = new LocalVariablesTest();
}
int num = 10;
}
- 反编译后,提出问题:下面代码中的 test 变量跑哪儿哪了呢?
- 我预计 test 变量和 num 变量共用一个 slot
字节码中办法内部结构的分析
-
[Ljava/lang/String]:
- [] 示意数组
- L 示意援用类型
- java/lang/String 示意 java.lang.String
- 合起来就是:main() 办法的形参类型为 String[]
- 字节码,字节码长度为 16(0~15)
- 办法异样信息表
- 杂项(Misc)
- 字节码指令行号和原始 java 代码行号的对应关系
- 留神:失效行数和残余无效行数都是针对于字节码文件的行数
3.2、对于 Slot 的了解
对于 Slot 的了解
- 参数值的寄存总是 从局部变量数组索引 0 的地位开始,到数组长度 - 1 的索引完结。
- 局部变量表,最根本的存储单元是 Slot(变量槽),局部变量表中寄存编译期可知的各种根本数据类型(8 种),援用类型(reference),returnAddress 类型的变量。
- 在局部变量表里,32 位以内的类型只占用一个 slot(包含 returnAddress 类型),64 位的类型占用两个 slot(1ong 和 double)。
- JVM 会为局部变量表中的每一个 Slot 都调配一个拜访索引,通过这个索引即可胜利拜访到局部变量表中指定的局部变量值
- 当一个实例办法被调用的时候,它的办法参数和办法体外部定义的局部变量将会 依照程序 被复制到局部变量表中的每一个 slot 上
- 如果须要拜访局部变量表中一个 64bit 的局部变量值时,只须要应用前一个索引即可。(比方:拜访 long 或 doub1e 类型变量)
- 如果以后帧是由 构造方法或者实例办法 创立的,那么 该对象援用 this 将会寄存在 index 为 0 的 slot 处,其余的参数依照参数表程序持续排列。
Slot 代码示例
this 寄存在 index = 0 的地位:
- 代码
public void test3() {this.count++;}
- 局部变量表:this 寄存在 index = 0 的地位
64 位的类型(1ong 和 double)占用两个 slot
- 代码
public String test2(Date dateP, String name2) {
dateP = null;
name2 = "songhongkang";
double weight = 130.5;
char gender = '男';
return dateP + name2;
}
- weight 为 double 类型,index 间接从 3 蹦到了 5
static 无奈调用 this
- this 不存在与 static 办法的局部变量表中,所以无奈调用
public static void testStatic(){LocalVariablesTest test = new LocalVariablesTest();
Date date = new Date();
int count = 10;
System.out.println(count);
}
3.3、Slot 的反复利用
Slot 的反复利用
栈帧中的局部变量表中的槽位是 能够重用 的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量变就很有可能会复用过期局部变量的槽位,从而达到节俭资源的目标。
- 代码
public void test4() {
int a = 0;
{
int b = 0;
b = a + 1;
}
int c = a + 1;
}
- 局部变量 c 重用了局部变量 b 的 slot 地位
动态变量与局部变量的比照
变量的分类:
-
依照数据类型分:
- 根本数据类型
- 援用数据类型
- 依照在类中申明的形式分:
-
类变量:
- linking 的 prepare 阶段:给类变量默认赋值
- initial 阶段:给类变量显式赋值即动态代码块赋值
- 实例变量:随着对象的创立,会在堆空间中调配实例变量空间,并进行默认赋值
- 局部变量:在应用前,必须要进行显式赋值的!否则,编译不通过,应该是栈中数据弹出后,不会革除上次的值,再次应用时,如果不显示初始化,就会呈现脏数据
- 参数表调配结束之后,再依据办法体内定义的变量的程序和作用域调配。
- 咱们晓得 类变量表有两次初始化的机会 , 第一次是在 ” 筹备阶段 ”,执行零碎初始化,对类变量设置零值,另一次则是在 ” 初始化 ” 阶段,赋予程序员在代码中定义的初始值。
- 和类变量初始化不同的是,局部变量表不存在零碎初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无奈应用。
代码示例
- 报错:局部变量未初始化
补充阐明
- 在栈帧中,与性能调优关系最为亲密的局部就是后面提到的局部变量表。在办法执行时,虚拟机应用局部变量表实现办法的传递。
- 局部变量表中的变量也是重要的垃圾回收根节点,只有被局部变量表中间接或间接援用的对象都不会被回收。
4、操作数栈
4.1、操作数栈的特点
操作数栈的特点
操作数栈:Operand Stack
- 每一个独立的栈帧除了蕴含局部变量表以外,还蕴含一个后进先出(Last – In – First -Out)的 操作数栈,也能够称之为 表达式栈(Expression Stack)
- 操作数栈,在办法执行过程中,依据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。应用它们后再把后果压入栈,比方:执行复制、替换、求和等操作
代码举例
- 右边为 java 源代码,左边为 java 代码编译生成的字节码指令
4.2、操作数栈的作用
操作数栈的作用
- 操作数栈,次要用于保留计算过程的两头后果,同时作为计算过程中变量长期的存储空间。
- 操作数栈就是 JVM 执行引擎的一个工作区,当一个办法刚开始执行的时候,一个新的栈帧也会随之被创立进去,这时办法的操作数栈是空的(这个时候数组是有长度的,只是操作数栈为空)
- 每一个操作数栈都会领有一个明确的栈深度用于存储数值,其所需的 最大深度在编译期就定义好了 ,保留在办法的 Code 属性中,为 maxstack 的值。
-
栈中的任何一个元素都是能够任意的 Java 数据类型
- 32bit 的类型占用一个栈单位深度
- 64bit 的类型占用两个栈单位深度
- 操作数栈并非采纳拜访索引的形式来进行数据拜访的,而是 只能通过规范的入栈和出栈操作来实现一次数据拜访
- 如果被调用的办法带有返回值的话,其 返回值将会被压入以后栈帧的操作数栈中,并更新 PC 寄存器中下一条须要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类测验阶段的数据流分析阶段要再次验证。
- 另外,咱们说 Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
操作数栈的深度
通过反编译生成的字节码指令查看操作数栈的深度
5、代码追踪
操作数栈代码追踪
- 代码
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
- 反编译失去的字节码指令
0 bipush 15
2 istore_1
3 bipush 8
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return
程序执行流程如下
- 首先执行第一条语句,PC 寄存器指向的是 0,也就是指令地址为 0,而后应用 bipush 让操作数 15 入操作数栈。
- 执行完后,让 PC + 1,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表 1 的地位,咱们能够看到局部变量表的曾经减少了一个元素
- 解释为什么局部变量表索引从 1 开始,因为该办法为实例办法,局部变量表索引为 0 的地位寄存的是 this
- 而后 PC+1,指向的是下一行。让操作数 8 也入栈,同时执行 store 操作,存入局部变量表中
- 而后从局部变量表中,顺次将数据放在操作数栈中,期待执行 add 操作
- 而后将操作数栈中的两个元素执行相加操作,并存储在局部变量表 3 的地位
对于 int j = 8; 的阐明
-
咱们反编译失去的字节码指令如下
- 因为 8 能够寄存在 byte 类型中,所以压入操作数栈的类型为 byte,而不是 int,所以执行的字节码指令为 bipush 8
- 而后执行将数值 8 寄存在 int 类型的变量中:istore_2
对于调用办法,返回值入操作数栈的阐明
- 代码
public int getSum(){
int m = 10;
int n = 20;
int k = m + n;
return k;
}
public void testGetSum(){int i = getSum();
int j = 10;
}
- getSum() 办法字节码指令:最初带着个 ireturn
- testGetSum() 办法字节码指令:一上来就加载 getSum() 办法的返回值
++i 与 i++ 的区别
- 代码
public void add(){
int i1 = 10;
i1++;
int i2 = 10;
++i2;
int i3 = 10;
int i4 = i3++;
int i5 = 10;
int i6 = ++i5;
int i7 = 10;
i7 = i7++;
int i8 = 10;
i8 = ++i8;
int i9 = 10;
int i10 = i9++ + ++i9;
}
- 上面,我依据字节码指令,简略说下 i++ 和 ++i 的区别
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 bipush 10
8 istore_2
9 iinc 2 by 1
12 bipush 10
14 istore_3
15 iload_3
16 iinc 3 by 1
19 istore 4
21 bipush 10
23 istore 5
25 iinc 5 by 1
28 iload 5
30 istore 6
32 bipush 10
34 istore 7
36 iload 7
38 iinc 7 by 1
41 istore 7
43 bipush 10
45 istore 8
47 iinc 8 by 1
50 iload 8
52 istore 8
54 bipush 10
56 istore 9
58 iload 9
60 iinc 9 by 1
63 iinc 9 by 1
66 iload 9
68 iadd
69 istore 10
71 return
i++
- java 源代码
int i3 = 10;
int i4 = i3++;
-
字节码指令:
- bipush 10:将 10 压入操作数栈
- istore_3:将操作数栈中的 10 保留到变量 i3 中
- iload_3:将变量 i3 的值(10)加载至操作数栈中
- iinc 3 by 1:变量 i3 执行 +1 操作
- istore 4:将操作数栈中的值保留至变量 i4 中(10)
12 bipush 10
14 istore_3
15 iload_3
16 iinc 3 by 1
19 istore 4
++i
- java 源代码
int i5 = 10;
int i6 = ++i5;
-
字节码指令
- bipush 10:将 10 压入操作数栈
- istore 5:将操作数栈中的 10 保留到变量 i5 中
- iinc 5 by 1:变量 i5 执行 +1 操作
- iload 5:将变量 i5 的值(11)加载至操作数栈中
- istore 6:将操作数栈中的值保留至变量 i6 中(11)
21 bipush 10
23 istore 5
25 iinc 5 by 1
28 iload 5
30 istore 6
总结:
- i++:先将 i 的值加载到操作数栈,再将 i 的值加 1
- ++i:先将 i 的值加 1,在将 i 的值加载到操作数栈
6、栈顶缓存技术
栈顶缓存技术:Top Of Stack Cashing
- 后面提过,基于栈式架构的虚拟机所应用的零地址指令更加紧凑,但实现一项操作的时候必然须要应用更多的入栈和出栈指令,这同时也就意味着将须要更多的指令分派(instruction dispatch)次数和内存读 / 写次数。
- 因为操作数是存储在内存中的,因而频繁地执行内存读 / 写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM 的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全副缓存在物理 CPU 的寄存器中,以此升高对内存的读 / 写次数,晋升执行引擎的执行效率。
- 寄存器的次要长处:指令更少,执行速度快
7、动静链接
动静链接(或指向运行时常量池的办法援用)
动静链接:Dynamic Linking
- 每一个栈帧外部都蕴含 一个指向运行时常量池中该栈帧所属办法的援用
- 蕴含这个援用的目标就是 为了反对以后办法的代码可能实现动静链接(Dynamic Linking),比方:invokedynamic 指令
- 在 Java 源文件被编译到字节码文件中时,所有的变量和办法援用都作为符号援用(Symbolic Reference)保留在 class 文件的常量池里
- 比方:形容一个办法调用了另外的其余办法时,就是通过常量池中指向办法的符号援用来示意的,那么 动静链接的作用就是为了将这些符号援用转换为调用办法的间接援用
代码示例
- 代码
public class DynamicLinkingTest {
int num = 10;
public void methodA(){System.out.println("methodA()....");
}
public void methodB(){System.out.println("methodB()....");
methodA();
num++;
}
}
- 在字节码指令中,methodB() 办法中通过 invokevirtual #7 指令调用了办法 A
- 那么 #7 是个啥呢?
public void methodB();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String methodB()....
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #7 // Method methodA:()V
12: aload_0
13: dup
14: getfield #2 // Field num:I
17: iconst_1
18: iadd
19: putfield #2 // Field num:I
22: return
LineNumberTable:
line 16: 0
line 18: 8
line 20: 12
line 21: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/atguigu/java1/DynamicLinkingTest;
-
往上面翻,找到常量池的定义:
#7 = Methodref #8.#31
-
先找 #8:
#8 = Class #32
:去找 #32#32 = Utf8 com/atguigu/java1/DynamicLinkingTest
- 论断:通过 #8 咱们找到了
DynamicLinkingTest
这个类
-
再来找 #31:
#31 = NameAndType #19:#13
:去找 #19 和 #13#19 = Utf8 methodA
:办法名为 methodA#13 = Utf8 ()V
:办法没有形参,返回值为 void
-
- 论断:通过 #7 咱们就能找到须要调用的 methodA() 办法,并进行调用
Constant pool:
#1 = Methodref #9.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#24 // com/atguigu/java1/DynamicLinkingTest.num:I
#3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #27 // methodA()....
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = String #30 // methodB()....
#7 = Methodref #8.#31 // com/atguigu/java1/DynamicLinkingTest.methodA:()V
#8 = Class #32 // com/atguigu/java1/DynamicLinkingTest
#9 = Class #33 // java/lang/Object
#10 = Utf8 num
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/atguigu/java1/DynamicLinkingTest;
#19 = Utf8 methodA
#20 = Utf8 methodB
#21 = Utf8 SourceFile
#22 = Utf8 DynamicLinkingTest.java
#23 = NameAndType #12:#13 // "<init>":()V
#24 = NameAndType #10:#11 // num:I
#25 = Class #34 // java/lang/System
#26 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#27 = Utf8 methodA()....
#28 = Class #37 // java/io/PrintStream
#29 = NameAndType #38:#39 // println:(Ljava/lang/String;)V
#30 = Utf8 methodB()....
#31 = NameAndType #19:#13 // methodA:()V
#32 = Utf8 com/atguigu/java1/DynamicLinkingTest
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (Ljava/lang/String;)V
</init></init></init>
- 在下面,其实还有很多符号援用,比方 Object、System、PrintStream 等等
为什么要用常量池呢?
- 因为在不同的办法,都可能调用常量或者办法,所以 只须要存储一份即可,而后记录其援用即可,节俭了空间
- 常量池的作用:就是为了提供一些符号和常量,便于指令的辨认
8、解析和分派
8.1、动态链接与动静链接
动态链接机制与动静链接机制
在 JVM 中,将符号援用转换为调用办法的间接援用与办法的绑定机制相干
- 动态链接 :当一个字节码文件被装载进 JVM 外部时, 如果被调用的指标办法在编译期确定,且运行期放弃不变时,这种状况下将调用办法的符号援用转换为间接援用的过程称之为动态链接
- 动静链接 : 如果被调用的办法在编译期无奈被确定下来 ,也就是说, 只可能在程序运行期将调用的办法的符号转换为间接援用 ,因为这种援用转换过程具备 动态性,因而也被称之为动静链接。
8.2、晚期绑定与早期绑定
办法的绑定机制
动态链接和动静链接对应的办法的绑定机制为:晚期绑定(Early Binding)和早期绑定(Late Binding)。绑定是一个字段、办法或者类在符号援用被替换为间接援用的过程,这仅仅产生一次。
- 晚期绑定 晚期绑定就是指被调用的指标办法如果在编译期可知,且运行期放弃不变时,即可将这个办法与所属的类型进行绑定,这样一来,因为明确了被调用的指标办法到底是哪一个,因而也就 能够应用动态链接的形式将符号援用转换为间接援用。
- 早期绑定 如果被调用的办法在编译期无奈被确定下来, 只可能在程序运行期依据理论的类型绑定相干的办法,这种绑定形式也就被称之为早期绑定。
代码示例
- 代码
class Animal {public void eat() {System.out.println("动物进食");
}
}
interface Huntable {void hunt();
}
class Dog extends Animal implements Huntable {
@Override
public void eat() {System.out.println("狗吃骨头");
}
@Override
public void hunt() {System.out.println("捕食耗子,多管闲事");
}
}
class Cat extends Animal implements Huntable {public Cat() {super();
}
public Cat(String name) {this();
}
@Override
public void eat() {super.eat();
System.out.println("猫吃鱼");
}
@Override
public void hunt() {System.out.println("捕食耗子,理所当然");
}
}
public class AnimalTest {public void showAnimal(Animal animal) {animal.eat();
}
public void showHunt(Huntable h) {h.hunt();
}
}
- invokevirtual 体现为早期绑定
- invokeinterface 也体现为早期绑定
- invokespecial 体现为晚期绑定
8.3、多态性与办法绑定
多态性与办法绑定机制
- 随着高级语言的横空出世,相似于 Java 一样的基于面向对象的编程语言现在越来越多,只管这类编程语言在语法格调上存在肯定的差异,然而它们彼此之间始终保持着一个共性,那就是都反对封装、继承和多态等面向对象个性,既然这一类的编程语言具备多态个性,那么天然也就具备晚期绑定和早期绑定两种绑定形式。
- Java 中任何一个一般的办法其实都具备虚函数的特色,它们相当于 C ++ 语言中的虚函数(C++ 中则须要应用关键字 virtual 来显式定义)。如果在 Java 程序中不心愿某个办法领有虚函数的特色时,则能够应用关键字 final 来标记这个办法。
虚办法与非虚办法
虚办法与非虚办法的区别
- 如果办法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的办法称为非虚办法。
- 静态方法、公有办法、fina1 办法、实例结构器、父类办法都是非虚办法。
- 其余办法称为虚办法。
子类对象的多态的应用前提:
- 类的继承关系
- 办法的重写
虚拟机中调用办法的指令
四条一般指令:
- invokestatic:调用静态方法,解析阶段确定惟一办法版本
- invokespecial:调用
<init></init>
办法、公有及父类办法,解析阶段确定惟一办法版本 - invokevirtual:调用所有虚办法
- invokeinterface:调用接口办法
一条动静调用指令
invokedynamic:动静解析出须要调用的办法,而后执行
区别
- 前四条指令固化在虚拟机外部,办法的调用执行不可人为干涉
- 而 invokedynamic 指令则反对由用户确定办法版本
- 其中 invokestatic 指令和 invokespecial 指令调用的办法称为非虚办法,其余的(fina1 润饰的除外)称为虚办法。
代码示例:
- 代码
class Father {public Father() {System.out.println("father 的结构器");
}
public static void showStatic(String str) {System.out.println("father" + str);
}
public final void showFinal() {System.out.println("father show final");
}
public void showCommon() {System.out.println("father 一般办法");
}
}
public class Son extends Father {public Son() {super();
}
public Son(int age) {this();
}
public static void showStatic(String str) {System.out.println("son" + str);
}
private void showPrivate(String str) {System.out.println("son private" + str);
}
public void show() {showStatic("atguigu.com");
super.showStatic("good!");
showPrivate("hello!");
showFinal();
super.showCommon();
showCommon();
info();
MethodInterface in = null;
in.methodA();}
public void info() {}
public void display(Father f) {f.showCommon();
}
public static void main(String[] args) {Son so = new Son();
so.show();}
}
interface MethodInterface {void methodA();
}
- Son 类中 show() 办法的字节码指令如下
对于 invokedynamic 指令
- JVM 字节码指令集始终比较稳定,始终到 Java7 中才减少了一个 invokedynamic 指令,这是 Java 为了实现【动静类型语言】反对而做的一种改良。
- 然而在 Java7 中并没有提供间接生成 invokedynamic 指令的办法,须要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令。直到 Java8 的 Lambda 表达式的呈现,invokedynamic 指令的生成,在 Java 中才有了间接的生成形式。
- Java7 中减少的动静语言类型反对的实质是对 Java 虚拟机标准的批改,而不是对 Java 语言规定的批改,这一块绝对来讲比较复杂,减少了虚拟机中的办法调用,最间接的受益者就是运行在 Java 平台的动静语言的编译器。
代码示例
- 代码
@FunctionalInterface
interface Func {public boolean func(String str);
}
public class Lambda {public void lambda(Func func) {return;}
public static void main(String[] args) {Lambda lambda = new Lambda();
Func func = s -> {return true;};
lambda.lambda(func);
lambda.lambda(s -> {return true;});
}
}
- 字节码指令
8.4、办法重写的实质
动静语言和动态语言
- 动静类型语言和动态类型语言两者的区别就在于 对类型的查看是在编译期还是在运行期,满足前者就是动态类型语言,反之是动静类型语言。
- 说的再直白一点就是,动态类型语言是判断变量本身的类型信息;动静类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动静语言的一个重要特色。
Java:String info = "mogu blog"; (Java是静态类型语言的,会先编译就进行类型检查)
JS:var name = "shkstart"; var name = 10; (运行时才进行检查)
办法重写的实质
Java 语言中办法重写的实质:
- 找到操作数栈顶的第一个元素所执行的对象的 理论类型,记作 C。
-
如果 在类型 C 中找到与常量中的形容合乎简略名称都相符的办法,则进行拜访权限校验
- 如果通过则返回这个办法的间接援用,查找过程完结
- 如果不通过,则返回 java.1ang.IllegalAccessError 异样
- 否则,依照继承关系从下往上顺次对 C 的各个父类进行第 2 步的搜寻和验证过程。
- 如果始终没有找到适合的办法,则抛出 java.lang.AbstractMethodError 异样。
IllegalAccessError 介绍
- 程序试图拜访或批改一个属性或调用一个办法,这个属性或办法,你没有权限拜访。
- 个别的,这个会引起编译器异样。这个谬误如果产生在运行时,就阐明一个类产生了不兼容的扭转。
- 比方,你把应该有的 jar 包放从工程中拿走了,或者 Maven 中存在 jar 包抵触
回看解析阶段
- 解析阶段就是 将常量池内的符号援用转换为间接援用的过程
- 解析动作次要针对类或接口、字段、类办法、接口办法、办法类型等。对应常量池中的 CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info 等
8.5、多态与虚办法表
虚办法表
- 在面向对象的编程中,会很频繁的应用到 动静分派 ,如果在每次动静分派的过程中都要从新在类的办法元数据中搜寻适合的指标的话就可能 影响到执行效率。
- 因而,为了进步性能,JVM 采纳在类的办法区建设一个虚办法表(virtual method table)来实现,非虚办法不会呈现在表中。应用索引表来代替查找。
- 每个类中都有一个虚办法表,表中寄存着各个办法的理论入口。
- 虚办法表是什么时候被创立的呢?虚办法表会在类加载的链接阶段被创立并开始初始化,类的变量初始值筹备实现之后,JVM 会把该类的虚办法表也初始化结束。
- 如图所示:如果类中重写了办法,那么调用的时候,就会间接在该类的虚办法表中查找
9、办法返回地址
办法返回地址(return address)
-
寄存调用该办法的 pc 寄存器的值。一个办法的完结,有两种形式:
- 失常执行实现
- 呈现未解决的异样,非正常退出
- 无论通过哪种形式退出,在办法退出后都返回到该办法被调用的地位。办法失常退出时,调用者的 pc 计数器的值作为返回地址 ,即调用该办法的指令的下一条指令的地址。而通过异样退出的, 返回地址是要通过异样表来确定,栈帧中个别不会保留这部分信息。
- 实质上,办法的退出就是以后栈帧出栈的过程。此时,须要复原下层办法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置 PC 寄存器值等,让调用者办法继续执行上来。
- 失常实现进口和异样实现进口的区别在于:通过异样实现进口退出的不会给他的下层调用者产生任何的返回值。
办法退出的两种形式
当一个办法开始执行后,只有两种形式能够退出这个办法,
失常退出:
- 执行引擎遇到任意一个办法返回的字节码指令(return),会有返回值传递给下层的办法调用者,简称失常实现进口;
- 一个办法在失常调用实现之后,到底须要应用哪一个返回指令,还须要依据办法返回值的理论数据类型而定。
-
在字节码指令中,返回指令蕴含:
- ireturn:当返回值是 boolean,byte,char,short 和 int 类型时应用
- lreturn:Long 类型
- freturn:Float 类型
- dreturn:Double 类型
- areturn:援用类型
- return:返回值类型为 void 的办法、实例初始化办法、类和接口的初始化办法
异样退出:
- 在办法执行过程中遇到异样(Exception),并且这个异样没有在办法内进行解决,也就是只有在本办法的异样表中没有搜寻到匹配的异样处理器,就会导致办法退出,简称异样实现进口。
- 办法执行过程中,抛出异样时的异样解决,存储在一个异样处理表,不便在产生异样的时候找到解决异样的代码
代码举例
- 代码
public class ReturnAddressTest {public boolean methodBoolean() {return false;}
public byte methodByte() {return 0;}
public short methodShort() {return 0;}
public char methodChar() {return 'a';}
public int methodInt() {return 0;}
public long methodLong() {return 0L;}
public float methodFloat() {return 0.0f;}
public double methodDouble() {return 0.0;}
public String methodString() {return null;}
public Date methodDate() {return null;}
public void methodVoid() {}
static {int i = 10;}
public void method2() {methodVoid();
try {method1();
} catch (IOException e) {e.printStackTrace();
}
}
public void method1() throws IOException {FileReader fis = new FileReader("atguigu.txt");
char[] cBuffer = new char[1024];
int len;
while ((len = fis.read(cBuffer)) != -1) {String str = new String(cBuffer, 0, len);
System.out.println(str);
}
fis.close();}
}
-
办法失常返回
- ireturn
- dreturn
- areturn
-
异样处理表:
- 反编译字节码文件,可失去 Exception table
- from:字节码指令起始地址
- to:字节码指令完结地址
- target:出现异常跳转至地址为 11 的指令执行
- type:捕捉异样的类型
10、一些附加信息
栈帧中还容许携带与 Java 虚拟机实现相干的一些附加信息。例如:对程序调试提供反对的信息。
11、栈相干面试题
举例栈溢出的状况?(StackOverflowError)
通过 -Xss 设置栈的大小
调整栈大小,就能保障不呈现溢出么?
不能保障不溢出
调配的栈内存越大越好么?
不是,肯定工夫内升高了 OOM 概率,然而会挤占其它的线程空间,因为整个虚拟机的内存空间是无限的
垃圾回收是否波及到虚拟机栈?
不会
办法中定义的局部变量是否线程平安?
何为线程平安?
- 如果只有一个线程才能够操作此数据,则必是线程平安的。
- 如果有多个线程操作此数据,则此数据是共享数据。如果不思考同步机制的话,会存在线程平安问题。
具体问题具体分析:
- 如果对象是在外部产生,并在外部沦亡,没有返回到内部,那么它就是线程平安的,反之则是线程不平安的。
- 看代码
public class StringBuilderTest {public static void method1(){StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
public static void method2(StringBuilder sBuilder){sBuilder.append("a");
sBuilder.append("b");
}
public static StringBuilder method3(){StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
public static String method4(){StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();}
public static void main(String[] args) {StringBuilder s = new StringBuilder();
new Thread(() -> {s.append("a");
s.append("b");
}).start();
method2(s);
}
}
运行时数据区,哪些局部存在 Error 和 GC?
运行时数据区是否存在 Error 是否存在 GC 程序计数器否否虚拟机栈是(SOF)否本地办法栈是否办法区是(OOM)是堆是(OOM)是
你只管学习,我来负责记笔记???? 关注公众号!, 更多笔记,等你来拿,谢谢