关于java:我所知道JVM虚拟机之运行时数据区的虚拟机栈

50次阅读

共计 16645 个字符,预计需要花费 42 分钟才能阅读完成。

前言

上篇文章介绍了运行时数据区的概述、以及 PC 寄存器,这篇文章介绍的是虚拟机栈

一、虚拟机栈的概述


虚拟机栈呈现的背景

================================

咱们晓得 Java 虚拟机是基于栈的一种设计架构,长处是跨平台指令集小编译器容易实现,毛病是性能降落,实现同样的性能须要更多的指令

因为跨平台性所以不能设计为基于寄存器的(设计成基于寄存器的,耦合度高,性能会有所晋升,因为能够对具体的 CPU 架构进行优化,然而跨平台性大大降低

内存中的栈与堆

================================

栈是运行时的单位,而堆是存储的单位

  • 栈解决程序的运行问题,即程序如何执行,或者说如何解决数据。
  • 堆解决的是数据存储的问题,即数据怎么放,放哪里

能够看看以下这幅图有点形象的意思

虚拟机栈根本内容

================================

Java 虚拟机栈(Java Virtual Machine Stack),晚期也叫 Java 栈。

每个线程在创立时都会创立一个虚拟机栈,其外部保留一个个的栈帧(Stack Frame),对应着一次次的 Java 办法调用,栈是线程公有的

咱们应用一个示例领会一下

public class StackTest {public void methodA() {
        int i = 10;
        int j = 20;
        methodB();}

    public void methodB(){
        int k = 30;
        int m = 40;
    }
    
    public static void main(String[] args) {StackTest test = new StackTest();
        test.methodA();}
}

而虚拟机栈呢是随着线程创立而创立的,所以它的生命周期也就是线程完结了,该虚拟机栈也销毁了

它主管着 Java 程序的运行,保留办法的局部变量、局部后果、并参加办法的调用和返回

虚拟机栈的特点

================================

因为栈是一种疾速无效的调配存储形式,访问速度仅次于程序计数器。

所以 JVM 间接对 Java 栈的操作只有两个

  • 每个办法执行,随同着进栈(入栈、压栈)
  • 执行完结后的出栈工作

所以对于栈来说不存在垃圾回收问题(不须要 GC),然而可能存在 OOM

面试题:开发中遇见的异样有哪些?

================================

思路:能够提到开发中的问题、框架的问题、以及 JVM 虚拟机的内存溢出等等

咱们这里阐明一下:栈中可能呈现的异样

Java 虚拟机标准容许Java 栈的大小是动静的或者是固定不变的

如果采纳固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量能够在线程创立的时候独立选定。

如果线程申请调配的栈容量超过 Java 虚拟机栈容许的最大容量,Java 虚拟机将会抛出一个 StackoverflowError 异样

如果 Java 虚拟机栈能够动静扩大,并且在尝试扩大的时候无奈申请到足够的内存,或者在创立新的线程时没有足够的内存去创立对应的虚拟机栈,那Java 虚拟机将会抛出一个 OutofMemoryError 异样

咱们通过例子演示一下 StackoverflowError 这个异常情况

public class StackErrorTest {public static void main(String[] args) {main(args);
    }
}
// 运行后果如下:Exception in thread "main" java. lang.]StackOverflowError
atcom.atguigu.java.StackErrorTest.main(StackErrorTest. java:11)

这种本人调本人的状况很容易呈现 StackoverflowError 这个异常情况

那么咱们说容许Java 栈的大小是动静的或者是固定不变的,那么如何设置栈的大小呢?

咱们能够应用 参数 -Xss 选项来设置线程的最大栈空间,栈的大小间接决定了函数调用的最大可达深度。

以下是一个官网的标准阐明:

-Xss size

Sets the thread stack size (in bytes). Append the letter k or K to indicate KB,
m or M to indicate MB, and g or G to indicate GB. The default value depends on the platform:

Linux/x64 (64-bit): 1024 KB
macOS (64-bit): 1024 KB
Oracle Solaris/x64 (64-bit): 1024 KB
Windows: The default value depends on virtual memory

接下来咱们在刚刚的示例上演示一下咱们当初的栈是多少大小

public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {System.out.println(count);
        count++;
        main(args);
    }
}
// 运行后果如下:1
....
11420
Exception in thread "main" java. lang.]StackOverflowError

