文章目录

  • 前言
  • 栈帧结构
    • 每个方法调用开始到退出,都对应着一个“栈帧”进站与出站。 运行时栈帧
    • 栈帧-局部变量表
    • 栈帧-操作数栈(Operand Stack)
    • 栈帧-动态连接
    • 栈帧-方法返回地址
  • 方法调用
    • 解析
    • 分派

前言

物理机对指令的执行建立在cpu、硬件、指令集、操作系统层面。而虚拟机对指令的执行可以自行实现,JVM Specification中定义了执行引擎这个概念模型作为JVM的统一Facade。通常会有解释器执行(逐条解释字节码并执行)、编译器执行(即时编译为本地后代码执行)两种执行字节码方式的执行引擎。

栈帧结构

每个方法调用开始到退出,都对应着一个“栈帧”进站与出站。
运行时栈帧

作为虚拟中中方法调用与方法执行的数据结构,它包含方法执行必备的局部变量表、操作数栈、动态链接与方法返回地址等信息
一个方法的执行,可能存在着多层方法调用,对于执行引擎来说,只有当前方法对应的当前栈帧才有效,才是活动的,也就是位于栈顶的栈帧(栈后进先出LIFO)

栈帧-局部变量表

局部变量表是栈帧中的一部分,是一个变量值存储空间,主要存储方法中的局部变量、方法参数
在class文件编译生成时,(Code属性)就决定了局部变量表的内容、以及最大容量:

 public class TestClass {public static void main(String[] args) {int m = 0;inc(m);}public static int inc(int m) {int f = 2;return incNext(m + f);}public static int incNext(int m) {int g = 2;return m + g;}
}
javap -c .\TestClass.class
Compiled from "TestClass.java"
public class com.zs.jvm.byteCode.TestClass {public com.zs.jvm.byteCode.TestClass();Code:0: aload_0// Method java/lang/Object."<init>":()V1: invokespecial #1                  4: returnpublic static void main(java.lang.String[]);Code:0: iconst_01: istore_12: iload_1// Method inc:(I)I3: invokestatic  #2                  6: pop7: returnpublic static int inc(int);Code:0: iconst_21: istore_12: iload_03: iload_14: iadd// Method incNext:(I)I5: invokestatic  #3                  8: ireturnpublic static int incNext(int);Code:0: iconst_21: istore_12: iload_03: iload_14: iadd5: ireturn
}

局部变量表中以“变量槽”(variable slot)作为存储数据的最小单位,并且每个slot都应该可以存储一个 byte、int、boolean、char、 short、 int、 float、 reference或returnAddress类型的数据(也就是除了double、long的基本变量类型数据和引用类型数据)。

JVMhotSpot中,通常每个slot是32bit,对于double、long,64bit的数据,jvm会把这两种数据分配在两个连续slot中,每次读(写)会分配为两次读(写)单个slot完成,而且不允许读写操作只执行其中一个slot。另外,因为局部变量表示线程私有的,所以这里对64位数据读写不会有线程安全问题。

JVM对reference类型的数据没有过多说明,但是一个Reference数据应该保证:
1.通过这个Reference可以间接或者直接找到对应对象在heap中存放的起始地址。
2.通过这个Reference可以间接或直接的找到这个对象在MethodArea区中存储的类型信息。

局部变量表使用索引定位的方式来读取slot,索引范围是从0到slot的最大个数,比如读(取)一个索引为n的数据,则就是读(取)第n个slot数据。而如果这个n索引的数据是个64bit的数据,那么读(取),就是要同时连续读(取)第n个与第n+1个slot。

对于对象实例类型的方法调用,通常局部变量表中第0位索引存放的是这个对象实例的引用,也就是this。

returnAddress类型目前已经很少见了,可以忽略,不详述。

栈帧-操作数栈(Operand Stack)

jvm的解释执行引擎是基于栈的执行引擎,这句话中的栈其实就是指Operand Stack。

是一个后进先出LIFO的数据模型,字节码指令执行时会不断的向操作数栈中插入数据、提取数据,称为出栈入栈。

栈帧-动态连接

每个栈帧都包含了一个常量池中的符号引用,这个符号引用指向这个栈帧所属的方法,而字节码方法调用会(如,invokespecial、invokestatic等方法调用指令,后文详细介绍)以这个符号引用作为参数。

这一类型的符号引用一部分会在类加载(Linking—resolve,参考深入理解JVM一加载机制)时或者第一次使用之后转换为内存中的直接引用,这个种转换成为 静态解析;

另一部分会在每一次运行期间(方法调用时)转换为直接引用,这部分称为动态连接。

