当前位置:   article > 正文

深入理解Java虚拟机:虚拟机字节码执行引擎_java 栈顶元素推入局部变量表

java 栈顶元素推入局部变量表

概述

    在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能有解释执行和编译执行两种选择,也可能两者兼备,甚至还可能包含几个不同级别的编译器执行引擎。但从外观上,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。


运行时栈帧结构

    栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧(每一个)存储了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

    一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,活动线程,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有的字节码都只针对当前栈帧进行操作,栈帧的结构如下图:

这里写图片描述


局部变量表

    局部变量表存放的一组变量的存储空间。存放方法参数和方法内部定义的局部变量表。在java编译成class的时候,已经确定了局部变量表所需分配的最大容量。

    局部变量表以变量槽(Variable Solt, 下称 Solt)为最小单位,虚拟机规范没有明确规定一个Slot占多少大小。只是规定,它可以放下boolean,byte,…reference &return address。

    reference 是指一个对象实例的引用。关于reference的大小,目前没有明确的指定大小。但是我们可以理解为它就是类似C++中的指针。

    局部变量表的读取方式是索引,从0开始。所以局部变量表可以简单理解为就是一个表.

    局部变量表的分配顺序如下:

  • this 引用。可以认为是隐式参数。
  • 方法的参数表。
  • 根据局部变量顺序,分配Solt。
  • 一个变量一个solt,64为的占2个solt。java中明确64位的是long & double
  • 为了尽可能的节约局部变量表,Solt可以重用。

    注意:局部变量只给予分配的内存,没有class对象的准备阶段,所以局部变量在使用前,必须先赋值。


操作数栈

     操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到 Code 属性的 max_stacks 数据项中。操作数栈的每一个元素可以是任意的 Java 数据类型,包括 long 和 double。32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值。

    操作数栈在概念上很像寄存器。java虚拟机无法使用寄存器,所以就有操作数栈来存放数据。虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

    比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:

         0: bipush        100 //推入操作栈
         2: istore_1      //将操作栈栈顶出栈并存放到第1个局部变量Slot中
         3: bipush        100
         5: istore_2      
         6: iload_1       //将局部变量表第1个Slot中的值copy到操作栈顶
         7: iload_2       
         8: iadd          //将操作栈中头两个栈顶元素出栈,做整型假发,然后把结果重新入栈。
         9: istore_3 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

     操作数栈 的数据读取、写入就是出栈和如栈操作。


动态链接

    每个栈帧都包含一个指向运行时常量池的引用,持有这个引用是为了支持动态连接。

    符号池的引用,有一部分是在第一次使用或者初始化的时候就确定下来,这个称为静态引用。

    还有一部分是在每次执行的时候采取确定,这个就是动态连接。


方法返回地址

    方法只有2中退出方式,正常情况下,遇到return指令退出。还有就是异常退出。

    正常情况:一般情况下,栈帧会保存 在程序计数器中的调用者的地址。虚拟机通过这个方式,执行方法调用者的地址然后把返回值压入调用者中的操作数栈。

    异常情况:方法不会返回任何值,返回地址有异常表来确定,栈帧一般不存储信息。


方法调用

    方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定调用方法的版本。class文件在编译阶段没有连接这一过程,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是直接地址。

解析

    所有方法调用的目标方法都是常量池中的符号引用。在类的加载解析阶段,会将一部分目标方法转化为直接引用。(可以理解为具体方法的直接地址)可以转化的方法,主要为静态方法 & 私有方法。

    Java虚拟机提供5中方法调用命令:

  • invokestatic:调用静态方法
  • invokespecial:调用构造器,私有方法和父类方法
  • invokevirtual:调用虚方法
  • invokeinterface:调用接口方法
  • invokedynamic:现在运行时动态解析出该方法,然后执行。
  • invokestatic & invokespecial 对应的方法,都是在加载解析后,可以直接确定的。所以这些方法为非虚方法。

    方法静态解析演示:

