JVM虚拟机(二):字节码执行引擎
运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接、和方法返回地址等信息。
局部变量表
局部变量表的容量以变量槽为最小单位。每个变量槽应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress(可忽略,现在已经很少见了)。reference类型表示对一个对象实例的引用,即根据引用直接或间接的查到对象在java堆中的数据存放的起始地址、索引或对象所属数据类型在方法区中的存储的类型信息。上述类型均占用一个变量槽。long和double占用两个连续的变量槽。
示例1
实例方法(没有被static修饰的方法)局部变量表第0位是this。
public void soltTest() {
    byte i = 15;
}

public void soltTest() {
    long i = 15;
}

为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重复使用的,方法体中定义的变量作用域没有全部覆盖整个方法,此变量占用的变量槽是可以被重复利用的。
注意:示例需设置虚拟机参数“-verbose:gc”
示例2
public static void main(String[] args) {
    byte[] bytes = new byte[64 * 1024 * 1024];
    System.gc();
}
控制台输出:
[GC (System.gc())  72123K->66690K(251392K), 0.0177919 secs]
[Full GC (System.gc())  66690K->66523K(251392K), 0.0042184 secs]
示例3
public static void main(String[] args) {
    {
        byte[] bytes = new byte[64 * 1024 * 1024];
    }
    System.gc();
}
控制台输出:
[GC (System.gc())  72123K->66674K(251392K), 0.0007715 secs]
[Full GC (System.gc())  66674K->66523K(251392K), 0.0041207 secs]
示例4
public static void main(String[] args) {
    {
        byte[] bytes = new byte[64 * 1024 * 1024];
    }
    int a = 0;
    System.gc();
}
控制台输出:
[GC (System.gc())  72123K->66690K(251392K), 0.0009232 secs]
[Full GC (System.gc())  66690K->987K(251392K), 0.0042235 secs]
结论:变量槽在没有复用时,不GC
操作数栈
操作数栈是后进先出栈。个人感觉操作数栈是局部变量表与方法区中间的数据中转站。
方法调用
方法调用不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定调用哪个方法,暂时还未涉及方法内部的具体运行过程 。
解析
调用方法在程序代码写好、编译器进行编译那一刻就确定下来了,这类方法的调用被称为解析。在Java中符合“编译期可知,运行期不可变”要求的方法主要有静态方法和私有方法两大类。
调用不同类型的方法,字节码指令集里面设计了不同的指令。分别是:
- invokestatic:用于调用静态方法。
- invokespecial:用于调用实例构造器()方法,私有方法和父类中的方法。
- invokevirtual:用于调用所有的虚方法。
- invokeinterface:用于调用接口方法,在运行时再确定一个实现该接口的对象。
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。
静态方法、私有方法、实例构造器、父类方法及final修饰的方法会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为”非虚方法“。
方法静态解析演示
/**
 * @author Wang Chinda
 * @date 2020/3/31
 * @see
 * @since 1.0
 */
public class StaticResolution {
    public static void sayHello() {
        System.out.println("Hello world");
    }
    public static void main(String[] args) {
        StaticResolution.sayHello();
    }
}
指令:
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <Hello world>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return

分派
静态分派
所有依赖静态类型来决定调用哪个方法的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载。
方法静态分派演示
/**
 * 控制台打印
 * hello, guy!
 * hello, guy!
 * @author Wang Chinda
 * @date 2020/3/31
 * @see
 * @since 1.0
 */
public class StaticDispatch {
    static abstract class Human {
    }
    static class Man extends Human {
    }
    static class Woman extends Human {
    }
    public void sayHello(Human human) {
        System.out.println("hello, guy!");
    }
    public void sayHello(Man man) {
        System.out.println("hello, man");
    }
    public void sayHello(Woman woman) {
        System.out.println("Hello, women");
    }
    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sd = new StaticDispatch();
        sd.sayHello(man);
        sd.sayHello(woman);
    }
}
上面代码中的“Human”称为变量的“静态类型”,而后面的“Man”称为变量的”实际类型“。静态类型和实际类型再程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型时在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
// 实际类型变化
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
// 静态类型变化
sd.sayHello((Man) man); // 控制台打印 hello, man
sd.sayHello((Woman) woman); // 控制台打印 Hello, women
“静态类型”在代码被编译器编译之后,就已经确定类型引用,但是实际类型只有在程序运行时,才可以确定具体的引用类型。即,调用哪个方法以句柄所属类型匹配方法所携带的形参。
注意:编译器虽然能确定方法的重载版本,但很多情况下这个重载版本并不是唯一的,程序往往只能确定一个“相对更合适的”方法调用。
重载方法匹配优先级
/**
 * @author Wang Chinda
 * @date 2020/4/2
 * @see
 * @since 1.0
 */