当咱们打印到 11420 的时候就出现异常了,这阐明默认状况下大小为 11420,接下来咱们设置一下大小

这时咱们再运行起办法就能够看到咱们批改了栈的大小容量了

public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {System.out.println(count);
        count++;
        main(args);
    }
}
// 运行后果如下:1
....
2465
Exception in thread "main" java. lang.]StackOverflowError

二、虚拟机栈的存储单位


栈中存储什么呢?

================================

每个线程都有本人的栈,栈中的数据都是以栈帧(Stack Frame)的格局 存在

在这个线程上正在执行的 每个办法都各自对应一个栈帧(Stack Frame)

栈帧是 一个内存区块是一个数据集,维系着办法执行过程中的各种数据信息。

栈运行原理

================================

JVM 间接对 Java 栈的操作只有两个,就是 对栈帧的压栈和出栈,遵循先进后出(后进先出)准则

在一条流动线程中一个工夫点上,只会有一个流动的栈帧。

即只有以后 正在执行的办法的栈帧(栈顶栈帧)是无效的,它被称为 以后栈帧(Current Frame)

与以后栈帧绝对应的办法就是 以后办法(Current Method),定义这个办法的类就是 以后类(Current Class)

而咱们执行引擎运行的所有字节码指令只针对以后栈帧进行操作,即操作那个办法就那个无效

若在一个办法中调用了其余办法,对应的新的栈帧会被创立进去,放在栈的顶端,成为新的以后帧。

栈桢的内部结构

================================

下面咱们说了栈的根本存储构造是栈桢还相熟了栈的执行原理 那么咱们思考一下:栈桢里有啥呢?

首先咱们先介绍一下栈桢中存储了些什么?次要分成以下几局部

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或表达式栈)
  • 动静链接(Dynamic Linking)(或指向运行时常量池的办法援用)
  • 办法返回地址(Return Address)(或办法失常退出或者异样退出的定义)
  • 一些附加信息

咱们后面说到一个栈能够设置固定大小或者动静扩大,那么栈到底能放多少栈桢呢?取决于栈桢大小

而栈桢的大小又是什么因数决定的呢?取决于这几个局部,次要是局部变量表与操作数栈

咱们当初看到的是单线程下的状况,咱们能够看看多个线程下是怎么样的?

因为咱们后面提到每个线程下的栈都是公有的,所以有本人各自的栈并且每个栈外面都有很多栈帧

三、栈桢的局部变量表


刚刚在下面介绍了栈桢大抵由几种局部组成:局部变量表、操作数栈、动静链接、办法返回地、一些附加信息

咱们当初就从局部变量表开始理解这个栈桢的内部结构

局部变量表介绍

================================

局部变量表也被称之为 局部变量数组或本地变量表

其实是定义为一个数字数组次要用于存储 办法参数和定义在办法体内的局部变量,这些数据类型包含各类根本数据类型、对象援用(reference),以及 returnAddress 返回值类型

因为局部变量表是 建设在线程的栈上 是线程的 公有数据 ,因而 不存在数据安全问题

局部变量表所需的容量大小是在 编译期确定下来的 ,并保留在办法的Code 属性的 maximum local variables 数据项 中。在办法运行期间是 不会扭转局部变量表的大小

对于编译期确定下来这件事件呢,咱们能够应用示例来领会与验证一下

public class LocalVariablesTest {

    private int count = 0;

    public static void main(String[] args) {LocalVariablesTest test = new LocalVariablesTest();
        int num = 10;
        test.test1();}
    
    public void test1() {Date date = new Date();
        String name1 = "atguigu.com";
        System.out.println(date + name1);
    }
}

而后咱们再通过字节码的察看一下是否是在编译期确定下来大小

以及 办法嵌套调用的次数由栈的大小决定 。一般来说, 栈越大,办法嵌套调用次数越多

  • 对一个函数而言,它的参数和局部变量越多,使得局部变量表收缩,它的栈帧就越大,以满足办法调用所需传递的信息增大的需要
  • 进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会缩小

