JVM 从入门到精通(六)JVM运行时数据区——虚拟机栈2

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: JVM 从入门到精通(六)JVM运行时数据区——虚拟机栈2

四、操作数栈(Operand Stack)


1.栈 :可以使用数组或者链表来实现


2.每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以成为表达式栈


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


某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。(如字节码指令bipush操作)



比如:执行复制、交换、求和等操作


代码举例



4.1 操作数栈特点


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


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


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


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


32bit的类型占用一个栈单位深度


64bit的类型占用两个栈深度单位


操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈push和出栈pop操作来完成一次数据访问


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


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


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


五、代码追踪


结合上图结合下面的图来看一下一个方法(栈帧)的执行过程


①15入栈;②存储15,15进入局部变量表


注意:局部变量表的0号位被构造器占用,这里的15从局部变量表1号开始



③压入8;④8出栈,存储8进入局部变量表;



⑤从局部变量表中把索引为1和2的是数据取出来,放到操作数栈;⑥iadd相加操作



⑦iadd操作结果23出栈⑧将23存储在局部变量表索引为3的位置上istore_3



六、栈顶缓存技术(Top Of Stack Cashing)


基于栈式架构的虚拟机所使用的零地址指令(即不考虑地址,单纯入栈出栈)更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数


由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率


七、动态链接(Dynamic Linking)



1.每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用


2.包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令


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


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



代码示例

public class DynamicLinkingTest {
    int num = 10;
    public void methodA(){
        System.out.println("methodA()....");
    }
    public void methodB(){
        System.out.println("methodB()....");
        methodA();
        num++;
    }
}


在字节码指令中,methodB() 方法中通过 invokevirtual #7 指令调用了方法 A


那么 #7 是个啥呢?

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;


往上面翻,找到常量池的定义:#7 = Methodref #8.#31


先找 #8 :


#8 = Class #32 :去找 #32

#32 = Utf8 com/atguigu/java1/DynamicLinkingTest

结论:通过 #8 我们找到了 DynamicLinkingTest 这个类


再来找 #31:

#31 = NameAndType #19:#13 :去找 #19 和 #13

#19 = Utf8 methodA :方法名为 methodA

#13 = Utf8 ()V :方法没有形参,返回值为 void


结论:通过 #7 我们就能找到需要调用的 methodA() 方法,并进行调用

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


在上面,其实还有很多符号引用,比如 Object、System、PrintStream 等等


为什么要用常量池呢?


1.因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,然后记录其引用即可,节省了空间


2.常量池的作用:就是为了提供一些符号和常量,便于指令的识别


八、方法的调用:解析和分派


8.1 静态链接与动态链接


静态链接机制与动态链接机制


在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关


静态链接:


当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期确定,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接


动态链接:


如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。


8.2 早期绑定与晚期绑定


方法的绑定机制


静态链接和动态链接对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。


早期绑定


早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。


晚期绑定


如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。


代码示例


代码

/**
 * 说明早期绑定和晚期绑定的例子
 *
 * @author shkstart
 * @create 2020 上午 11:59
 */
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 {
    public Cat() {
        super();//表现为:早期绑定
    }
    public Cat(String name) {
        this();//表现为:早期绑定
    }
    @Override
    public void eat() {
        super.eat();//表现为:早期绑定
        System.out.println("猫吃鱼");
    }
    @Override
    public void hunt() {
        System.out.println("捕食耗子,天经地义");
    }
}
public class AnimalTest {
    public void showAnimal(Animal animal) {
        animal.eat();//表现为:晚期绑定
    }
    public void showHunt(Huntable h) {
        h.hunt();//表现为:晚期绑定
    }
}


invokevirtual 体现为晚期绑定



invokeinterface 也体现为晚期绑定



invokespecial 体现为早期绑定



8.3 多态性与方法绑定


多态性与方法绑定机制


1.随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。


2.Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。


虚方法与非虚方法


虚方法与非虚方法的区别


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


2.静态方法、私有方法、fina1方法、实例构造器、父类方法都是非虚方法。


3.其他方法称为虚方法。


子类对象的多态的使用前提:


1.类的继承关系


2.方法的重写


虚拟机中调用方法的指令


四条普通指令:


1.invokestatic:调用静态方法,解析阶段确定唯一方法版本


2.invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本


3.invokevirtual:调用所有虚方法


4.invokeinterface:调用接口方法


一条动态调用指令:


invokedynamic:动态解析出需要调用的方法,然后执行


区别:


1.前四条指令固化在虚拟机内部,方法的调用执行不可人为干预


2.而invokedynamic指令则支持由用户确定方法版本


3.其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(fina1修饰的除外)称为虚方法。


代码示例:


代码

/**
 * 解析调用中非虚方法、虚方法的测试
 *
 * invokestatic指令和invokespecial指令调用的方法称为非虚方法
 * @author shkstart
 * @create 2020 下午 12:07
 */
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!");
        //invokevirtual
        //虽然字节码指令中显示为invokevirtual,但因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
        showFinal();
        //invokespecial
        super.showCommon();
        //invokevirtual
        //有可能子类会重写父类的showCommon()方法
        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();
}


Son 类中 show() 方法的字节码指令如下



关于 invokedynamic 指令


JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进。


但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。


Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在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;
        });
    }
}


字节码指令



目录
相关文章
|
1月前
|
Java
jvm复习,深入理解java虚拟机一:运行时数据区域
这篇文章深入探讨了Java虚拟机的运行时数据区域,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、元空间和运行时常量池,并讨论了它们的作用、特点以及与垃圾回收的关系。
64 19
jvm复习,深入理解java虚拟机一:运行时数据区域
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
69 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
30天前
|
存储 Dart Java
Dart 虚拟机运行原理
【10月更文挑战第20天】Dart 虚拟机通过一系列复杂的机制和操作,确保 Dart 代码能够准确、高效地执行。它为 Dart 语言的广泛应用提供了坚实的基础和可靠的运行环境
25 6
|
30天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
43 1
|
17天前
|
存储 SQL 数据库
虚拟化数据恢复—Vmware虚拟机误还原快照的数据恢复案例
虚拟化数据恢复环境: 一台虚拟机从物理机迁移到ESXI虚拟化平台,迁移完成后做了一个快照。虚拟机上运行了一个SQL Server数据库,记录了数年的数据。 ESXI虚拟化平台上有数十台虚拟机,EXSI虚拟化平台连接了一台EVA存储,所有的虚拟机都存放在EVA存储上。 虚拟化故障: 工组人员误操作将数年前迁移完成后做的快照还原了,也就意味着虚拟机状态还原到数年前,近几年数据都被删除了。 还原快照相当于删除数据,意味着部分存储空间会被释放。为了不让这部分释放的空间被重用,需要将连接到这台存储的所有虚拟机都关掉,需要将不能长时间宕机的虚拟机迁移到别的EXSI虚拟化平台上。
94 50
|
1月前
|
安全 虚拟化 数据中心
Xshell 连接 VMware虚拟机操作 截图和使用
Xshell 连接 VMware虚拟机操作 截图和使用
54 4
|
1月前
|
Linux 虚拟化
vmware虚拟机安装2024(超详细)
vmware虚拟机安装2024(超详细)
306 6
|
1月前
|
虚拟化 网络虚拟化 网络架构
虚拟机 VMware Workstation 16 PRO 的网络配置
虚拟机 VMware Workstation 16 PRO 的网络配置