JVM-虚拟机栈

虚拟机栈

由于跨平台的设计,java的指令都是根据栈来设计的。由于不同平台的CPU架构不一样所以不能设计为基于寄存器。
优点: 跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多指令。
设置栈大小:-Xss 1m(size)
在这里插入图片描述

运行时栈帧结构

栈帧用于存放局部变量表 ,操作数栈,动态链接,方法出口。每一个方法被调用到执行完毕的过程,就对应着一个栈帧进栈到出栈的过程。执行引擎所运行的所有字节码指令都只针对当前栈帧(栈顶)操作。

局部变量表

  1. 局部变量表是一组变量值的存储空间,用来存放方法参数和方法内定义的局部变量。

  2. 局部变量表的大小在编译时期就已经知道了,可以根据class文件解析出来的局部变量表计算出栈内运行时局部变量表的大小。

  3. 局部变量表的容量以变量槽(slot )为最小单位,如果 是long或者double类型,需要两个slot 并且index 记录前一个。么

  4. 分配规则:如果是实例方法,那么0 index 存的是this
    其余参数安装顺序依次分配
    根据方法体内部定义变量顺序和作用域分配

  5. 局部变量槽能够复用

  6. 局部变量表中的变量也会重要的垃圾回收根节点。只要是被局部变量表引用的对象都不会被回收。

操作数栈

  1. 操作数栈是一个后入先出的栈,最大深度在编译的时候就已经确定了。主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时储存空间。

  2. 一个方法在执行的时候这个栈是空的。在执行的过程中,会不断通过字节码指令对这个栈进行操作。

  3. 在大多虚拟机实现上,会令相邻栈帧出现一部分重叠。让下面栈帧的部分操作数栈和上面部分局部变量重叠

  4. 如果被调用的方法有返回值,那么要把返回值入栈

动态连接

每一个帧都包含一个指向运行时常量池中该帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。这个引用是这个方法一些信息的人口(我们称一些运行时常量池中的符号引用在运行时转化为直接引用的叫动态链接)
在这里插入图片描述

方法返回地址

当一个方法开始执行之后,只有两种方式退出这个方法。第一种:执行引擎遇到任意一个方法返回的字节码指令。第二种:遇到异常。无论是哪一种退出方式都需要返回到最初方法使用的位置。一般情况下,正常退出的时候,主调方法的PC计数器的值就好了。异常退出的时候就不会保存。
方法退出的过程:

  1. 当前栈帧出栈
  2. 恢复上层方法的局部变量表和操作数栈
  3. 如果有返回值,压入操作数栈
  4. 调整PC计数器的值为调用指令后面的一条指令

一些其他信息

方法调用

解析

在类加载的解析阶段,会将其中一部分符号引用转化为直接引用。这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且运行期不可改变。也就是说,调用目标在程序代码写好,编译器进行编译的那一刻就已经确定下来了。这类方法的调用被称为解析,例如静态方法,final方法,私有方法,实例构造器,实例构造器。因为这些不会被改变,是非虚方法。

  1. invokestatic 调用静态方法
  2. invokespecial 调用实例构造器init方法,私有方法,父类中的方法
  3. invokevirtual 调用虚方法(final 方法也在里面 不过会被解析)
  4. invokeinterface 调用接口方法,会在运行时再确定一个实现方法
  5. invokedynamic 现在运行时动态解析出调用限定符所应器的方法,然后再执行该方法。(java7 增加的,为实现动态类型语言)

解析调用是一个静态过程,在编译期间就完全确定了。而另一种主要的方法调用形式:分派。

分派

  • 静态分派
    看看下面程序的输出
public class TestClass {
    
    
    static abstract class Human{
    
    }
    static class Man extends Human{
    
    }
    static class Woman extends Human{
    
    }
    public void sayHello(Human guy){
    
    
        System.out.println("Hello guy");
    }
    public void sayHello(Man guy){
    
    
        System.out.println("Hello man");
    }
    public void sayHello(Woman guy){
    
    
        System.out.println("Hello woman");
    }