并且 局部变量表中的变量只在以后办法调用中无效

  • 在办法执行时,虚拟机通过应用局部变量表实现参数值到参数变量列表的传递过程
  • 当办法调用完结后,随着办法栈帧的销毁,局部变量表也会随之销毁

为了对局部变量表有一个更分明的了解,咱们间接用 jclasslib 来看字节码,以 main 办法为例来解说。

咱们通过插件能够关上这个类观看具体的 Code 里的信息

局部变量表存储单位

================================

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

而参数值的寄存总是从局部变量数组索引 0 的地位开始,到数组长度 - 1 的索引完结

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

  • byte、short、char 在贮存前辈转换为 int,boolean 也被转换为 int,0 示意 false,非 0 示意 true
  • long 和 double 则占据两个 slot

JVM 会为局部变量表中的每一个 Slot 都调配一个拜访索引,通过这个索引即可胜利拜访到局部变量表中指定的局部变量值

当一个实例办法被调用的时候,它的办法参数和办法体外部定义的局部变量将会依照程序被复制到局部变量表中的每一个 slot 上

如果以后帧是由构造方法或者实例办法创立的,那么该对象援用 this 将会寄存在 index 为 0 的 slot 处,其余的参数依照参数表程序持续排列。(this 也相当于一个变量)

咱们能够应用代理示例领会一下 this 这个变量

public void test3() {this.count++;}

接着咱们运行插件查看这个类的字节码与 test3 这个办法的局部变量表

所以当咱们能在构造方法与办法中应用 this 变量是因为它存储在局部变量表,无奈再 static 办法中应用是因为它不存在所以不能调用

对于 Slot 的反复利用

================================

其实栈帧中的局部变量表中的槽位是能够重用的

如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量变就很有可能会复用过期局部变量的槽位,从而达到节俭资源的目标

咱们增加多一个办法在原来的类中并应用示例代码来领会一下这个意思

public void test4() {
    int a = 0;
    {
        int b = 0;
        b = a + 1;
    }
    // 变量 c 应用之前曾经销毁的变量 b 占据的 slot 的地位
    int c = a + 1;
}

依照咱们的思路运行起来并且查看 test4 的字节码与相干信息

动态变量与局部变量的比照

================================

首先咱们先介绍个别有哪几种形式分变量

  • 依照数据类型分:① 根本数据类型 ② 援用数据类型
  • 依照在类中申明地位分有:成员变量、局部变量

成员变量:在应用前都经验过默认初始化赋值并且有 类变量、实例变量辨别

局部变量:在应用前,必须要进行显式赋值的!否则编译不通过。

// 局部变量未赋值的谬误示范
public void test5Temp(){
    int num;
    System.out.println(num);// 错误信息: 变量 num 未进行初始化
}

对于类变量有两次初始化的机会

  • 一次在 linking 的 prepare 阶段给类变量默认赋值
  • 一次在 initial 阶段给类变量显式赋值(即动态代码块赋值)

而实例变量:随着对象的创立,会在堆空间中调配实例变量空间,并进行默认赋值

补充阐明

在栈帧中,与性能调优关系最为亲密的局部就是后面提到的局部变量表。在办法执行时,虚拟机应用局部变量表实现办法的传递

局部变量表中的变量也是重要的垃圾回收根节点,只有被局部变量表中间接或间接援用的对象都不会被回收

四、栈桢的操作数栈


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

而操作数栈,在办法执行过程中会依据字节码指令,往栈中写入数据或提取数据 即入栈(push)和 出栈(pop)

  • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。应用它们后再把后果压入栈
  • 比方:执行复制、替换、求和等操作

咱们依据类的示例办法代码块与字节码来看看这些信息

由此可见操作数栈,次要用于保留计算过程的两头后果,同时作为计算过程中变量长期的存储空间。

能够说操作数栈就是 JVM 执行引擎的一个工作区,当一个办法刚开始执行的时候,一个新的栈帧也会随之被创立进去,这时办法的操作数栈是空的

每一个操作数栈都会领有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保留在办法的 Code 属性中,为 maxstack 的值

