深入理解JVM一字节码执行
文章目录
- 前言
- 栈帧结构
- 每个方法调用开始到退出,都对应着一个“栈帧”进站与出站。 运行时栈帧
- 栈帧-局部变量表
- 栈帧-操作数栈(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一字节码执行相关推荐
- 第六章JVM虚拟机字节码执行引擎——类文件和类加载之前必看
文章目录 虚拟机字节码执行引擎 运行时栈帧结构 局部变量表(Local Variables) 操作数栈 动态链接(Dynamic Linking) 方法返回地址 附加信息 方法调用 解析 分派 虚方法 ...
- 深入理解JVM 一字节码详解
今天继续总结JVM,计划本周完成这个系列的整理.总结. 本节内容枯燥,胆小者勿入! Write Once,Run Anywhere--byteCode byteCode 平台无关 java通过存储编译 ...
- 深入理解JVM虚拟机(七):虚拟机字节码执行引擎
代码编译的结果就是从本地机器码转变为字节码.我们都知道,编译器将Java源代码转换成字节码?那么字节码是如何被执行的呢?这就涉及到了JVM字节码执行引擎,执行引擎负责具体的代码调用及执行过程.就目前而 ...
- jvm(8)-虚拟机字节码执行引擎
[0]README 0.1)本文转自 "深入理解jvm",旨在学习 虚拟机字节码执行引擎 的基础知识: [1]概述 1)物理机和虚拟机的执行引擎: 物理机的执行引擎是直接建立在处理 ...
- 深入理解java虚拟机(5)---字节码执行引擎
字节码是什么东西? 以下是百度的解释: 字节码(Byte-code)是一种包含执行程序.由一序列 op 代码/数据对组成的二进制文件.字节码是一种中间码,它比机器码更抽象. 它经常被看作是包含一个执行 ...
- JAVA类加载对字节码的处理_深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)...
[本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 周志明的< ...
- 《深入理解Java虚拟机》笔记5——类加载机制与字节码执行引擎
第七章 虚拟机类加载机制 7.1 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 在J ...
- JVM实战与原理---字节码执行引擎
JVM实战与原理 目录 字节码执行引擎 1. 方法区 2. 栈帧 2.1 局部变量表 2.2 操作数栈 2.3 动态连接 2.4 方法返回地址 字节码执行引擎 章节目的:虚拟机是如何找到正确的方法,如 ...
- JVM类加载机制_字节码执行引擎_Java内存模型
类加载机制: 类加载生命期:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Usi ...
最新文章
- 【Android 插件化】现有的针对插件化恶意应用的解决方案 | 插件化应用开发推荐方案
- debian 7上安装svn
- 在el-table中使用el-popover,没法点击确定或取消来关闭
- 2020-11-29(准备考试)
- Android NDK开发一:配置环境
- hdu 5444 Elven Postman(根据先序遍历和中序遍历求后序遍历)2015 ACM/ICPC Asia Regional Changchun Online...
- RHEL7 USB installation problem and solving
- linux的驱动开发——下载地址
- php判断ipv6是否在范围内,[PHP] IPv6檢查IP是否在某個網段內 mtachcidr6
- 线程之间的通信(thread signal)
- H - 数据结构实验之链表九:双向链表
- 关于String类的split方法
- 天线基础知识(三)天线增益
- 插入视频短代码WordPress函数wp_video_shortcode
- Pandas——数据清洗1
- oracle自动加一天,如何将Oracle 当前日期加一天、一分钟
- git patch 使用
- 三菱GX works2的应用安装
- ps基础学习:邮票效果制作
- Ubuntu 16.04 LTS将移除私有的AMD催化剂驱动