尚硅谷2020最新版宋红康JVM教程学习笔记 二 运行时数据区

点击查看合集

运行时数据区的内部结构在这里插入图片描述

灰色部分为每个线程独立拥有。红色部分为所有线程共享。

JVM中的线程

线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行执行。在Hotspot JVM中,每个线程都与操作系统的本地线程直接映射。当一个Java线程准备号执行以后,此时一个操作 系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,他就会调用Java线程中的run()方法。

程序计数器(PC寄存器/Program Counter Register)

用来存储指向当前正在执行的指令的地址(行号),也即将要执行的指令代码。由执行引擎读取下一条指令。
在这里插入图片描述
它是一块很小的内存空间,也是运行速度最快的存储区域。
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期和线程一致。
任何时间一个线程只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的jvm指令地址。或者如果是在执行本地方法,则是未指定值。
在这里插入图片描述

内存中的栈和堆

栈是运行时的单位,堆是存储的单位。
栈解决程序的运行问题,即程序如何执行(如何处理数据)。堆解决的是数据存储问题即数据怎么放,放在哪。
栈中也存在数据,比如对象的引用或者基本局部变量(int )。

Java虚拟机栈

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的java方法调用。即每个执行的方法都对应一个栈帧。栈顶的栈帧是当前方法。java虚拟机栈主管Java程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用和返回。
在这里插入图片描述
栈不需要垃圾回收。
Java虚拟机规范规定Java栈的大小是动态的或者是固定不变的。
如果采用固定大小的栈,那么每个线程的栈容量可以在创建现成的时候选定。如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,Java虚拟机将抛出一个StackOverflowError异常
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或创建新线程时没有足够的内存去创建对应的虚拟机栈,那么java虚拟机将抛出一个outofMemoryError

StackOverflowError

public class Test7 {
    
    
    static int count = 0;
    public static void main(String[] args) {
    
    
        System.out.println(count++);
        main(args);
    }
}

在这里插入图片描述
可以通过-Xss参数设置java虚拟机栈的大小
在这里插入图片描述
在这里插入图片描述

栈中存储的内容

1.每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在。
2.在这个线程上正在执行的每个方法都各自对应一个栈帧
3.栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
JVM对java栈的操作只有压栈和出栈。在一个线程中,只有位于栈顶的栈帧(当前栈帧)对应的方法在执行。定义这个方法的类叫做当前类。
执行引擎运行的所有字节码指令只对当前栈帧进行操作。
如果在改方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧。
在这里插入图片描述
java方法有两种返回函数的方式,一种是正常的函数返回(return语句)。另一种是抛出异常。这两种方式都会导致栈帧被弹出。

栈帧的内部结构

每个栈帧中存储着:
1.局部变量表(Local Variables)
2.操作数栈(Operand Stack)或者叫表达式栈
3.动态链接(Dynamic Linking)或者叫指向运行时常量池的方法引用
4.方法返回地址(Return Address)或者叫方法正常退出或者异常退出的定义
5.一些附加信息
在这里插入图片描述

局部变量表

1.局部变量表也称之为局部变量数组或者本地变量表。
2.定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用,以及返回值类型。
3.由于局部变量是建立在线程的栈上,是现成的私有属性,因此不存在数据安全问题。
4.局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local variables属性项中。在方法运行期间是不会改变局部变量表的大小的。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
启始PC就是变量声明的字节码指令序号
长度就是变量声明周期长度
启始PC+长度=字节码指令数

Slot(槽)

Slot是局部变量表的最基本存储单元。局部变量表中存放编译器可知的各种基本数据类型(8种),引用类型(reference),返回值类型(returnAddress)的变量。
在局部变量表中,32位以内的类型只占用一个slot,64位的类型占用两个slot。
注意byte short char boolean在存储前被转换为int。
在这里插入图片描述
JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
当一个实例方法被调用的时候,他的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。
注意:
1.如果需要访问局部变量表中一个64bit的局部变量时,只需要是同开始的索引。
2.如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处。

为什么不能在静态方法里引用this

因为静态方法的局部变量表中没有this变量

Slot的重复利用

栈帧中的局部变量表中的槽位是可以重用的。

public class Test8 {
    
    
    public static void main(String[] args) {
    
    

    }
    public void method(int num1){
    
    
        int num2 = 10;
        {
    
    
            int temp = 110;
            num2 +=temp;
        }
        int num3 = 200;
    }
}