可查看 testAddOperation()办法的字节码查看相干信息,原 locals 是局部变量的深度

栈中的任何一个元素都是能够任意的 Java 数据类型,咱们之前也说过

  • 32bit 的类型占用一个栈单位(槽)深度
  • 64bit 的类型占用两个栈单位(槽)深度

与槽不是一样通过索引拜访形式拜访数据,而是只能 通过规范的入栈和出栈操作来实现一次数据拜访

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

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

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

操作数栈的代码追踪

================================

咱们采纳刚刚示例来进行代码追踪一下刚刚下面提到操作数栈具体实现

public void testAddOperation() {
   //byte、short、char、boolean:都以 int 型来保留
   byte i = 15;
   int j = 8;
   int k = i + j;
}

在下面图中,咱们也看到有绝对应的字节码指令

 0 bipush 15
 2 istore_1
 3 bipush 8
 5 istore_2
 6 iload_1
 7 iload_2
 8 iadd
 9 istore_3
10 return

咱们剖析这些字节码指令哪些是操作数栈相干,哪些又与局部变量表相干

从第一步开始 bipush 15,这时将 15push 进操作数栈(代码中 byte、short、char、boolean:都以 int 型来保留)

第二步 PC 寄存器往下移将操作数栈的元素存储到局部变量表 1 的地位,并且局部变量表的减少了一个元素,而操作数栈为空

咱们之前提到过因为在非静态方法,局部变量表索引为:0 的地位是寄存的而是 this 变量

第三步 PC 寄存器下移指向的是下一行。让操作数 8 也入栈并且执行 store 操作,存入局部变量表中

第四步 PC 寄存器往下移将操作数栈的元素存储到局部变量表 2 的地位,并且局部变量表的减少了一个元素,而操作数栈为空

第五步从局部变量表中,顺次将数据放在操作数栈中 iload_1:取出索引为 1,iload_2:取出索引为 2

第六步执行 iadd 操作指令将操作数栈中的两个元素执行相加操作

第七步 PC 寄存器往下挪动执行 istore_3,并且局部变量表的减少了一个元素,而操作数栈为空

第八步因为前面没有相干操作并且也没有返回值,所以就间接返回完结操作

四、栈顶缓存技术


后面提过,基于栈式架构的虚拟机所应用的零地址指令更加紧凑,但实现一项操作的时候必然须要应用更多的入栈和出栈指令

这同时也就意味着将须要更多的指令分派(instruction dispatch)次数(也就是你会发现指令很多)和导致内存读 / 写次数多,效率不高

所以咱们将栈顶元素全副缓存在物理 CPU 的寄存器中,以此升高对内存的读 / 写次数,晋升执行引擎的执行效率

寄存器的次要长处:指令更少,执行速度快,然而指令集(也就是指令品种)很多

五、栈桢的动静链接


每一个栈帧外部都蕴含一个指向运行时常量池中该栈帧所属办法的援用,目标就是为了 反对以后办法的代码可能实现动静链接(Dynamic Linking),比方:invokedynamic 指令

在 Java 源文件被编译到字节码文件中时,所有的变量和办法援用都作为符号援用(Symbolic Reference)保留在 class 文件的常量池里

比如说当咱们一个办法调用了另外的其余办法时,就是通过常量池中指向办法的符号援用来示意的,那么动静链接的作用就是为了将这些符号援用转换为调用办法的间接援用

咱们应用一个例子来领会一下这个动静链接

public class DynamicLinkingTest {

    int num = 10;

    public void methodA(){System.out.println("methodA()....");
    }
    public void methodB(){System.out.println("methodB()....");
        methodA();
        num++;
    }
}

这时咱们运行这个类的字节码并且看看办法 B 调用办法 A 时是什么状况

public void methodB();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String methodB()....
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: aload_0
         9: invokevirtual #7                  // Method methodA:()V
        12: aload_0
        13: dup
        14: getfield      #2                  // Field num:I
        17: iconst_1
        18: iadd
        19: putfield      #2                  // Field num:I
        22: return
      LineNumberTable:
        line 16: 0
        line 18: 8
        line 20: 12
        line 21: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  this   Lcom/atguigu/java1/DynamicLinkingTest;
}

