目录
4.6,栈顶缓存技术ToS(Top-of-Stack Cashing)
接着上一次的运行时数据区一继续看,如果第一部分没有看的话,请在这里看:
4.4,操作数栈
4.4.1,概述
- 栈 :可以使用数组或者链表来实现
- 每一个独立的栈帧中除了包含局部变量表以外,还包括一个后进先出(last-in-first-out)的操作数栈,也可以称为表达式栈(expression stack)。
- 操作数栈,在方法执行的过程中,根据字节码指令,往栈中写入数据或者是提取数据,即入栈或者出栈。
- 某一些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。
- 默认用数组实现,因此在编译阶段大小已经确定。
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并且更新pc寄存器中吓一跳需要执行的字节码指令。
- 操作数栈中元素的数据类型必须和字节码指令的序列严格匹配,这是由编译器的编译期间进行验证的,同时在类加载的过程中类检验阶段的数据流分析阶段要再次验证。
- 另外,我们说java虚拟机的解析引擎是基于栈的执行引擎,其中的栈指的是操作数栈
4.4.2,操作数栈的作用
- 操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量的临时存储位置。
- 操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需要的最大深度在编译期间就定义好,保存在方法的code属性中,为max_stack的值。
- 栈中的数据类型可以是任何一个java数据类型。
- 32bit的类型占用一个栈单位深度。
- 64bit类型占据两个栈单位的深度。
- 操作数栈并非采用访问索引的方式来进行数据访问的,而只是通过标准的入栈和出栈操作来完成数据的访问。
4.5,代码追踪
public class OperantStackTest {
public static void main(String[] args) {
byte i=15;
int j=8;
int m=i+j;
}
}
//编译结果
Classfile /D:/intellij/ideaWork/jvm/character01/target/classes/qq/com/rzf/OperantStackTest.class
Last modified 2020-4-17; size 481 bytes
MD5 checksum 775fd5fe9a4b1c367860391e1435daef
Compiled from "OperantStackTest.java"
public class qq.com.rzf.OperantStackTest
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // qq/com/rzf/OperantStackTest
#3 = Class #23 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lqq/com/rzf/OperantStackTest;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 i
#16 = Utf8 I
#17 = Utf8 j
#18 = Utf8 m
#19 = Utf8 SourceFile
#20 = Utf8 OperantStackTest.java
#21 = NameAndType #4:#5 // "<init>":()V
#22 = Utf8 qq/com/rzf/OperantStackTest
#23 = Utf8 java/lang/Object
{
public qq.com.rzf.OperantStacktest();
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 Lqq/com/rzf/OperantStackTest;
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 8
2: istore_1
3: bipush 9
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
3 8 1 i I
6 5 2 j I
10 1 3 m I
}
SourceFile: "OperantStackTest.java"
//byte,short,char,boolean像数组中存放都以int类型保存
通过分析上面的代码,我们来看一个方法(栈帧)的执行过程,局部变量表是从1开始的,因为0位置存储的是this指针。
- 1:pc寄存器的值为0,标示执行的是第一条指令。15入栈--->15存储到局部变量表。
- pc寄存器是3,标示当前执行第三条指令。8入栈操作--->8存储到局部变量表。
- 执行第6,7条指令:从局部变量表中把索引为1和2的数据加载出来,然后放到操作数栈中,然后执行第八条指令,iadd指令,把两个数据相加。
- 执行第九条指令:把相加的结果入栈,再把相加的结果存储到局部变量表索引为3的位置。
bipush:表示存储`byte`类型数据。Istore_1表示存储的是int类型的数据。
- i++和++i的区别:
4.6,栈顶缓存技术ToS(Top-of-Stack Cashing)
- 基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
- 由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在cpu的寄存器中,以此降低对内存的读/写次数,提升执行疫情的执行效率
4.7,动态链接(Dynamic Linking)
- 每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令
-
在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
- 为什么需要常量池呢:常量池的作用,就是为了提供一些符号和常量,便于指令的识别。节省存储空间。在class字节码文件中常量池中的符号引用被加载到内存时就会放到方法区的常量池中,然后栈帧中需要符号引用时就直接引用方法区中的符号引用。方法区的常量池称作运行时的常量池。动态连接的目的就是将这些符号的引用转换为调用方法的直接引用。也就是说方法区的常量池信息就是字节码文件中常量池的信息。
- 符号引用:
- 运行时常量池:
4.8,方法的调用,解析与分派
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
对应的方法的绑定机制为:早起绑定(Early Binding)和晚期绑定(Late Bingding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
- 早期绑定
- 晚期绑定
- 小结:
-
编译期间决定---->早期绑定
-
运行期间决定---->晚期绑定
-
晚期绑定体现在多态处。构造器表现为早期绑定。
-
随着高级语言的横空出世,类似于java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装,集成和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
- 虚拟机中提供了一下几条方法调用指令。
- 测试:
/**
* 解析调用中非虚方法、虚方法的测试
*/
class Father {
public Father(){
System.out.println("Father默认构造器");
}
public static void showStatic(String s){
System.out.println("Father show static"+s);
}
public final void showFinal(){
System.out.println("Father show final");
}
public void showCommon(){
System.out.println("Father show common");
}
}
public class Son extends Father{
public Son(){
super();
}
public Son(int age){
this();
}
public static void main(String[] args) {
Son son = new Son();
son.show();
}
//不是重写的父类方法,因为静态方法不能被重写
public static void showStatic(String s){
System.out.println("Son show static"+s);
}
private void showPrivate(String s){
System.out.println("Son show private"+s);
}
public void show(){
//invokestatic
showStatic(" 大头儿子");
//invokestatic
super.showStatic(" 大头儿子");
//invokespecial
showPrivate(" hello!");
//invokespecial
super.showCommon();
//invokevirtual 因为此方法声明有final 不能被子类重写,所以也认为该方法是非虚方法
showFinal();
//虚方法如下
//invokevirtual
showCommon();//没有显式加super,被认为是虚方法,因为子类可能重写showCommon
info();
MethodInterface in = null;
//invokeinterface 不确定接口实现类是哪一个 需要重写
in.methodA();
}
public void info(){
}
}
interface MethodInterface {
void methodA();
}
- 关于invokedynamic指令:
- 动态类型语言和静态类型语言
- java语言中方法重写的本质。
- 找到操作数栈的第一个元素所执行的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
- .否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程。
-
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。 IllegalAccessError介绍 程序视图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
- 虚方法表
- 虚方法表:friendly是一个接口。
也就是说dog类实现了父类的那些方法,dog类的虚方法表中的函数地址就直接指向dog类,调用的时候直接调用dog实现的方法,如果dog没有实现,就调用dog父类的方法。
具体狗的实现类,没有重写父类dog的tostring()方法,那么此方法就指向父类的tostring方法,实现了父类sayHello()和sayGoodbye方法,那么这两个方法就指向具体的实现类,如果用其他的方法,都是调用object()中的方法。
4.9,方法返回地址
-
本质上,方法的退出就是当前栈帧出栈的过程,此时,需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈的操作数栈,设置pc寄存器值等,让调用者的方法继续执行下去。
-
- 正常执行完毕,执行return语句。
- 出现未处理的异常,非正常退出。
- 无论通过哪种方式退出,在方法退出后都会返回到该方法被调用的位置,方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址,而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
- 当一个方法开始执行后,只有两种方式可以退出这个方法:
前面两行数字都是字节码指令的地址,也就是说如果指令从4-16出现异常,那么就按照19行处进行处理。
注意:正常完成出口和异常完成出口的区别在于:通过异常完成的出口退出不会给他的上层调用者产生任何的返回值信息。
4.10,相关题目
-
开发中遇到的异常有哪些?(高级一点的异常:java虚拟机中的异常)
- 调整栈大小,就可以保证不出现溢出情况么?
- 不可以,调整栈空间的大小,可以让stackoverflowerror出现的时间晚一点,但是不能完全避免。就比如给你500元你一周可以用完,但是给你1000元,你还会把钱用完,只不过用完时间晚一点而已。
/**
* 栈内存溢出
* 默认不设置栈内存大小:num最大值是9654
* 设置栈的大小:-Xss256k,num值是:2469
*/
public class StackerrorFlow {
private static int num=0;
public static void main(String[] args) {
System.out.println(num);
num++;
main(args);//主函数递归调用自己
}
}
- 垃圾回收机制是否会涉及到虚拟机栈?不会。
内存块区域 | 栈溢出或者内存溢出 | GC |
Pc寄存器 | 不存在 | 不存在 |
虚拟机栈 | 存在 | 不存在 |
存在 | 不存在 | |
堆 | 存在 | 存在 |
方法区 | 存在 | 存在 |
- 分配栈内存空间越大越好么?
- 不一定,应为栈的总容量大小是一定的,如果给某一个线程的栈容量空间分配的很大,会导致其他线程的栈空间容量变小发生溢出情况,对于当前的线程,分配更大的栈内存。可以让stackoverflowerror出现的时间晚一点,并不能完全避免。
- 方法中定义的局部变量是否是线程安全的?
- 这个问题要具体问题具体分析。线程安全:如果只有一个线程才可以操作此数据,则必定是线程安全的,如果是多个线程操作此数据,则此数据是共享数据,如果不考虑同步机制的话,会存在线程安全问题。
//但是stringBuilder的声明方式是线程安全的
//因为method1()归某一个栈帧单独所有,其他线程不能够访问,所以是线程安全的,变量多个线程共享的时候存在线程安全问题。
public static void mothod1(){
//StringBuilder类本身线程不安全
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("aa");
stringBuilder.append("bb");
}
//method2()的stringBuilder声明方式是不安全的,参数是从外面传进来,不归一个线程单独所有,可能多个线程共享,所以不安全
public static void mothod2( StringBuilder stringBuilder){
//StringBuilder类本身线程不安全
stringBuilder.append("aa");
stringBuilder.append("bb");
}
//method3()的stringBuilder声明方式是不安全的,虽然stringBuilder是在方法里面创建的,但是有返回值,可能被其他线程所调用修改,所以不安全。
public static StringBuilder mothod3(){
//StringBuilder类本身线程不安全
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("aa");
stringBuilder.append("bb");
return stringBuilder;
}
//method4()的stringBuilder声明方式是安全的,Method3不安全,应为直接将stringBuilder进行返回,但是method4是安全的,应为在方法里面,stringBuilder已经不存在了,方法里面的StringbUILDER已经消亡,在这里stringBuilder安全,但是string可能不安全,应为tostring()方法有创建一个方法返回,可能多个线程共享string
public static String mothod4(){
//StringBuilder类本身线程不安全
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("aa");
stringBuilder.append("bb");
return stringBuilder.toString();
}
小结:对象如果内部产生,内部消亡,就是安全的。不是内部产生的,或者内部产生,但又返回生命周期没有结束就是不安全的。
4.11,本地方法栈
- Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用
- 本地方法栈,也是线程私有的。
- 允许被实现成固定或者是可动态拓展的内存大小。(在内存溢出方面是相同的)
- 本地方法是使用C语言实现的
- 它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限
- 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
- 在hotSpot JVM中,直接将本地方法栈和虚拟机栈合二为一。
也就是说本地方法栈主要和本地方法库,本地方法接口打交道。通过本地方法接口,本地方法栈可以访问jvm的运行时数据区。
参考资料:
[1] 深入理解java虚拟机。周志明
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。