public class OverLoad {
    public static void sayHello(Object arg) {
        System.out.println("hello Object!");
    }
    public static void sayHello(int arg) {
        System.out.println("Hello int!");
    }
    public static void sayHello(long arg) {
        System.out.println("hello long");
    }
    public static void sayHello(Character character) {
        System.out.println("hello character");
    }
    public static void sayHello(char arg) {
        System.out.println("hello char");
    }
    public static void sayHello(char... arg) {
        System.out.println("hello char...");
    }
    public static void sayHello(Serializable arg) {
        System.out.println("Hello serializable");
    }
    public static void main(String[] args) {
        sayHello('a');
    }
}
上面代码控制台打印:
hello char
'a'是char类型数据,最优匹配的当然是char类型形参方法调用。注释掉sayHello(char arg)方法,控制台打印:
Hello int!
这时发生了一次自动类型转换,'a'除了可以代表一个字符,还可以代表数字97(字符'a'的Unicode数值为十进制数字97),因此参数类型为int的重载方法最合适。我们继续注释掉sayHello(int arg)方法,控制台打印:
hello long
这时发生了两次自动类型转换,'a'转换为int的97之后,进一步转型为long的97L,此时参数类型为long的重载方法最合适。不过自动转型还会多次发生,按照char>int>long>float>double的顺序自动转型。我们继续注释掉sayHello(long arg)方法,控制台打印:
hello character
这时发生了一次自动装箱,'a'被包装为它的封装类型java.lang.Character,此时参数类型为Character的重载方法最合适。我们继续注释掉sayHello(Character character)方法,控制台打印:
Hello serializable
之所以输出Hello serializable是因为
java.lang.Character implements java.io.Serializable, Comparable<Character>
此时若是同时存在sayHello(Comparable arg)方法, 编译会抛出模糊的方法调用错误,并拒绝编译。
Ambiguous method call. Both sayHello (Serializable) in OverLoad and sayHello (Comparable) in OverLoad match
我们继续注释掉sayHello(Serializable arg)方法,控制台打印:
hello Object!
这时是char装箱后转型为父类,如果有多层级父类,越接近的优先级越高。我们继续注释掉sayHello(Object arg)方法,控制台打印:
hello char...
可见边长参数的重载优先级是最低的。
动态分派
在运行期间根据实际类型确定调用哪个目标方法的分派过程称为动态分派。
方法动态分派演示
/**
 * 控制台打印:
 * man say hello
 * woman say hello
 * woman say hello
 *
 * @author Wang Chinda
 * @date 2020/4/2
 * @see
 * @since 1.0
 */
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();
    }
}
指令展示
 0 new #2 <com/chinda/invoke/DynamicDispatch$Man>
 3 dup
 4 invokespecial #3 <com/chinda/invoke/DynamicDispatch$Man.<init>>
 7 astore_1
 8 new #4 <com/chinda/invoke/DynamicDispatch$Woman>
11 dup
12 invokespecial #5 <com/chinda/invoke/DynamicDispatch$Woman.<init>>
15 astore_2
16 aload_1
17 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
20 aload_2
21 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
24 new #4 <com/chinda/invoke/DynamicDispatch$Woman>
27 dup
28 invokespecial #5 <com/chinda/invoke/DynamicDispatch$Woman.<init>>
31 astore_1
32 aload_1
33 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
36 return
invokevirtual指令的运行解析过程:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符与简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验证过程。
- 如果之中没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,只会对方法有效,对字段是无效的,因为字段不使用这条指令。
字段没有多态性演示
/**
 * 控制台打印
 * I am Son, I have $0
 * I am Son, I have $4
 * This gay has $2
 *
 * @author Wang Chinda
 * @date 2020/4/2
 * @see
 * @since 1.0
 */