发现没有,咱们的办法 b 在字节码指令中,通过 invokevirtual #7 指令调用了办法 A,那么 #7 是个啥呢?

咱们能够看看常量池里的定义是怎么定义的

Constant pool:
   #1 = Methodref          #9.#23         // java/lang/Object."<init>":()V
   #2 = Fieldref           #8.#24         // com/atguigu/java1/DynamicLinkingTest.num:I
   #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = String             #27            // methodA()....
   #5 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = String             #30            // methodB()....
   #7 = Methodref          #8.#31         // com/atguigu/java1/DynamicLinkingTest.methodA:()V
   #8 = Class              #32            // com/atguigu/java1/DynamicLinkingTest
   #9 = Class              #33            // java/lang/Object
  #10 = Utf8               num
  #11 = Utf8               I
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               Lcom/atguigu/java1/DynamicLinkingTest;
  #19 = Utf8               methodA
  #20 = Utf8               methodB
  #21 = Utf8               SourceFile
  #22 = Utf8               DynamicLinkingTest.java
  #23 = NameAndType        #12:#13        // "<init>":()V
  #24 = NameAndType        #10:#11        // num:I
  #25 = Class              #34            // java/lang/System
  #26 = NameAndType        #35:#36        // out:Ljava/io/PrintStream;
  #27 = Utf8               methodA()....
  #28 = Class              #37            // java/io/PrintStream
  #29 = NameAndType        #38:#39        // println:(Ljava/lang/String;)V
  #30 = Utf8               methodB()....
  #31 = NameAndType        #19:#13        // methodA:()V
  #32 = Utf8               com/atguigu/java1/DynamicLinkingTest
  #33 = Utf8               java/lang/Object
  #34 = Utf8               java/lang/System
  #35 = Utf8               out
  #36 = Utf8               Ljava/io/PrintStream;
  #37 = Utf8               java/io/PrintStream
  #38 = Utf8               println
  #39 = Utf8               (Ljava/lang/String;)V

咱们能够常量池的定义推演一下到底是怎么回事呢?首先咱们是看看#7 = Methodref #8.#31

  • 找到 #8:#8 = Class #32
  • 找到 #32:#32 = Utf8 com/atguigu/java1/DynamicLinkingTest

论断:通过 #8 咱们找到了 DynamicLinkingTest 这个类

  • 找到 #31:#31 = NameAndType #19:#13
  • 找到 #19:#19 = Utf8 methodA
  • 找到 #13:#13 = Utf8 ()V

论断:通过 #7 咱们就能找到须要调用的 methodA() 办法,并进行调用

在下面,其实还有很多符号援用,比方 Object、System、PrintStream 等等

为什么要用常量池呢?

因为在不同的办法,都可能调用常量或者办法,所以只须要存储一份即可,而后记录其援用即可,节俭了空间。

常量池的作用:就是为了提供一些符号和常量,便于指令的辨认

六、办法的调用


在 JVM 中,将符号援用转换为调用办法的间接援用,它与办法的绑定机制相干

动态链接:

当一个字节码文件被装载进 JVM 外部时,如果 被调用的指标办法在编译期确定,且运行期放弃不变时 ,这种状况下将调用办法的 符号援用转换为间接援用 的过程称之为 动态链接

动静链接:

如果被调用的办法 在编译期无奈被确定下来 ,也就是说, 只可能在程序运行期将调用的办法的符号转换为间接援用 ,因为这种援用转换过程具备动态性,因而也被称之为 动静链接

那么对于对应的办法的绑定机制为:晚期绑定(Early Binding) 和早期绑定(Late Binding)

绑定是一个字段、办法或者类在符号援用被替换为间接援用的过程,这仅仅产生一次。

晚期绑定:

晚期绑定就是指被调用的指标办法如果在编译期可知,且运行期放弃不变时,即可将这个办法与所属的类型进行绑定,这样一来,因为明确了被调用的指标办法到底是哪一个,因而也就能够应用动态链接的形式将符号援用转换为间接援用。

早期绑定:

如果被调用的办法在编译期无奈被确定下来,只可能在程序运行期依据理论的类型绑定相干的办法,这种绑定形式也就被称之为早期绑定

咱们举一个例子来领会一下这种晚期绑定与早期绑定

// 定义一个动物进食的父类
class Animal {public void eat() {System.out.println("动物进食");
    }
}
// 定义一个捕猎的接口
interface Huntable {void hunt();
}

这时咱们有两个动物:猫和狗,去做一些具体的事件信息

class Dog extends Animal implements Huntable {
    @Override
    public void eat() {System.out.println("狗吃骨头");
    }

    @Override
    public void hunt() {System.out.println("捕食耗子,多管闲事");
    }
}
class Cat extends Animal implements Huntable {

    @Override
    public void eat() {super.eat();// 体现为:晚期绑定
        System.out.println("猫吃鱼");
    }

    @Override
    public void hunt() {System.out.println("捕食耗子,理所当然");
    }
}

这时咱们再创立一个 Test 类调用对应 eat 办法与 hunt 办法

public class AnimalTest {public void showAnimal(Animal animal) {animal.eat();// 体现为:早期绑定
    }

    public void showHunt(Huntable h) {h.hunt();// 体现为:早期绑定
    }
}

因为咱们在编译期没有对具体的办法确定下来,只能在运行期间绑定理论的类,所以他们都是早期绑定

咱们能够查看一下字节码看看是否是这样,运行起来一起看看吧

随着高级语言的横空出世,相似于 Java 一样的基于面向对象的编程语言现在越来越多,只管这类编程语言在语法格调上存在肯定的差异

然而它们彼此之间始终保持着一个共性,那就是 都反对封装、继承和多态等面向对象个性 ,既然这一类的编程语言具备多态个性,那么天然也就具备 晚期绑定和早期绑定两种绑定形式

而 Java 中任何一个一般的办法其实都具备虚函数的特色,它们相当于 C ++ 语言中的虚函数(C++ 中则须要应用关键字 virtual 来显式定义)。

如果在 Java 程序中不心愿某个办法领有虚函数的特色时,则能够应用 关键字 final来标记这个办法

虚办法与非虚办法的区别

================================

如果办法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。咱们称为非虚办法。

静态方法、公有办法、final 办法、实例结构器、父类办法都是非虚办法

其余办法称为虚办法

子类对象的多态的应用前提:类的继承关系、办法的重写

虚拟机中调用办法的指令

================================

一般指令:
  • invokestatic:调用静态方法,解析阶段确定惟一办法版本
  • invokespecial:调用 <init> 办法、公有及父类办法,解析阶段确定惟一办法版本
  • invokevirtual:调用所有虚办法
  • invokeinterface:调用接口办法
动静调用指令:
  • invokedynamic:动静解析出须要调用的办法,而后执行

前四条指令固化在虚拟机外部,办法的调用执行不可人为干涉

而 invokedynamic 指令则反对由用户确定办法版本。

其中invokestatic 指令和 invokespecial 指令调用的办法称为非虚办法,其余的(final 润饰的除外)称为虚办法

咱们应用示例领会一下来辨别虚办法与非虚办法,咱们先创立一个父类

class Father {public Father() {System.out.println("father 的结构器");
    }
    public static void showStatic(String str) {System.out.println("father" + str);
    }
    public final void showFinal() {System.out.println("father show final");
    }
    public void showCommon() {System.out.println("father 一般办法");
    }
}

接着创立一个子类继承这个父类

public class Son extends Father {public Son() {
        //invokespecial
        super();}

    public Son(int age) {
        //invokespecial
        this();}

    // 不是重写的父类的静态方法,因为静态方法不能被重写!public static void showStatic(String str) {System.out.println("son" + str);
    }

    private void showPrivate(String str) {System.out.println("son private" + str);
    }

    public void show() {
        //invokestatic
        showStatic("atguigu.com");
        //invokestatic
        super.showStatic("good!");
        //invokespecial
        showPrivate("hello!");
        //invokespecial
        super.showCommon();

        //invokevirtual
        showFinal();// 因为此办法申明有 final,不能被子类重写,所以也认为此办法是非虚办法。// 虚办法如下:/*
        invokevirtual  你没有显示的加 super.,编译器认为你可能调用子类的 showCommon(即便 son 子类没有重写,也          会认为),所以编译期间确定不下来,就是虚办法。*/
        showCommon();
        info();

        MethodInterface in = null;
        //invokeinterface
        in.methodA();}

    public void info() {}

    public void display(Father f) {f.showCommon();
    }

    public static void main(String[] args) {Son so = new Son();
        so.show();}
}

