JVM-虚拟机栈

运行时数据区-虚拟机栈

JAVA技术交流群:737698533

java虚拟机在执行java程序过程中会把它所管理的内存划分为若干个不同的区域,这些区域各有各的作用,根据java虚拟机规范,java虚拟机所管理的内存将会包括以下几个内存,入上图所示

运行时数据区 是否可能抛出错误 线程是否私有 是否存在GC 生命周期
程序计数器 × × 线程
虚拟机栈 × 线程
本地方法栈 × 线程
× 进程
方法区 × 进程

注:这里错误指OutOfMemoryError(无法申请到足够内存)

程序计数器(Program Counter Registers)

程序计数器,或者叫做PC寄存器,程序计数器是一块比较小的空间,可以看做是当前线程所执行的字节码的行号指示器,线程隔离,它的作用就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础操作都需要依赖程序计数器来完成

如果线程正在执行一个java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址,如果正在执行的是一个本地方法,这个计数器的值应该为(UndeFined)

虚拟机栈(Java Virtual Machine Stack)

线程私有,声明周期和线程一致,虚拟机描述的是java方法执行的线程内存模型,每个方法被执行时,java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态连接,方法出口等信息,每一个方法被调用执行直到完毕过程,都会对应着一个栈帧在虚拟机栈中入栈出栈的过程,在栈顶的栈帧称为当前栈帧,下面如果没有特殊说明"栈",那么就是指的java虚拟机栈,而不是本地方法栈

栈中存储什么

  • 每一个线程都有自己的栈,栈中数据都是以栈帧(Stack Frame) 的格式存在
  • 线程上正在执行的每个方法都对应一个栈帧
  • 栈帧是一个内存区块是一个数据集,维系着方法执行过程中的各种数据信息

如果java虚拟机允许栈动态扩展大小,当栈扩展时无法申请到足够内存将会抛出OOM(OutOfMemoryError)异常,如果不允许动态扩展,当线程请求的栈深度大于虚拟机允许深度将抛出StackOverflowError异常

演示StackOverflowError异常情况,代码非常简单

public class Demo {
    public static void main(String[] args) {
        main(args);
    }
}
//异常
Exception in thread "main" java.lang.StackOverflowError

当main方法调用main方法,而被调用的main方法中又调用main方法,一直循环下去,上面说了当一个方法被调用那么在栈中就会对应产生一个栈帧,当无限的栈帧添加到栈中就会导致栈溢出的情况

HotSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机倒是可以,在HotSpot虚拟机中无法演示OOM异常

设置栈的大小,默认大小为byte,如果想设置不同的单位只需要后面加上单位的简写

k=KB m=MB g=G 例如设置1024KB -Xss1024k,设置3MB -Xss3m

栈运行原理

  • JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循"先进后出" / "后进先出"原则。
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame) ,与当前栈帧相对应的方法就是当前方法(currentMethod) ,定义这个方法的类就是当前类(current class)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
  • 不同的线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传递会此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使前一个栈帧重新成为当前栈帧
  • Java方法有两种返回函数方式,一种正常的函数返回,使用return指令,另一种抛出异常,不管使用哪种方式,都会导致栈帧被弹出

返回方法的两种方式抛出异常指的是没有捕获的异常,而不是指进行try的异常

例如方法A调用方法B,如果在B方法中出现异常没有进行try,那么B方法就属于抛出异常结束,如果在方法A中将B的异常进行了捕获,并成功走完程序,那么A就属于return指令结束,如果A和B都没有进行try,那么就都属于抛出异常结束

对于上面的方法返回return或抛出异常,都知道方法返回参数为void的可以不写return,就和上面的"return指令或抛出异常结束"冲突,但是如果查看字节码的话,就会发现即使不写return,在字节码最后也会有一条return指令

局部变量表(Local Variables Table)

局部变量表是一组变量值的存储空间,定义为一个数字数组,主要存储方法参数和方法内部定义的局部变量,这些数据类型包括基本数据类型,对象引用(reference)和returnAddress类型,由于局部变量表是建立在线程的栈上的,属于线程的私有数据,所有不存在数据的安全问题(多线程并发情况),而局部变量表的大小在java编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量

  • 方法嵌套调用次数由栈的大小决定,一般来说栈越大,方法嵌套调用次数越多,对一个函数而言,它的参数和局部变量越多,使得局部变量表越大,它的栈帧就越大.以满足方法调用所需传递的信息增大的需求,进行函数调用就会占用更多的栈空间,导致其嵌套次数就会减少
  • 局部变量表的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数列表的传递过程,方法调用接收后,随着方法栈帧的销毁,局部变量表也会随之销毁
  • 如果一个变量未赋值,那么它是不会出现在该栈帧的局部变量表中的。因为是无用功

