运行时栈帧结构

    栈帧是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接、和方法返回地址等信息。

局部变量表

  局部变量表的容量以变量槽为最小单位。每个变量槽应该能存放一个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指令的运行解析过程:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符与简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验证过程。
  4. 如果之中没有找到合适的方法,则抛出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虚拟机(二):字节码执行引擎的更多相关文章

  1. 深入理解Java虚拟机(字节码执行引擎)

    深入理解Java虚拟机(字节码执行引擎) 本文首发于微信公众号:BaronTalk 执行引擎是 Java 虚拟机最核心的组成部分之一.「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力, ...

  2. JVM基础结构与字节码执行引擎

    JVM基础结构 JVM内部结构如下:栈.堆. 栈 JVM中的栈主要是指线程里面的栈,里面有方法栈.native方法栈.PC寄存器等等:每个方法栈是由栈帧组成的:每个栈帧是由局部变量表.操作数栈等组成. ...

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

    虚拟机字节码执行引擎   转自https://juejin.im/post/5abc97ff518825556a727e66 所谓的「虚拟机字节码执行引擎」其实就是 JVM 根据 Class 文件中给 ...

  4. 【java虚拟机系列】从java虚拟机字节码执行引擎的执行过程来彻底理解java的多态性

    我们知道面向对象语言的三大特点之一就是多态性,而java作为一种面向对象的语言,自然也满足多态性,我们也知道java中的多态包括重载与重写,我们也知道在C++中动态多态是通过虚函数来实现的,而虚函数是 ...

  5. 深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)

    目录 1.类文件结构 1.1 Class类文件结构 1.2 魔数与Class文件的版本 1.3 常量池 1.4 访问标志 1.5 类索引.父索引与接口索引集合 1.6 字段表集合 1.7 方法集合 1 ...

  6. 一夜搞懂 | JVM 字节码执行引擎

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习字节码执行引擎? 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一 ...

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

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

  8. 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的

    概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ...

  9. 深入理解Java虚拟机读书笔记5----虚拟机字节码执行引擎

    五 虚拟机字节码执行引擎   1 运行时栈帧结构     ---栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素.     ---栈帧中存储了方法的局部变 ...

  10. JVM总结(五):JVM字节码执行引擎

    JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ...

随机推荐

  1. NLP之统计句法分析(PCFG+CYK算法)

    一.认识句法分析 首先,了解一下句法分析到底是什么意思?是做什么事情呢?顾名思义,感觉是学习英语时候讲的各种句法语法.没错!这里就是把句法分析过程交给计算机处理,让它分析一个句子的句法组成,然后更好理 ...

  2. MySQL错误修复:Table xx is marked as crashed and last (automatic?) repair failed

    问题一 Table xx is marked as crashed and last (automatic?) repair failed 有开发找到我,说数据库坏了,连不上数据库,看了下 MySQL ...

  3. 金九银十想面BAT?那这些JDK 动态代理的面试点你一定要知道

    一.什么是代理 代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问.代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理. 代理模式UM ...

  4. 深度分析:Redis 的数据结构及其使用场景分析,原来这么简单?

    Redis基础数据结构有哪些? 一.String(字符串) 在任何一种编程语言里,字符串String都是最基础的数据结构, 那你有想过Redis中存储一个字符串都进行了哪些操作嘛? 在Redis中St ...

  5. Python一行式代码

    # 简易Web Server,可以直接快速共享文件 python -m http.server # 脚本性能分析 python -m cProfile my_script.py # 列表辗平 impo ...

  6. Guitar Pro 7教程之打开播放文件的操作技巧

    前面的章节我们讲过了很多关于Guitar Pro的相关教程,由于最近{cms_selflink page='index' text='Guitar Pro'} 7中文版刚上新没多久,很多玩吉他的小伙伴 ...

  7. 数据恢复当选EasyRecovery,设备不再受限

    我们在逐渐适应信息电子化的同时,也有一些潜在的麻烦接踵而来,其中较为常见的就是文件和数据的保存问题. 显然,设备的存储空间是有限的,这就不可避免地会出现数据被删除.覆盖或丢失的现象,如果丢失的是重要数 ...

  8. ABBYY FineReader 15高级转换功能详解

    ABBYY FineReader 15(Windows系统)OCR文字识别软件拥有强大的OCR项目功能,能帮助用户检查识别区域.验证识别出的文本.预处理图像以提高 OCR精确性等等.其强大的OCR微调 ...

  9. kafka对接Rancher日志

    kafka对接Rancher日志 目录 kafka对接Rancher日志 概述 环境准备 正常对接kafka集群 1.helm添加bitnami库 2.下载 kafka 对应的chart压缩文件 3. ...

  10. spring框架:(一) 技术说明(技术介绍,技术优势以及发展史等)

    一.技术说明(技术介绍,技术优势以及发展史等) 1.1.什么是spring 1.2.spring由来(发展历程) 1.3.spring核心 1.4.spring优点 1.5.spring体系结构 1. ...