interface MethodInterface {void methodA();
}

这时咱们运行看看后果就能够看到这些办法是否为虚办法

当咱们在编译时确定下来的都是非虚办法,咱们也能够对这个 show 办法运行查看字节码

invokedynamic 指令

JVM 字节码指令集始终比较稳定,始终到 Java7 中才减少了一个 invokedynamic 指令,这是Java 为了实现【动静类型语言】反对而做的一种改良

然而在 Java7 中并没有提供间接生成 invokedynamic 指令的办法,须要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令。

直到 Java8 的 Lambda 表达式的呈现,invokedynamic 指令的生成,在 Java 中才有了间接的生成形式

咱们应用一个示例来领会一下这个指令

@FunctionalInterface
interface Func {public boolean func(String str);
}

public class Lambda {public void lambda(Func func) {return;}

    public static void main(String[] args) {Lambda lambda = new Lambda();

        Func func = s -> {return true;};

        lambda.lambda(func);

        lambda.lambda(s -> {return true;});
    }
}

这时咱们运行字节码就能够看到具体的指令了

七、对于动静语言和动态语言


动静类型语言和动态类型语言两者的区别就在于对类型的查看是在编译期还是在运行期,满足前者就是动态类型语言,反之是动静类型语言

动态类型语言是 判断变量本身的类型信息 ;动静类型语言是 判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动静语言的一个重要特色

  • Java:String info =“mogu blog”; (Java 是动态类型语言的,会先编译就进行类型查看)
  • JS:var name =“shkstart”; var name = 10;(运行时才进行查看)
  • Python: info = 130.5 (运行时才查看)

八、Java 语言中办法重写的实质


会找到操作数栈顶的第一个元素所执行的对象的理论类型,记作 C

如果在类型 C 中找到与常量中的形容合乎简略名称都相符的办法,则进行拜访权限校验

  • 如果通过则返回这个办法的间接援用,查找过程完结
  • 如果不通过,则返回 java.lang.IllegalAccessError 异样

否则,依照继承关系从下往上顺次对 C 的各个父类进行第 2 步的搜寻和验证过程

如果始终没有找到适合的办法,则抛出 java.lang.AbstractMethodError 异样

对于 IllegalAccessError 介绍

================================

程序试图拜访或批改一个属性或调用一个办法,这个属性或办法,你没有权限拜访。

个别的,这个会引起编译器异样。这个谬误如果产生在运行时,就阐明一个类产生了不兼容的扭转

比方你把应该有的 jar 包放从工程中拿走了,或者 Maven 中存在 jar 包抵触

咱们刚刚再说如果子类找不到,还要从下往上找其父类,十分耗时

因而为了进步性能,JVM 采纳在类的办法区建设一个虚办法表(virtual method table)来实现,非虚办法不会呈现在表中

每个类中都有一个虚办法表,表中寄存着各个办法的理论入口

虚办法表会在类加载的链接阶段被创立并开始初始化,类的变量初始值筹备实现之后,JVM 会把该类的虚办法表也初始化结束

比如说 son 在调用 toString 的时候,Son 没有重写过,Son 的父类 Father 也没有重写过,那就间接调用 Object 类的 toString

防止了之前先找 Son–> 再找 Father–> 最初才到 Object 的这样的一个过程

九、栈桢的办法的返回地址


在一些帖子里,办法返回地址、动静链接、一些附加信息 也叫做帧数据区

它寄存调用该办法的 pc 寄存器的值

一个办法的完结,有两种形式:

  • 失常执行实现
  • 呈现未解决的异样,非正常退出

无论通过哪种形式退出,在办法退出后都返回到该办法被调用的地位。