Slot

  • 在局部变量表中,最基本的存储单元是Slot(变量槽)

  • 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束

  • 局部变量表中存放编译期间可知的各种基本数据类型(8种),引用数据类型(reference),returnAddress 类型的变量

  • 在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(long和double)占用两个slot

    • byte,short,char 在存储前被转换为int boolean 也被转换为int 0代表false 非0 表示true
    • long 和double 则占用两个slot

  • JVM会为局部变量变中每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
  • 当一个实例方法被调用时,它的方法参数和方法体内部定义的局部变量会按照顺序复制到局部变量表中的每一个Slot上
  • 如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可(例如访问long或double类型)
  • 如果当前帧是由构造方法或者实例方法(非静态方法)创建的,那么该对象引用this将会放在index为0的solot处,其余参数参照表顺序继续排列

Slot重复利用

为了尽可能节省栈帧耗用的内存空间,局部变量表中的槽是可以重复利用的,方法中定义了变量,其作用域不一定会覆盖整个方法,在作用域结束后对应的变量槽就可以交给其他变量使用

例如下面这个代码

public static void main(String[] args) {
    int a = 2;
    {
        int b = 3;
    }
    int c = 10;
}

按理来说槽的最大深度应该是4,一个形参args,三个int类型变量,我们通过idea插件jclasslib来查看一下

发现最大槽数为3,也就是说,int b=3;这段代码直到执行完,走出代码块{}后,它的作用域就消失了,而它的位置剩出一个槽,于是int c=10;这段代码中的c放到了b空出的位置,我们去掉代码块在来看一下

发现去掉代码块{}后槽数变为4了,因为直到方法结束,没有能重复利用的槽位

操作数栈(Operand Stack)

  • 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression stack)

  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push) /出栈(pop)。

    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
    • 比如:执行复制、交换、求和等操作
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

  • 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

  • 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max stack的值。

  • 栈中的任何一个元素都是可以任意的Java数据类型。

    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈单位深度
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。

  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

  • 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

  • 向操作数栈添加int类型时,根据数值的大小通过不同会根据不同的添加方式压入操作数栈

    • bipush,sipush

java代码

public void testAddOperation() {
    byte i = 15;
    int j = 8;
    int k = i + j;
}
  1. 将15 放到操作数栈中

  1. 将操作数栈中的15放入局部变量表下标1的位置,因为不是静态方法,下标0的位置存放了this,所以从1开始存放

  1. 将8放入操作数栈

  1. 将操作数栈中的8放入局部变量表中的下标2位置

5.取出局部变量表中下标1的数据放入操作数栈中,当前15就是栈顶

6.将局部变量表中的下标2的数据8放入操作数栈,当前8就是栈顶

  1. 将操作数栈中的两个数据出栈,通过执行引擎进行和的操作,然后在将结果放入操作数栈

  1. 将操作数栈中得23放入局部变量表中的3下标位置,然后return,栈帧结束

动态连接

每一个栈帧内部都包含一个指向运行时常量池该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指令

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里,在类加载初始化时,一部分符号引用就被转换为直接引用,这些转化被称为静态解析,而另一部分将在运行期间转化为直接引用,这些就叫做动态连接

比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

方法调用

方法调用并不等同于方法中的代码被执行,方法调用的唯一任务就是确定被调用方法的版本(即调用哪一个方法)

解析调用

所有方法调用的目标方法在Class文件里面都是一个常量池的符号应引用,在类加载的解析阶段,会将其中一部分的符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可以确定的调用版本,并且这个方法的调用版本在运行期是不可改变的,换句话说,调用目标在程序代码中写好,编译器进行编译那一刻就已经确定下来了,这类方法的调用称为解析(Resolution)

在java中符合"编译器可知,运行期不变"这个方法的要求主要有静态方法个私有方法两大类,前者与类型直接关联,后置在外部不可访问,这两种方法的特点决定它们都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析

