运行时栈帧结构

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

局部变量表

  局部变量表的容量以变量槽为最小单位。每个变量槽应该能存放一个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. 通过Tomcat Manager拿shell

    一.通过弱口令登录Tomcat后台 二.制作木马.war 1安装JDK 2.写一个jsp小马(我的小马是6.jsp) 3.cmd进小马的目录,然后运行 jar cvf shell.war  6.jsp ...

  2. gcc和g++理解

    环境使用的编译器版本是是gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39) 编译使用了c++11标准的程序时不能通过. 先放解决方法:g++ -std=c++11 ...

  3. python多线程——如何停止一个死循环的线程

    进程想要执行任务就需要依赖线程.换句话说,就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程. 那什么是多线程?提到多线程这里要说两个概念,就是串行和并行,搞清楚这个,我们才能更好地理解多 ...

  4. Postman设置自动捕获传递Cookie教程

    目录 前言 一.安装 1.Postman安装Install Interceptor Bridge 2.谷歌浏览器安装扩展Postman Interceptor 二.使用 1. 打开Capture Co ...

  5. 再论<? extends T>和<? super T>

    参考: https://www.cnblogs.com/drizzlewithwind/p/6100164.html Java 泛型 <? super T> 中 super 怎么 理解?与 ...

  6. LaTeX的字体字号设置

    字体属性: 字体族代码及注释: 显示效果: 字体系列设置及字体形状设置代码及注释: 显示效果: 字体大小及中文字号的设置代码及注释: 显示效果: ctex文档有关中文字号的排版: ctex文档的打开方 ...

  7. Beta冲刺随笔——Day_Six

    这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 这个作业要求在哪里 Beta 冲刺 这个作业的目标 团队进行Beta冲刺 作业正文 正文 其他参考文献 无 今日事今日毕 林涛: ...

  8. DRF使用Serializer来进行序列化和反序列化操作

    在serlizers中添加 # -*- coding: utf-8 -*- from rest_framework import serializers from .models import * c ...

  9. requests请求高德地图api

    高德地图通过GET方式发送url请求数据.url里需要包含开发者key以及一些请求的具体参数.(详情可见高德官网)高德返回的数据默认为JSON格式,方便处理. 顺带来回忆一下requests模块的一些 ...

  10. 老猿学5G扫盲贴:移动边缘计算(Mobile Edge Computing, MEC)

    版权声明:本文为CSDN博主「魏晓蕾」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明. 原文链接:https://blog.csdn.net/gongxifaca ...