局部变量的最大槽数为4而不是5因为num3和temp共用一个槽。这样做可以节省资源
在这里插入图片描述

操作数栈(表达式栈)

每一个栈帧中除了包含局部变量表以外,它还包含一个操作数栈(表达式栈)。
在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈
在这里插入图片描述
1.如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令。
2.操作数栈中元素的数据类型必须与字节码指令的序列匹配,这由编译器在编译期间验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
3.java虚拟机的解释引擎是基于栈(操作数栈)的执行引擎。
4.操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
5操作数栈的最大栈深度在编译器就定义好了。为max_stack的值。(32位类型占用一个栈单位深度,64占两个)
在这里插入图片描述
push就是向操作数栈中暂存数据。(压栈)
store就是向局部变量表中存储数据(出栈)

栈顶缓存技术

将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

动态链接(指向运行时常量池的方法引用)

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
在这里插入图片描述

方法的调用

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
对应的方法的绑定机制为:早期绑定和晚期绑定。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

早期绑定(静态链接)

早期绑定就是指被调用的目标方法在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,即可以使用静态链接的方式将符号引用转换为直接引用,

晚期绑定(动态链接)

如果调用的方法在编译器无法确定,只有在运行期根据实际绑定的类型的相关方法来确定。

public class Test9 {
    
    
    public void execute(Animal animal){
    
    
        //晚期绑定(动态链接)
        animal.call();
    }
}
interface Animal{
    
    
    void call();
    void name();
}
class Cat implements Animal{
    
    

    @Override
    public void call() {
    
    
        System.out.println("喵喵");
        //早期绑定(静态链接)
        this.name();
    }

    @Override
    public void name() {
    
    
        System.out.println("猫");
    }
}
class Dog implements Animal{
    
    

    @Override
    public void call() {
    
    
        System.out.println("汪汪");
        //早期绑定(静态链接)
        this.name();
    }

    @Override
    public void name() {
    
    
        System.out.println("狗");
    }
}

虚方法

无法在编译器确定具体的调用版本,需要在运行期动态确定。

非虚方法

如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
在这里插入图片描述
invokedynamic:lamad表达式

public class Test10 {
    
    
    public static void main(String[] args) {
    
    
        TestDynamic t = ()->{
    
    
            System.out.println(1);
        };
    }
}
interface TestDynamic{
    
    
    void fun();
}

在这里插入图片描述

执行虚方法

当要执行虚方法时,需要从当前对象开始查找,若不存在该方法,则去父类查找直到找到或者没有父类为之。这样的话,每次调用都需要进行查找,会降低性能。因此jvm采取了虚方法表的方式来解决这一问题。
每个类都有一个虚方法表,表中存放的是方法的实际入口。如果子类没有重写该方法,则虚方法表中存放的就是父类方法入口。若重写了则存储自身的方法入口。
虚方法表再类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,jvm会把该类的方法表也初始化完毕。
在这里插入图片描述
虚方法表存储在类的方法区

方法返回地址

存放调用该方法的pc寄存器的值,即调用该方法的指令的下一条指令的地址。若是通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

一些附加信息

栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。但是这个附加信息不是必须的。

本地方法接口(使用的越来越少除非是与硬件有关的应用)(了解)

在java中有时需要引用一些用非java语言写的方法。这些方法需要用native来修饰。
例如
在这里插入图片描述

为什么要用本地方法

一:与java环境外交互
有时java应用需要与java外面的环境交互,这是本地方法存在的主要原因。例如java需要与一些底层系统,如操作系统或者某些硬件交换信息时的情况。本地方法正事这样一种交流机制,它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐细节。
二:与操作系统交互
虽然java程序是运行在jvm上的,但是jvm也需要依赖一些底层系统的支持。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至jvm的一些部分就是用c写的。当要使用一些java没有封装的操作系统特性时,也需要本地方法。
三:sun’s java
sun的解释器使用c实现的,这使得它能像一些普通的c一样与外交部交互。

本地方法栈(与虚拟机栈差不多)

java虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用。
hotsport将虚拟机栈和本地方法栈合二为一。

猜你喜欢

转载自blog.csdn.net/qq_30033509/article/details/110396720