public class FieldHasNoPolymorphic {
    static class Father {
        public int money = 1;
        public Father() {
            money = 2;
            showMeTheMoney();
        }
        public void showMeTheMoney() {
            System.out.println("I am Father, I have $" + money);
        }
    }
    static class Son extends Father {
        public int money = 3;
        public Son() {
            money = 4;
            showMeTheMoney();
        }
        @Override
        public void showMeTheMoney() {
            System.out.println("I am Son, I have $" + money);
        }
    }
    public static void main(String[] args) {
        Father gay = new Son();
        System.out.println("This gay has $" + gay.money);
    }
}
字类初始化时,首先触发父类初始化,在父类初始化时,调用showMeTheMoney()虚方法,实际执行的是Son::showMeTheMoney()方法,此时子类还没有初始化,所以money值为0。初始化完父类初始化子类,此时money为4。执行打印时,调用的是父类中的属性,所以值为2。
单分派与多分派
方法的接收者和方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
单分派和多分派演示
/**
 * 控制台打印
 * father choose 360
 * son choose qq
 * @author Wang Chinda
 * @date 2020/4/2
 * @see
 * @since 1.0
 */
public class Dispatch {
    static class QQ{}
    static class _360{}
    public static class Father {
        public void hardChoice(QQ arg) {
            System.out.println("father choose qq");
        }
        public void hardChoice(_360 arg) {
            System.out.println("father choose 360");
        }
    }
    public static class Son extends Father {
        @Override
        public void hardChoice(QQ arg) {
            System.out.println("son choose qq");
        }
        @Override
        public void hardChoice(_360 arg) {
            System.out.println("son choose 360");
        }
    }
    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}
指令演示
 0 new #2 <com/chinda/invoke/Dispatch$Father>
 3 dup
 4 invokespecial #3 <com/chinda/invoke/Dispatch$Father.<init>>
 7 astore_1
 8 new #4 <com/chinda/invoke/Dispatch$Son>
11 dup
12 invokespecial #5 <com/chinda/invoke/Dispatch$Son.<init>>
15 astore_2
16 aload_1
17 new #6 <com/chinda/invoke/Dispatch$_360>
20 dup
21 invokespecial #7 <com/chinda/invoke/Dispatch$_360.<init>>
24 invokevirtual #8 <com/chinda/invoke/Dispatch$Father.hardChoice>
27 aload_2
28 new #9 <com/chinda/invoke/Dispatch$QQ>
31 dup
32 invokespecial #10 <com/chinda/invoke/Dispatch$QQ.<init>>
35 invokevirtual #11 <com/chinda/invoke/Dispatch$Father.hardChoice>
38 return
注意:invokevirtual #11 <com/chinda/invoke/Dispatch$Father.hardChoice> 静态分派指向的是Father::hardChoice()方法,但动态分派时,将方法指向到实际类型中的目标方法,即Son::hardChoice()。
基于栈的解释器执行过程
代码演示一
public int calc() {
    int a = 100;
    int b = 200;
    int c = 300;
    return (a + b) * c;
}
指令集
 0 bipush 100
 2 istore_1
 3 sipush 200
 6 istore_2
 7 sipush 300
10 istore_3
11 iload_1
12 iload_2
13 iadd
14 iload_3
15 imul
16 ireturn
局部变量表、操作数栈深度

指令集概念模型

代码演示二
public void inc() {
    int i = 1;
    i = i++;
    int j = i++;
    int k = i + ++i * i++;
}
指令集
 0 iconst_1
 1 istore_1
 2 iload_1
 3 iinc 1 by 1
 6 istore_1
 7 iload_1
 8 iinc 1 by 1
11 istore_2
12 iload_1
13 iinc 1 by 1
16 iload_1
17 iload_1
18 iinc 1 by 1
21 imul
22 iadd
23 istore_3
24 return
局部变量表、操作数栈深度

指令集概念模型

虚拟机最终会对执行过程做出一些列优化来提高性能,实际的运作过程会和概念模型差距非常大,产生差距的原因时虚拟机中解析器和即时编译器都会对输入的字节码进行优化,即使解释器中也不是按照字节码指令去逐条执行的。
JVM虚拟机(二):字节码执行引擎的更多相关文章
- 深入理解Java虚拟机(字节码执行引擎)
		深入理解Java虚拟机(字节码执行引擎) 本文首发于微信公众号:BaronTalk 执行引擎是 Java 虚拟机最核心的组成部分之一.「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力, ... 
- JVM基础结构与字节码执行引擎
		JVM基础结构 JVM内部结构如下:栈.堆. 栈 JVM中的栈主要是指线程里面的栈,里面有方法栈.native方法栈.PC寄存器等等:每个方法栈是由栈帧组成的:每个栈帧是由局部变量表.操作数栈等组成. ... 