    public static void main(String[] args) {
    
    
        Human man = new Man();
        Human woman = new Woman();
        TestClass tc = new TestClass();
        tc.sayHello(man);
        tc.sayHello(woman);
    }

}
//输出:
//Hello guy
//Hello guy

上面表示的是方法的重载(OverLoad)也就是要说的方法的静态分派

Human man = new Man();

上面代码中的Human 称为变量的 静态类型或者叫外观类型,而后面 的Man 称为变量的实际类型,或者叫运行时类型。
变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化是在运行期才确定的。
在调用的时候,我们已经确认了是使用的是tc 对象 ,要使用哪一个重载的版本取决于传人参数的数量和数据类型。由于编译器在重载的时候是通过参数的静态类型而不是运行时类型,因此输出的都是Hello guy。

  • 动态分派
    看看下面程序的输出
public class TestClass {
    
    
    static abstract class Human{
    
    
        protected abstract void sayHello();
    }
    static class Man extends Human{
    
    
        @Override
        protected void sayHello() {
    
    
            System.out.println("Hello man");
        }
    }
    static class Woman extends Human{
    
    
        @Override
        protected void sayHello() {
    
    
            System.out.println("Hello woman");
        }
    }


    public static void main(String[] args) {
    
    
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
    }
}
//输出:
//Hello man
//Hello woman

很显然,只要懂java的 都知道这是java的重写。那么java虚拟机怎么判断调用哪一个方法?
java虚拟机的调用方法的过程就是动态分派的过程。
显然,在这里不会根据变量的静态类型来决定,因为两个都是Human 但是产生了不同的行为。我们通过javap 查看了一下字节码发现两个地方在调用invokevirtual 指令的参数都是一样的。说明主要是invokevirtual指令里面的具体实现进行了动态分配。根据《java虚拟机规范》invokevirtual指令解析过程主要是分为下面几步。

  1. 找到操作数栈顶的第一个元素所指向的对象实际类型,记O。
  2. 如果在O中找到与常量中的描述符和简单名称都相符的方法,那么久进行访问权限校验,如果通过就返回这个方法的直接引用,不通过就报错。
  3. 否则按照继承关系,依次对父类进行第二步的操作(为了提高性能,JVM采用在类的方法区建立一个虚方法表来实现,在链接阶段就开始初始化。)
  4. 如果都找不到,报错
    注意:在java中只有虚方法的存在,字段是没有虚的,也就是说字段永远不参与多态。当子类声明了与父类同名的字段时,虽然在子类内存中两个字段都会存在,但是子类的字段会掩蔽父类的同名字段。

看看下面“劣质面试题”

public class TestClass {
    
    

    static  class Father{
    
    
        public int money = 1;

        public Father() {
    
    
            this.money = 2;
            showMoney();
        }

        protected  void showMoney(){
    
    
            System.out.println("I am Father , I hava $"+money);
        }
    }

    static  class Son extends Father{
    
    
        public int money = 3;

        public Son() {
    
    
            this.money = 4;
            showMoney();
        }

        protected  void showMoney(){
    
    
            System.out.println("I am Son , I hava $"+money);
        }
    }
    public static void main(String[] args) {
    
    
        Father guy = new Son();
        System.out.println("This guy has $"+guy.money);
    }

}
//输出
/*
I am Son , I hava $0
I am Son , I hava $4
This guy has $2
*/

解释:
首先,因为创建Son的过程中会先去调用父类的构造器方法来初始化父类的字段信息。这个时候会运行父类的构造器方法,将父类的money设置为 2 并且调用showMoney() ,这个时候是调用虚方法,因此是调用的是Son的而不是Father的 输出:I am Son , I hava $0 (由于还没有执行son的构造器所以是0)。后面执行Son的构造器方法 输出:I am Son , I hava $4。最后调用的由于字段是不支持多态的所以在调用guy.money是根据变量的静态类型去找的 money 输出:This guy has $2

猜你喜欢

转载自blog.csdn.net/null_zhouximin/article/details/112628519