调用不同的类型的方法,字节码指令集中设计了不同的指令

  • invokestatic 调用静态方法
  • invokespecial 调用实例构造器init()方法,私有方法,父类中的方法
  • invokevirtual 调用所有虚方法
  • invokeinterface 调用接口方法,运行时再确定一个实现该接口的对象
  • invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,前面4条调用指令,分派逻辑都是固化在java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的

只要能被invokestatic,invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,java语言中符合这个条件的方法有4种,加上final修饰的方法(final方法使用invokevirtual指令调用),总共5种方法,这5种方法调用会在类加载的时候就可以把符号引用转化为直接引用,这些方法统称为非虚方法(Non-Virtual Method)

  1. 静态方法
  2. 私有方法
  3. 实例构造器
  4. 父类方法
  5. final修饰的方法

小例子

public class StaticResolution {
    public static void sayHello(){
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        StaticResolution.sayHello();
    }
}

可以使用javap -v 类名.class 进行反编译来查看字节码指令

也可以使用IDEA插件jclasslib来查看

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #5                  // Method sayHello:()V
         3: return

可以看到使用invokestatic指令来调用sayHello方法,非虚方法除了使用invokestatic和invokespecial调用以外,被final修饰的方法将会使用invokevirtual来调用

分派调用

可以是静态调用也可以是动态调用,依据分派的宗量数有可以分为单分派和多分派,这两类分派的组合就构成静态单分派,动态单分派,静态多分派,动态多分派,分派和java虚拟机实现重载和重写有很大的联系

静态分派

先来看一个小例子

public class Assign {
    static abstract class Human {
    }

    static class Man extends Human {
    }

    static class WoMan extends Human {
    }

    public void sayHello(Human human) {
        System.out.println("hello human!");
    }

    public void sayHello(Man man) {
        System.out.println("hello man!");
    }

    public void sayHello(WoMan woMan) {
        System.out.println("hello woMan!");
    }

    public static void main(String[] args) {
        Assign assign = new Assign();
        Human human1=new Man();
        Human human2=new WoMan();
        assign.sayHello(human1);
        assign.sayHello(human2);
    }
}
//运行结果
hello human!
hello human!

那么为什么虚拟机会选择执行参数为Human的重载版本呢?再解决这个问题前先来了解两个概念

Human man = new Man();

我们把上面"Human"称为"静态变量"(Static Type) 或者称为"外观类型"(Apparent Type),而后面的"Man"称为"实际类型"(Actual Type)或者叫"运行时类型"(Runtime Type)

静态类型和实际类型在程序中都有可能发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会发生改变,并且最终的静态类型在编译期间是可知的; 而实际类型变化的结果在运行期间才可以确定,编译器在编译程序时并不知道对象的实际类型是什么,例如下面的例子

//实际类型变化
Human human = (args == null) ? new WoMan() : new Man();

//静态类型变化
assign.sayHello((Man) human1);
assign.sayHello((WoMan) human1);

实际类型变化:先看上面的一个Human赋值操作,这时human变量的静态类型已经确定,就是Human类型,而它的实际类型赋值是一个三元运算符,只有在程序运行中判断是和否来进行赋值WoMan类型或Man类型,这种已经确定静态类型,不确定实际类型的就叫做实际类型变化

静态类型变化:下面两个强转的变量是已经确定它们的静态类型就是Human,进行强转后也可以明确知道它们的类型,这种就叫做静态类型变化

明确了这两个概念再回到上面的例子,在明确方法接收者是对象assign的前提下,使用哪个重载版本就完全取决于传入参数数量和类型,代码中故意定义了两个静态类型相同,实际类型不同的变量,但是虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据的,由于静态类型在编译期间可知,所以在编译阶段,javac编译器就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令参数中

所有依赖静态类型来决定方法执行版本的分派动作,都成为静态分派,其中最经典的应用表现就是方法的重载,静态分派是发生在编译期间,而不是由虚拟机来执行的

需要注意的是javac编译器虽然能确定方法的重载版本,但很多情况下并不是唯一的,往往只能确定一个相对合适的版本,例如下面例子

public class OverLoad {
    public void get(int num){
        System.out.println("int");
    }
    public void get(long num){
        System.out.println("long");
    }
    public void get(char num){
        System.out.println("char");
    }
    public void get(int... num){
        System.out.println("int...");
    }
}   
public static void main(String[] args) {
    OverLoad load = new OverLoad();
    load.get('1');
}

