JVM 学习第三天

虚拟机栈

常见异常

image-20201209123318571

image-20201209124912667

可以通过-Xss去设置栈的大小

栈的存储单位

栈中存储什么?

  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
  • 在这个线程上正在指定的每个方法都各自对应一个栈帧。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
  • JVM直接对Java栈的操作只有两个,就是对栈帧的压栈出栈,遵循“先进后出”/后进先出的原则。
  • 在一条活动线程中,一个时间点上只会有一个活动的栈帧。即只有当前正在指定的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类.
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 如果该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。

image-20201209130007864

复习:

  • OOP的基本概念:类、对象
  • 类中基本结构:field(属性、字段、域)、method

栈运行原理

  • 不同的线程中所包含的栈帧是不允许存在相互引用的,即不可能在同一个栈帧之中引用另外一个线程的栈帧。
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
  • Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
/**
 * @author WangYifei
 * @date 2020-12-08 17:23
 * @describe 
 */
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        System.out.println("main开始..");
        test.methodA();
        System.out.println("main结束...");

    }

    public void methodA() {
        System.out.println("方法开始》。。");
        methodB();
        System.out.println("方法结束。。");
    }

    public void methodB() {
        System.out.println("B开始...");
        System.out.println("B结束...");
    }


}

结果:

main开始..
方法开始》。。
B开始...
B结束...
方法结束。。
main结束...

可以看出,模拟出出栈入栈的操作:

  1. main入栈,执行main开始。。
  2. 随后发现methodA() 将methodA()压栈,程序计数器进入methoadA() 执行方法开始》.。
  3. 随后发现methodB()将methodB()压栈,程序计数器进入methodB()执行B开始。。
  4. B中每发现方法正常执行,执行B结束。。随后方法B执行完毕,B方法出栈。
  5. B出栈之后,A成为当前栈帧,程序计数器继续执行A所保存的记录,打印方法结束。。
  6. 随后方法A出栈,程序计数器继续进入main方法,执行main结束,栈清空程序结束,销毁。

栈帧的内部结构

  • 局部变量表·
  • 操作数栈(或表达式栈)
  • 动态链接 (或指向运行时常量池的方法引用)
  • 方法返回地址(或方法正常退出或者异常退出的定义)
  • 一些附加信息image-20201209202541230

栈帧的大小决定了栈能存放多少栈帧,栈帧的大小又取决于局部变量表和操作数栈。

局部变量表

  • 局部变量表也称之为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型主要包括各类基本数据类型、对象引用,一集return Addres类型。
  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译器确定下来的,并保存在方法的code 属性的maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的
  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

image-20201209211412359

查看字节码。

image-20201209211443828

所对应的源码方便查看。

image-20201209211525045

查看局部变量表,变量一共三个:args, s, num

image-20201209211601037

起始PC表示,这个变量从哪一行开始发生作用即可以被使用,长度为可以被使用多少行,可以发现,起始PC + 长度 = 字节码长度,因为作用域就这么大。过去了这些变量就不能被使用。

需要注意的是,声明的行号是声明作用,不能被使用。

image-20201209211831735

起始是0,这个起始是字节码所表示的行号索引,从0开始。而行号是Java代码所标注的行号。看下图:

image-20201209211944176

字节码就不截图了= =可以往上翻,毕竟截图耗费空间= =,哈哈哈哈

image-20201209212022554

字节码这一块,也有一些常量池的引用,比如什么#2 这个2就是后面这个java.../String所在常量池的索引。

Slot理解

  • 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
  • 局部变量表,最基本的存储单元是Slot(变量槽)
  • 局部变量表中存放编译器可知的各种基本数据类型(8种),引用类型,return Address类型的变量。
  • 在局部变量表中,32位以内的类型只占用一个slot(包括return Address类型),64位的类型(long和double)占用两个solt

    • byte、short、char在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
    • long和double则占据两个slot。
  • JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
  • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
  • 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个即起始索引即可。(比如,访问long或者double类型变量)
  • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照表顺序继续排列。

关于Slot的使用的理解

如果在当前帧中调用别的拥有返回值的方法,如果没有使用变量参数进行接收,那么是不占用局部变量表的空间的。构造方法和实例方法会自动加载this变量在index为0的位置

image-20201209213954699

注意看 weight为什么是3直接跳到了5,因为之前提到的,double占用2个slot,直接使用起始索引,即3,4也归weight所以直接到了5。

Slot的重复利用

栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量表,过了其作用域,那么其作用域之后声明的新的局部变量就很有可能会复用国企局部变量的槽位,从而达到节省子员的目的

image-20201209214323354

(P50.。。/301)

最后修改:2020 年 12 月 09 日 09 : 47 PM
如果觉得我的文章对你有用,请随意赞赏