办法失常退出时,调用者的 pc 计数器的值作为返回地址,即调用该办法的指令的下一条指令的地址

而通过异样退出的,返回地址是要通过异样表来确定,栈帧中个别不会保留这部分信息

实质上,办法的退出就是以后栈帧出栈的过程。

此时,须要复原下层办法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置 PC 寄存器值等,让调用者办法继续执行上来

失常实现进口和异样实现进口的区别在于:通过异样实现进口退出的不会给他的下层调用者产生任何的返回值

办法的失常退出
  • 执行引擎遇到任意一个办法返回的字节码指令(return),会有返回值传递给下层的办法调用者,简称失常实现进口;
  • 一个办法在失常调用实现之后,到底须要应用哪一个返回指令,还须要依据办法返回值的理论数据类型而定。

在字节码指令中,返回指令蕴含:

  • ireturn:当返回值是 boolean,byte,char,short 和 int 类型时应用
  • lreturn:Long 类型
  • freturn:Float 类型
  • dreturn:Double 类型
  • areturn:援用类型
  • return:返回值类型为 void 的办法、实例初始化办法、类和接口的初始化办法
办法的异样退出

在办法执行过程中遇到异样(Exception),并且这个异样没有在办法内进行解决,也就是只有在本办法的异样表中没有搜寻到匹配的异样处理器,就会导致办法退出,简称异样实现进口

办法执行过程中,抛出异样时的异样解决,存储在一个异样处理表,不便在产生异样的时候找到解决异样的代码

异样处理表:

反编译字节码文件,可失去 Exception table

  • from:字节码指令起始地址
  • to:字节码指令完结地址
  • target:出现异常跳转至地址为 11 的指令执行
  • type:捕捉异样的类型

十、栈桢的一些附加信息


栈帧中还容许携带与 Java 虚拟机实现相干的一些附加信息。例如:对程序调试提供反对的信息

十一、相干面试题


举例栈溢出的状况?

SOF(StackOverflowError),栈大小分为固定的,和动态变化。如果是固定的就可能呈现 StackOverflowError。如果是动态变化的,内存不足时就可能呈现 OOM

调整栈大小,就能保障不呈现溢出么?

不能保障不溢出,只能保障 SOF 呈现的几率小

调配的栈内存越大越好么?

不是,肯定工夫内升高了 OOM 概率,然而会挤占其它的线程空间,因为整个虚拟机的内存空间是无限的

垃圾回收是否波及到虚拟机栈?

不会

办法中定义的局部变量是否线程平安?
具体问题具体分析

  • 如果只有一个线程才能够操作此数据,则必是线程平安的。
  • 如果有多个线程操作此数据,则此数据是共享数据。如果不思考同步机制的话,会存在线程平安问题。

如果对象是在外部产生,并在外部沦亡,没有返回到内部,那么它就是线程平安的,反之则是线程不平安的

/**
 * 面试题:* 办法中定义的局部变量是否线程平安?具体情况具体分析
 *
 *   何为线程平安?*      如果只有一个线程才能够操作此数据,则必是线程平安的。*      如果有多个线程操作此数据,则此数据是共享数据。如果不思考同步机制的话,会存在线程平安问题。*/
public class StringBuilderTest {

    int num = 10;

    //s1 的申明形式是线程平安的(只在办法外部用了)public static void method1(){
        //StringBuilder: 线程不平安
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        //...
    }
    //sBuilder 的操作过程:是线程不平安的(作为参数传进来,可能被其它线程操作)public static void method2(StringBuilder sBuilder){sBuilder.append("a");
        sBuilder.append("b");
        //...
    }
    //s1 的操作:是线程不平安的(有返回值,可能被其它线程操作)public static StringBuilder method3(){StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1;
    }
    //s1 的操作:是线程平安的(s1 本人沦亡了,最初返回的智商 s1.toString 的一个新对象)public static String method4(){StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1.toString();}

    public static void main(String[] args) {StringBuilder s = new StringBuilder();


        new Thread(() -> {s.append("a");
            s.append("b");
        }).start();

        method2(s);

    }

}

参考资料


尚硅谷:JVM 虚拟机(宋红康老师)

正文完
 0