当我们直接执行,输出char,也就是旋转类型为char的重载版本,将char参数的重载方法注释后继续执行,又选择了int,注释int参数的方法后选择了long,继续注释又选择了int可变长参数

还有一点可能比较混淆,前面解析和分派这两者之间关系并不是二选一的排他关系,它们是在不同层次上去筛选,确定目标方法的过程,例如静态方法会在编译器确定,在类加载进行解析,而静态方法显然也可以有重载版本,选择重载版本过程就是在静态分派完成的

动态分派

动态分派和java实现重写有密切的关联,来看例子

public class DynamicDispatch {
    static abstract class Human{
        public abstract void sayHello();
    }
    static class Man extends Human{
        @Override
        public void sayHello() {
            System.out.println("man");
        }
    }
    static class WoMan extends Human{
        @Override
        public void sayHello() {
            System.out.println("woman");
        }
    }

    public static void main(String[] args) {
        Human man=new Man();
        Human woman=new WoMan();
        man.sayHello();
        woman.sayHello();
        man=new WoMan();
        man.sayHello();
    }
}
//结果
man
woman
woman

显然这里选择调用方法版本不可能是再根据静态类型来决定的,因为静态变量相同都是Human的两个变量man和woman都调用sayHello()方法却产生了不同的行为,甚至man在两次调用中还执行了两个不同的方法,导致这个现象原因很明显,因为这两个变量的实际类型不同,java虚拟机是如何根据实际类型来分派方法执行的版本呢?我们使用javap命令输出这段代码的字节码

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class DynamicDispatch$Man
         3: dup
         4: invokespecial #3                  // Method DynamicDispatch$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class DynamicDispatch$WoMan
        11: dup
        12: invokespecial #5                  // Method DynamicDispatch$WoMan."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
        24: new           #4                  // class DynamicDispatch$WoMan
        27: dup
        28: invokespecial #5                  // Method DynamicDispatch$WoMan."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method DynamicDispatch$Human.sayHello:()V
        36: return

其中16-21行是关键的部分,aload指令分别把两个创建的对象引用压到栈顶,这两个对象将要执行sayHello()方法的所有者,称为接收者,17-21行是调用的指令,这两条指令从字节码指令来看,无论是指令(invokevirtual)还是参数(都指向常量池中Human.sayHello的符号引用)都一模一样,但是这两条执行的执行目标方法却不相同,具体来看一下invokevirtual指令的运行时解析过程大致分为几步

  1. 找到操作数栈栈顶的第一个元素指向的对象的实际类型,记作C
  2. 如果在类型C中找到与常量池中描述符和简单名称都相符的方法,则进行访问呢权限效验,如果通过返回这个对象的直接引用,否则返回java.lang.IllegalAccessError异常
  3. 否则按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
  4. 如果始终没有找到,则抛出java.lang.AbstractMethodError异常

正是因为invokevirtual指令第一步就是在运行期确定接收者的实际类型,所有两次调用中的invokevirtual指令并不是把常量池中的符号引用直接解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是java重写的本质,把这种在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派

既然多态的根源就在于虚方法调用指令的invokevirtual的指令逻辑,那么自然得出的结论就是只对方法有效,对字段无效,字段永远不会参与多态,那个类的方法访问某个名字的字段时,该名字指的就是这个类能看到的字段,当子类声明了和父类的同名字段,虽然在子类内存中都会出现,但是子类字段会掩蔽父类的同名字段,看下面例子

public class Demo {

    static class Father {
        public int i = 1;
        public Father(){
            i=2;
            showI();
        }
        public void showI() {
            System.out.println("father"+i);
        }
    }
    static class Son extends Father {
        public int i = 3;
        public Son(){
            i=4;
            showI();
        }
        public void showI() {
            System.out.println("son"+i);
        }
    }

    public static void main(String[] args) {
        Father p=new Son();
        System.out.println(p.i);
    }
}
//结果
son0
son4
2

输出两句都为son,因为son类创建时,隐式调用父类构造,而父类构造中showI()是一个虚方法,也就是使用invokevirtual指令来执行,上面写过invokervirtual的执行过程,那么就调用了son类的showI()方法,而这时son类还没有进行初始化,int i还是为0,所以第一次输出son0,当父类构造完成后回到子类,子类进行i=4赋值操作,然后调用showI(),第二次显示为son4,最后一句通过静态类型访问到父类中的i为2