public class StaticResolution {
    public static void sayHello(){
        System.out.println("hello world!");
    }
    public static void main(String[] args){
        StaticResolution.sayHello();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

    使用 javap 命令查看结果:

rockmbp:Java rocki$ javap -verbose StaticResolution

public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #5                  // Method sayHello:()V
         3: return        
      LineNumberTable:
        line 7: 0
        line 8: 3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
分派

    静态分配:

/**
    重载为例
*/
public class StaticDispatch {
    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,gentleman!");
    }

    public void sayHello(Woman guy)
    {
        System.out.println("hello,lady");
    }

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

        /*
            如果强转了以后,类型也跟着变化了。
            静态分配的典型应用是方法重载。但是方法重载有时候不是唯一的,所以只能选合适的。
            dispatch.sayHello((Man)man);
            dispatch.sayHello((Woman)woman);
        */
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

    运行结果:

hello,guy!
hello,guy!
  • 1
  • 2

    这里的Human我们理解为静态类型,后面的Man是实际类型。我们在编译器只知道静态类型,后面的实际类型等到动态连接的时候才知道。所以对于sayHello方法,虚拟机在重载时,是通过参数的静态类型,而不是实际类型来判断使用那个方法的。

    动态分配

/**
     重写为例
 */
public class DynamicDispatch {
    static abstract class Human{
        protected abstract void sayHello();
    }

    static class Man extends Human{

        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }

    static class Woman extends Human{

        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }

    public static void main(String[] args)
    {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

    运行结果

man say hello
woman say hello 
woman say hello
  • 1
  • 2
  • 3

    先来看上面标红的这句:方法要解析man 的sayhello,问题是man是什么东西,我在解析的时候,是不知道的。所以“man.sayHello();”具体执行的那个类的方法,是需要在虚拟机动态连接的时候才知道,这个就是多态。如果使用javap分析就可以知道这句话,在class文件里面是ynamicDispatch$Human: sayHello. 是的class文件不知道这个sayhello到底要去调哪个方法。

    invokevirtual指令解析的过程大概如下:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
  2. 如果在类型 C 中找到与常量中的描述和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接饮用,查找介绍;如果不通过,则返回 java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError异常。一般情况下,编译工具会帮我们避免这种情况。

    单分派和多分派
    概念上理解比较麻烦,说白了一点就是重载和重写都存在的情况:

public class Dispatch {
    static class QQ{}
    static class _360{}

    public static class Father{
        public void hardChoice(QQ qq){
            System.out.println("Father QQ");
        }

        public void hardChoice(_360 aa){
            System.out.println("Father 360");
        }
    }

    public static class Son extends Father{
        public void hardChoice(QQ qq){
            System.out.println("Son QQ");
        }

        public void hardChoice(_360 aa){
            System.out.println("Son 360");
        }
    }

    public static void action()
    {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

    运行结果:

Father 360
Son QQ
  • 1
  • 2

    结果没有任何悬念,但是过程还是需要明确的。hardChoice的选择是在静态编译的时候就确认的。

    而son.hardchoise 已经确认了函数的类型,只是需要进一步确认实体类型。所以动态连接是单分派。

    动态语言支持:
    jdk 1.7 MehtodHandle:

package cn.kawa;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
/**
    Method Handle 基础用法
 */
public class MethodHandleTest {

    static class ClassA{
        public void println(String s){
            System.out.println("Class A :" + s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();;

        // 无论那个 obj 最终是哪个实现类,下面这句都能正确调用到 println 方法
        getPrintlnMH(obj).invokeExact("icyfenix");
    }


    private static MethodHandle getPrintlnMH(Object reveriver) throws Throwable {
        /*MethodType:代表 “方法类型”,包含了方法的返回值(methodType() 的第一个参数)
         和具体参数(methodType() 第二个参数)
         */
        MethodType mt = MethodType.methodType(void.class, String.class);
        /*
            MethodHandles.lookup这句的作用是在指定类中查找符合给定的方法名、方法类型,并且符合调用权限的句柄
         */
        return MethodHandles.lookup().findVirtual(reveriver.getClass(), "println", mt).bindTo(reveriver);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

基于栈的字节码执行引擎

    基于栈的指令集 和基于寄存器的指令集。

    先看一个加法过程:

    iconst_1
    iconst_1
    iadd
    istore_0
  • 1
  • 2
  • 3
  • 4

    这是基于栈的,也就是上文说的操作数栈。

    先把2个元素要入栈,然后相加,放回栈顶,然后把栈顶的值存在slot 0里面。

    基于寄存器的就不解释了。

    基于寄存器 和基于栈的指令集现在都存在。所以很难说孰优孰劣。

    基于栈的指令集 是和硬件无关的,而基于寄存器则依赖于硬件基础。基于寄存器在效率上优势。

但是虚拟机的出现,就是为了提供跨平台的支持,所以jvm的执行引擎是基于栈的指令集。

public class Demo {
    public static void foo() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 5;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

    直接使用命令javap查看它的字节码指令如下:

public static void foo();
  Code:
     0: iconst_1//把操作数压入操作数栈
     1: istore_0//将操作数栈顶元素弹出保存至局部变量表中
     2: iconst_2
     3: istore_1
     4: iload_0
     5: iload_1
     6: iadd
     7: iconst_5
     8: imul
     9: istore_2
    10: return
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

    执行过程如下图:
这里写图片描述

    注意: 该方法为static方法,所以局部变量表中的第1个元素不是this。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/199133?site
推荐阅读
相关标签
  

闽ICP备14008679号