- 深入理解JVM虚拟机5:虚拟机字节码执行引擎
		虚拟机字节码执行引擎 转自https://juejin.im/post/5abc97ff518825556a727e66 所谓的「虚拟机字节码执行引擎」其实就是 JVM 根据 Class 文件中给 ... 
- 【java虚拟机系列】从java虚拟机字节码执行引擎的执行过程来彻底理解java的多态性
		我们知道面向对象语言的三大特点之一就是多态性,而java作为一种面向对象的语言,自然也满足多态性,我们也知道java中的多态包括重载与重写,我们也知道在C++中动态多态是通过虚函数来实现的,而虚函数是 ... 
- 深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)
		目录 1.类文件结构 1.1 Class类文件结构 1.2 魔数与Class文件的版本 1.3 常量池 1.4 访问标志 1.5 类索引.父索引与接口索引集合 1.6 字段表集合 1.7 方法集合 1 ... 
- 一夜搞懂 | JVM 字节码执行引擎
		前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习字节码执行引擎? 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一 ... 
- 深入理解java虚拟机(5)---字节码执行引擎
		字节码是什么东西? 以下是百度的解释: 字节码(Byte-code)是一种包含执行程序.由一序列 op 代码/数据对组成的二进制文件.字节码是一种中间码,它比机器码更抽象. 它经常被看作是包含一个执行 ... 
- 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的
		概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ... 
- 深入理解Java虚拟机读书笔记5----虚拟机字节码执行引擎
		五 虚拟机字节码执行引擎 1 运行时栈帧结构 ---栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素. ---栈帧中存储了方法的局部变 ... 
- JVM总结(五):JVM字节码执行引擎
		JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ... 
随机推荐
- [原题复现]百度杯CTF比赛 十月场 WEB EXEC(PHP弱类型)
			简介 原题复现: 考察知识点:PHP弱类型. 线上平台:https://www.ichunqiu.com/battalion(i春秋 CTF平台) 过程 看源码发现这个 vim泄露 下方都试了 ... 
- [web安全原理分析]-文件上传漏洞基础
			简介 前端JS过滤绕过 待更新... 文件名过滤绕过 待更新 Content-type过滤绕过 Content-Type用于定义网络文件的类型和网页编码,用来告诉文件接收方以什么形式.什么编码读取这个 ... 
- 安装mongodb扩展
			curl -O https://pecl.php.net/get/mongodb-1.2.3.tgz tar zxf mongodb-1.2.3.tgzcd mongodb-1.2.3 phpize ... 
- 手把手教你用思维导图软件iMindMap制作计划表
			在日常生活中小编也经常使用思维导图软件iMindMap来创建思维导图以规划工作及学习的安排.尤其是时间安排类型的思维导图,能极大程度的节约我们的时间,接下来就由小编以自己假期的社会实践向大家分享一下怎 ... 
- mac实用软件推荐 mac好用的软件
			终于入手了梦寐以求的苹果电脑,但却发现其操作系统与Windows大相径庭!不会使用怎么办?不用担心,我们可以借助软件的力量.一款实用的Mac软件不仅能够使你的工作效率显著提高,同时它还能帮助你更快地熟 ... 
- 「LOJ 6287」诗歌
			题面 LOJ 6287 Solution 枚举中间点\(j\),题目即求是否存在\(m\)使\(a[j]-m\)与\(a[j]+m\)分别在\(j\)两侧. 对于\(j\)左侧任意一个点\(i\),都 ... 
- 「CSP-S 2020」儒略日
			description luogu loj(暂无数据) solution 这道题作为T1,对选手们仔细看清题目的好习惯,以及不为2h调试.5k代码而心态爆炸的重要能力进行了较好的锻炼, 特别准备的只有 ... 
- Java基础教程——RunTime类
			RunTime类 java.lang.RunTime类代表Java程序的运行时环境. 可以进行垃圾回收(gc()),可以进行系统资源清理(runFinalization()): 可以加载文件(load ... 
- sharding事务。
			我们这么操作: 本地事务,但是会发现如果有异常两边都回滚了.看代码如下: rollback看一看: cachedConnections中缓存了2个connection. 对于每个connection调 ... 
- 学会了这一招,距离Git大神不远了!
			大家好,今天我们来介绍git当中一项非常重要的功能--交互式工具 有的时候如果我们要处理的文件很多,使用git add .等操作会非常有隐患,因为很有可能我们一不小心就疏忽了一些内容.如果我们使用一个 ... 