单分派和多分派

方法的接收者与方法的参数统称为方法的宗量

public class Demo {
    static class Open {
    }

    static class Book {
    }

    public static class Father {
        public void show(Open open) {
            System.out.println("father open");
        }

        public void show(Book book) {
            System.out.println("father book");
        }
    }

    public static class Son extends Father {
        public void show(Open open) {
            System.out.println("Son open");
        }

        public void show(Book book) {
            System.out.println("Son book");
        }
    }
    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.show(new Open());
        son.show(new Book());
    }
}
//结果
father book
Son book

在main方法中调用了两次show方法,首先要关注的是编译器的选择过程,也就是静态分派的过程,选择目标的依据有两点,一:静态类型是Father还是Son,二方法参数是Book还是Open,这次选择最终产生两条invokevirtual指令,而这两条指令指向Father.show(Open)方法和Father.show(Book)方法,因为是根据两个宗量进行选择,所以java语言的静态分派属于多分派类型

        16: aload_1
        17: new           #6                  // class Demo$Open
        20: dup
        21: invokespecial #7                  // Method Demo$Open."<init>":()V
        24: invokevirtual #8                  // Method Demo$Father.show:(LDemo$Open;)V
        27: aload_2
        28: new           #9                  // class Demo$Book
        31: dup
        32: invokespecial #10                 // Method Demo$Book."<init>":()V
        35: invokevirtual #11                 // Method Demo$Father.show:(LDemo$Book;)V

再来看运行阶段,在执行invokevirtual指令是已经确定目标方法的签名为show(Open),唯一影响虚拟机选择的就是接收者的实例是son还是Father,只有一个影响宗量,所有动态分派属于单分派类型

动态分派是执行非常繁琐的动作,而且动态分派的方法版本选择过程需要运行时再接收者类型的方法元数据中搜索合适的方法,因此java虚拟机实现基于性能考虑,真正运行时一般不会如此反复的搜索类型元数据,而是建立一个虚方法表(Virtual Method Table),使用虚方法表索引来替代元数据查找以提高性能

虚方法表中存存放着各个方法的实际入口,如果某个方法在子类中没重写,那么子类的虚方法表中的地址入口和父类相同的方法的地址入口是相同的,都指向父类的实现入口,如果子类中重写了这个方法,子类虚方法表中的地址就会被替换为指向子类实现版本的入口地址

方法返回地址

一个方法退出有两种方式,一是遇到没有进行处理的异常,二是执行引擎执行到return字节码指令,这时候可能有返回值传递给上层调用者(调用当前方法的方法称为调用者或主调方法),方法是否有返回值以及返回值的类型根据遇到何种返回指令来决定的,如果是遇到没有处理的异常进行退出的,这种退出称为异常调用完成,方法使用异常调用完成退出是不会给它的上层调用者提供任何返回值的

无论采用那种方式退出,在方法退出后,都必须返回到最初方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,来帮助恢复它的上层主调方法的执行状态,一般来说,方法正常退出时,主调方法的PC计数器就可以作为方法返回地址,栈帧中很可能就会保存这个计数器的值,而方法异常退出时,方法的返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息

方法的退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者的操作数栈中,调整PC计数器的值以指向方法调用指令的后面一条指令等

一些附加信息

java虚拟机规范允许虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试,性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现

本地方法

简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如c。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中你可以用extern "c"告知C++编译器去调用一个c的函数。

在定义一个native method时,并不提供实现体(有些像定义一个Javainterface) ,因为其实现体是由非java语言在外面实现的。

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合c/C++程序。

  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。

  • 本地方法栈,也是线程私有的。

  • 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)

    • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量, Java虚拟机将会抛出一个stackoverflowError异常。
    • 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
  • 本地方法是使用c语言实现的。

  • 它的具体做法是Native Method stack中登记native方法,在Execution Engine执行时加载本地方法库

    • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    • 它甚至可以直接使用本地处理器中的寄存器
    • 直接从本地内存的堆中分配任意数量的内存
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。

  • 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。

本文仅个人理解,如果有不对的地方欢迎评论指出或私信,谢谢٩(๑>◡<๑)۶

posted @ 2021-01-29 15:20  Jame!  阅读(117)  评论(0编辑  收藏  举报