栈帧-方法返回地址

就是决定当前方法调用退出后,应该返回的位置。
通常会有两种方式结束一个方法调用:正常退出、异常退出。

正常退出时,调用者的PC计数器的值可以作为返回地址,通常返回地址保持在栈帧中。
异常退出时,返回地址是通过异常处理器来确定的,一般不会保存在栈帧中。

一个方法的退出会恢复这个方法调用者的局部变量表、操作数栈,把返回值(如果有的话)压入调用者的操作数栈,让PC计数器执行下一条指令。

方法调用

方法调用是在运行时确定调用哪个方法的操作,是一个很频繁的操作。

因为class在编译后操作指令都是一堆常量池中符号引用,并没有直接指向内存地址入口(直接引用)。这给java带了了强大的动态扩展空间,但是也带来了复杂度,通常是在类加载(Linking—resolve,参考深入理解JVM一加载机制)、甚至运行时才确定具体执行的是哪一个方法。

解析

字节码执行,其实主要是执行方法中指令,如果在编译期就可以确定要执行的具体方法,那么对这类方法的调用的确认成为解析。

java代码编译后的字节码中,方法调用的代码都是一堆常量池中的符号引用,需要通过解析将符号引用转换为(包含内存的入口的)直接引用。

在方法调用时有如下几种指令:

//(私有方法、实例构造器、父类方法的调用)
invokespecial
//(静态方法调用)
invokestatic
//(虚方法调用\final修饰的方法调用)
invokevirtual
//(调用接口方法,会在运行时确认一个接口的实现;调用重载方法等产生多态选择的情况)
invokeinterface
//(、、、、、、、)
invokedynamic

示例:

public class TestGCChild implements GCTest {static String TEST_NAME = "Test";static GCTest t = new TestGCChild();public static void main(String[] args) throws Exception {TestGCChild.easyStatic();// static mehotdt.easy();// interface implementsnew TestGCChild().easyUnStatic();// normal methodnew TestGCChild().easyFinal();// normal method}@Overridepublic String easy() {return "easy";}public static String easyStatic() {return "easy";}public String easyUnStatic() {return "easy";}public final String easyFinal() {return "easy";}
}
Compiled from "TestGCChild.java"
public class com.zs.test.TestGCChild implements com.zs.test.GCTest {static java.lang.String TEST_NAME;static com.zs.test.GCTest t;static {};Code:0: ldc           #14                 // String Test2: putstatic     #16                 // Field TEST_NAME:Ljava/lang/String;5: new           #1                  // class com/zs/test/TestGCChild8: dup9: invokespecial #18                 // Method "<init>":()V12: putstatic     #21                 // Field t:Lcom/zs/test/GCTest;15: returnpublic com.zs.test.TestGCChild();Code:0: aload_01: invokespecial #25                 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]) throws java.lang.Exception;Code:0: invokestatic  #33                 // Method easyStatic:()Ljava/lang/String;3: pop4: getstatic     #21                 // Field t:Lcom/zs/test/GCTest;7: invokeinterface #37,  1           // InterfaceMethod com/zs/test/GCTest.easy:()Ljava/lang/String;12: pop13: new           #1                  // class com/zs/test/TestGCChild16: dup17: invokespecial #18                 // Method "<init>":()V20: invokevirtual #40                 // Method easyUnStatic:()Ljava/lang/String;23: pop24: new           #1                  // class com/zs/test/TestGCChild27: dup28: invokespecial #18                 // Method "<init>":()V31: invokevirtual #43                 // Method easyFinal:()Ljava/lang/String;34: pop35: returnpublic java.lang.String easy();Code:0: ldc           #48                 // String easy2: areturnpublic static java.lang.String easyStatic();Code:0: ldc           #48                 // String easy2: areturnpublic java.lang.String easyUnStatic();Code:0: ldc           #48                 // String easy2: areturnpublic final java.lang.String easyFinal();Code:0: ldc           #48                 // String easy2: areturn
}

对于私有方法、实例构造器、父类方法、静态方法的调用,符合编译期可知,运行时不变的要求。(因为私有方法不会被覆盖或者改写,静态方法只属于当前类也不会被改写。)除此之外,被final修饰的方法,因为它无法被重写,所以也是确定的,它虽然使用了invokevirtual调用,但也是在编译期间即可确定的。这类在编译时即可确认实际调用实现的方法称为非虚方法,使用invokespeical、invokestatic指令、以及使用了final修饰的方法都属于非虚方法。除此之外都是虚方法,需要进行多态选择,后期绑定实际的方法。

分派

静态分派:
我们先看一个小程序,请输出下列代码的执行结果:

class Human {}class Man extends Human {}class Woman extends Human {}public class TestDispatch {public void test(Human h) {System.out.println("human");}public void test(Woman w) {System.out.println("Woman");}public void test(Man m) {System.out.println("Man");}public static void main(String[] args) {Human human = new Human();Human woman = new Woman();Human man = new Man();TestDispatch test = new TestDispatch();test.test(man);test.test(woman);test.test(human);}
}

//outpu:
human
human
human

如上边的示例,Human m(等号左边)称为静态类型(Static Type)或者称为显示类型(Apparent Type),等号右边部分称为实际类型(Actual Type),是真正初始化的对象。
静态类型都是在编译期决定并不可改变的,而实际类型只能到运行时才能真正决定,在编译期(编译后的字节码)无法确认实际类型。

因为重载,众多重载方法中具体执行哪一个方法是在编译期确定的(编译器会自动选择最合适的一个),所以产生了上边的代码执行结果。

接着,我们引出静态分派(static dispatch)的概念:所有依赖静态类型来决定具体方法执行的分派动作(分派可理解为对多态的选择)称为静态分派。

动态分派:

动态分配最典型的例子就是“重写”(override).
看一下这个例子:

class Human {String say() {System.out.println("human");return "human";}
}class Son extends Human {public String say() {System.out.println("Son");return "Son";}}class Father extends Human {public String say() {System.out.println("Father");return "Father";}
}public class TestGC {static Human human = new Human();static Human father = new Father();static Human son = new Son();public static void main(String[] args) throws Exception {human.say();// humanfather.say();// Fatherson.say();// Son}
}
//output:
//human
//Father
//Son

可以看出来,运行的say()其实是具体实现类型的方法,并不是Human.say(),很明显无法根据静态编译来确定实际执行的方法。我们再看看这段代码的字节码,特别看一下main()中的指令:

public class com.zs.test.TestGC {static com.zs.test.Human human;static com.zs.test.Human father;static com.zs.test.Human son;static {};Code:0: new           #12                 // class com/zs/test/Human3: dup4: invokespecial #14                 // Method com/zs/test/Human."<init>":()V7: putstatic     #17                 // Field human:Lcom/zs/test/Human;10: new           #19                 // class com/zs/test/Father13: dup14: invokespecial #21                 // Method com/zs/test/Father."<init>":()V17: putstatic     #22                 // Field father:Lcom/zs/test/Human;20: new           #24                 // class com/zs/test/Son23: dup24: invokespecial #26                 // Method com/zs/test/Son."<init>":()V27: putstatic     #27                 // Field son:Lcom/zs/test/Human;30: returnpublic com.zs.test.TestGC();Code:0: aload_01: invokespecial #31                 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]) throws java.lang.Exception;Code:0: getstatic     #17                 // Field human:Lcom/zs/test/Human;3: invokevirtual #39                 // Method com/zs/test/Human.say:()Ljava/lang/String;6: pop7: getstatic     #22                 // Field father:Lcom/zs/test/Human;10: invokevirtual #39                 // Method com/zs/test/Human.say:()Ljava/lang/String;13: pop14: getstatic     #27                 // Field son:Lcom/zs/test/Human;17: invokevirtual #39                 // Method com/zs/test/Human.say:()Ljava/lang/String;20: pop21: return
}

我们看到,从静态编译的字节码中无法判定方法调用者的实际类型,那么jvm是如何知道具体的实现者是哪个呢?是如何进行动态选择的呢?关键就在invokevirtual指令:

invokevirtual指令动态查找的过程如下:

1.在操作栈的栈顶弹出首元素,将首元素所指向对象的实际类型记作C类型。
2.在C类型中按方法描述符等常量查找匹配的方法,如果找到了,再进行访问权限的校验,如果校验通过允许访问,那么就直接返回这个方法的直接引用。如果访问权限校验不允许访问,抛出IllegalAccessError异常。
3.如果在C类型中找不到匹配的方法,那么就从它的直接父类开始从下到上查找,找到后再进行访问权限校验,通过后返回。
4.如果始终找不到匹配方法(在C类型、以及C的父类),那么久抛出AbstractMethodError异常。

这样看来,如果子类中没有override父类中的方法,那么调用会直接执行父类方法。如下:

class Human {String say() {System.out.println("human");return "human";}
}class Son extends Human {//    public String say() {
//        System.out.println("Son");
//        return "Son";
//    }}class Father extends Human {// public String say() {// System.out.println("Father");// return "Father";// }
}public class TestGC {static Human human = new Human();static Human father = new Father();static Human son = new Son();public static void main(String[] args) throws Exception {human.say();// humanfather.say();// humanson.say();// human}
}//output:
// human
// human
// human

动态分配:在运行期间,根据对象的实际类型确认具体要执行方法的分配。

待续…

深入理解JVM一字节码执行相关推荐

  1. 第六章JVM虚拟机字节码执行引擎——类文件和类加载之前必看

    文章目录 虚拟机字节码执行引擎 运行时栈帧结构 局部变量表(Local Variables) 操作数栈 动态链接(Dynamic Linking) 方法返回地址 附加信息 方法调用 解析 分派 虚方法 ...

  2. 深入理解JVM 一字节码详解

    今天继续总结JVM,计划本周完成这个系列的整理.总结. 本节内容枯燥,胆小者勿入! Write Once,Run Anywhere--byteCode byteCode 平台无关 java通过存储编译 ...

  3. 深入理解JVM虚拟机(七):虚拟机字节码执行引擎

    代码编译的结果就是从本地机器码转变为字节码.我们都知道,编译器将Java源代码转换成字节码?那么字节码是如何被执行的呢?这就涉及到了JVM字节码执行引擎,执行引擎负责具体的代码调用及执行过程.就目前而 ...

  4. jvm(8)-虚拟机字节码执行引擎

    [0]README 0.1)本文转自 "深入理解jvm",旨在学习 虚拟机字节码执行引擎 的基础知识: [1]概述 1)物理机和虚拟机的执行引擎: 物理机的执行引擎是直接建立在处理 ...

  5. 深入理解java虚拟机(5)---字节码执行引擎

    字节码是什么东西? 以下是百度的解释: 字节码(Byte-code)是一种包含执行程序.由一序列 op 代码/数据对组成的二进制文件.字节码是一种中间码,它比机器码更抽象. 它经常被看作是包含一个执行 ...

  6. JAVA类加载对字节码的处理_深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)...

    [本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 周志明的< ...

  7. 《深入理解Java虚拟机》笔记5——类加载机制与字节码执行引擎

    第七章 虚拟机类加载机制 7.1 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 在J ...

  8. JVM实战与原理---字节码执行引擎

    JVM实战与原理 目录 字节码执行引擎 1. 方法区 2. 栈帧 2.1 局部变量表 2.2 操作数栈 2.3 动态连接 2.4 方法返回地址 字节码执行引擎 章节目的:虚拟机是如何找到正确的方法,如 ...

  9. JVM类加载机制_字节码执行引擎_Java内存模型

    类加载机制: 类加载生命期:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Usi ...

最新文章

  1. 【Android 插件化】现有的针对插件化恶意应用的解决方案 | 插件化应用开发推荐方案
  2. debian 7上安装svn
  3. 在el-table中使用el-popover,没法点击确定或取消来关闭
  4. 2020-11-29(准备考试)
  5. Android NDK开发一:配置环境
  6. hdu 5444 Elven Postman(根据先序遍历和中序遍历求后序遍历)2015 ACM/ICPC Asia Regional Changchun Online...
  7. RHEL7 USB installation problem and solving
  8. linux的驱动开发——下载地址
  9. php判断ipv6是否在范围内,[PHP] IPv6檢查IP是否在某個網段內 mtachcidr6
  10. 线程之间的通信(thread signal)
  11. H - 数据结构实验之链表九:双向链表
  12. 关于String类的split方法
  13. 天线基础知识(三)天线增益
  14. 插入视频短代码WordPress函数wp_video_shortcode
  15. Pandas——数据清洗1
  16. oracle自动加一天,如何将Oracle 当前日期加一天、一分钟
  17. git patch 使用
  18. 三菱GX works2的应用安装
  19. ps基础学习:邮票效果制作
  20. Ubuntu 16.04 LTS将移除私有的AMD催化剂驱动

热门文章

  1. 解剖华为--再读《华为真相》
  2. 北理工汇编语言与接口技术结课实验,包括大数乘法,计算器,贪吃蛇三个项目
  3. Android-支付宝支付
  4. 要跑得快,还需跑的稳
  5. 资深架构师谈 DDD 兴起,解决难题与实现步骤
  6. p80 红蓝对抗-AWD 模式准备攻防监控批量
  7. https htttp 区别
  8. matlab基因频率是看最大值吗,基于ICA的语音信号盲分离.doc
  9. 如何高效、精准地进行图片搜索?看看轻量化视觉预训练模型
  10. win7最大内存设置问题,导致系统无法启动的解决方法