运行时栈帧结构

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

局部变量表

  局部变量表的容量以变量槽为最小单位。每个变量槽应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress(可忽略,现在已经很少见了)。reference类型表示对一个对象实例的引用,即根据引用直接或间接的查到对象在java堆中的数据存放的起始地址、索引或对象所属数据类型在方法区中的存储的类型信息。上述类型均占用一个变量槽。long和double占用两个连续的变量槽。

示例1

实例方法(没有被static修饰的方法)局部变量表第0位是this。

  1. public void soltTest() {
  2. byte i = 15;
  3. }

  1. public void soltTest() {
  2. long i = 15;
  3. }

  为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重复使用的,方法体中定义的变量作用域没有全部覆盖整个方法,此变量占用的变量槽是可以被重复利用的。

注意:示例需设置虚拟机参数“-verbose:gc”

示例2

  1. public static void main(String[] args) {
  2. byte[] bytes = new byte[64 * 1024 * 1024];
  3. System.gc();
  4. }

控制台输出:

  1. [GC (System.gc()) 72123K->66690K(251392K), 0.0177919 secs]
  2. [Full GC (System.gc()) 66690K->66523K(251392K), 0.0042184 secs]

示例3

  1. public static void main(String[] args) {
  2. {
  3. byte[] bytes = new byte[64 * 1024 * 1024];
  4. }
  5. System.gc();
  6. }

控制台输出:

  1. [GC (System.gc()) 72123K->66674K(251392K), 0.0007715 secs]
  2. [Full GC (System.gc()) 66674K->66523K(251392K), 0.0041207 secs]

示例4

  1. public static void main(String[] args) {
  2. {
  3. byte[] bytes = new byte[64 * 1024 * 1024];
  4. }
  5. int a = 0;
  6. System.gc();
  7. }

控制台输出:

  1. [GC (System.gc()) 72123K->66690K(251392K), 0.0009232 secs]
  2. [Full GC (System.gc()) 66690K->987K(251392K), 0.0042235 secs]

结论:变量槽在没有复用时,不GC

操作数栈

操作数栈是后进先出栈。个人感觉操作数栈是局部变量表与方法区中间的数据中转站。

方法调用

  方法调用不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定调用哪个方法,暂时还未涉及方法内部的具体运行过程 。

解析

调用方法在程序代码写好、编译器进行编译那一刻就确定下来了,这类方法的调用被称为解析。在Java中符合“编译期可知,运行期不可变”要求的方法主要有静态方法和私有方法两大类。

调用不同类型的方法,字节码指令集里面设计了不同的指令。分别是:

  • invokestatic:用于调用静态方法。
  • invokespecial:用于调用实例构造器()方法,私有方法和父类中的方法。
  • invokevirtual:用于调用所有的虚方法。
  • invokeinterface:用于调用接口方法,在运行时再确定一个实现该接口的对象。
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

静态方法、私有方法、实例构造器、父类方法及final修饰的方法会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为”非虚方法“。

方法静态解析演示

  1. /**
  2. * @author Wang Chinda
  3. * @date 2020/3/31
  4. * @see
  5. * @since 1.0
  6. */
  7. public class StaticResolution {
  8. public static void sayHello() {
  9. System.out.println("Hello world");
  10. }
  11. public static void main(String[] args) {
  12. StaticResolution.sayHello();
  13. }
  14. }

指令:

  1. 0 getstatic #2 <java/lang/System.out>
  2. 3 ldc #3 <Hello world>
  3. 5 invokevirtual #4 <java/io/PrintStream.println>
  4. 8 return

分派

静态分派

所有依赖静态类型来决定调用哪个方法的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载。

方法静态分派演示

  1. /**
  2. * 控制台打印
  3. * hello, guy!
  4. * hello, guy!
  5. * @author Wang Chinda
  6. * @date 2020/3/31
  7. * @see
  8. * @since 1.0
  9. */
  10. public class StaticDispatch {
  11. static abstract class Human {
  12. }
  13. static class Man extends Human {
  14. }
  15. static class Woman extends Human {
  16. }
  17. public void sayHello(Human human) {
  18. System.out.println("hello, guy!");
  19. }
  20. public void sayHello(Man man) {
  21. System.out.println("hello, man");
  22. }
  23. public void sayHello(Woman woman) {
  24. System.out.println("Hello, women");
  25. }
  26. public static void main(String[] args) {
  27. Human man = new Man();
  28. Human woman = new Woman();
  29. StaticDispatch sd = new StaticDispatch();
  30. sd.sayHello(man);
  31. sd.sayHello(woman);
  32. }
  33. }

上面代码中的“Human”称为变量的“静态类型”,而后面的“Man”称为变量的”实际类型“。静态类型和实际类型再程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型时在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

  1. // 实际类型变化
  2. Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
  3. // 静态类型变化
  4. sd.sayHello((Man) man); // 控制台打印 hello, man
  5. sd.sayHello((Woman) woman); // 控制台打印 Hello, women

“静态类型”在代码被编译器编译之后,就已经确定类型引用,但是实际类型只有在程序运行时,才可以确定具体的引用类型。即,调用哪个方法以句柄所属类型匹配方法所携带的形参。

注意:编译器虽然能确定方法的重载版本,但很多情况下这个重载版本并不是唯一的,程序往往只能确定一个“相对更合适的”方法调用。

重载方法匹配优先级

  1. /**
  2. * @author Wang Chinda
  3. * @date 2020/4/2
  4. * @see
  5. * @since 1.0
  6. */
  7. public class OverLoad {
  8. public static void sayHello(Object arg) {
  9. System.out.println("hello Object!");
  10. }
  11. public static void sayHello(int arg) {
  12. System.out.println("Hello int!");
  13. }
  14. public static void sayHello(long arg) {
  15. System.out.println("hello long");
  16. }
  17. public static void sayHello(Character character) {
  18. System.out.println("hello character");
  19. }
  20. public static void sayHello(char arg) {
  21. System.out.println("hello char");
  22. }
  23. public static void sayHello(char... arg) {
  24. System.out.println("hello char...");
  25. }
  26. public static void sayHello(Serializable arg) {
  27. System.out.println("Hello serializable");
  28. }
  29. public static void main(String[] args) {
  30. sayHello('a');
  31. }
  32. }

上面代码控制台打印:

  1. hello char

'a'是char类型数据,最优匹配的当然是char类型形参方法调用。注释掉sayHello(char arg)方法,控制台打印:

  1. Hello int!

这时发生了一次自动类型转换,'a'除了可以代表一个字符,还可以代表数字97(字符'a'的Unicode数值为十进制数字97),因此参数类型为int的重载方法最合适。我们继续注释掉sayHello(int arg)方法,控制台打印:

  1. hello long

这时发生了两次自动类型转换,'a'转换为int的97之后,进一步转型为long的97L,此时参数类型为long的重载方法最合适。不过自动转型还会多次发生,按照char>int>long>float>double的顺序自动转型。我们继续注释掉sayHello(long arg)方法,控制台打印:

  1. hello character

这时发生了一次自动装箱,'a'被包装为它的封装类型java.lang.Character,此时参数类型为Character的重载方法最合适。我们继续注释掉sayHello(Character character)方法,控制台打印:

  1. Hello serializable

之所以输出Hello serializable是因为

  1. java.lang.Character implements java.io.Serializable, Comparable<Character>

此时若是同时存在sayHello(Comparable arg)方法, 编译会抛出模糊的方法调用错误,并拒绝编译。

  1. Ambiguous method call. Both sayHello (Serializable) in OverLoad and sayHello (Comparable) in OverLoad match

我们继续注释掉sayHello(Serializable arg)方法,控制台打印:

  1. hello Object!

这时是char装箱后转型为父类,如果有多层级父类,越接近的优先级越高。我们继续注释掉sayHello(Object arg)方法,控制台打印:

  1. hello char...

可见边长参数的重载优先级是最低的。

动态分派

在运行期间根据实际类型确定调用哪个目标方法的分派过程称为动态分派。

方法动态分派演示

  1. /**
  2. * 控制台打印:
  3. * man say hello
  4. * woman say hello
  5. * woman say hello
  6. *
  7. * @author Wang Chinda
  8. * @date 2020/4/2
  9. * @see
  10. * @since 1.0
  11. */
  12. public class DynamicDispatch {
  13. static abstract class Human {
  14. protected abstract void sayHello();
  15. }
  16. static class Man extends Human {
  17. @Override
  18. protected void sayHello() {
  19. System.out.println("man say hello");
  20. }
  21. }
  22. static class Woman extends Human {
  23. @Override
  24. protected void sayHello() {
  25. System.out.println("woman say hello");
  26. }
  27. }
  28. public static void main(String[] args) {
  29. Human man = new Man();
  30. Human woman = new Woman();
  31. man.sayHello();
  32. woman.sayHello();
  33. man = new Woman();
  34. man.sayHello();
  35. }
  36. }

指令展示

  1. 0 new #2 <com/chinda/invoke/DynamicDispatch$Man>
  2. 3 dup
  3. 4 invokespecial #3 <com/chinda/invoke/DynamicDispatch$Man.<init>>
  4. 7 astore_1
  5. 8 new #4 <com/chinda/invoke/DynamicDispatch$Woman>
  6. 11 dup
  7. 12 invokespecial #5 <com/chinda/invoke/DynamicDispatch$Woman.<init>>
  8. 15 astore_2
  9. 16 aload_1
  10. 17 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
  11. 20 aload_2
  12. 21 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
  13. 24 new #4 <com/chinda/invoke/DynamicDispatch$Woman>
  14. 27 dup
  15. 28 invokespecial #5 <com/chinda/invoke/DynamicDispatch$Woman.<init>>
  16. 31 astore_1
  17. 32 aload_1
  18. 33 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
  19. 36 return

invokevirtual指令的运行解析过程:

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

多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,只会对方法有效,对字段是无效的,因为字段不使用这条指令。

字段没有多态性演示

  1. /**
  2. * 控制台打印
  3. * I am Son, I have $0
  4. * I am Son, I have $4
  5. * This gay has $2
  6. *
  7. * @author Wang Chinda
  8. * @date 2020/4/2
  9. * @see
  10. * @since 1.0
  11. */
  12. public class FieldHasNoPolymorphic {
  13. static class Father {
  14. public int money = 1;
  15. public Father() {
  16. money = 2;
  17. showMeTheMoney();
  18. }
  19. public void showMeTheMoney() {
  20. System.out.println("I am Father, I have $" + money);
  21. }
  22. }
  23. static class Son extends Father {
  24. public int money = 3;
  25. public Son() {
  26. money = 4;
  27. showMeTheMoney();
  28. }
  29. @Override
  30. public void showMeTheMoney() {
  31. System.out.println("I am Son, I have $" + money);
  32. }
  33. }
  34. public static void main(String[] args) {
  35. Father gay = new Son();
  36. System.out.println("This gay has $" + gay.money);
  37. }
  38. }

字类初始化时,首先触发父类初始化,在父类初始化时,调用showMeTheMoney()虚方法,实际执行的是Son::showMeTheMoney()方法,此时子类还没有初始化,所以money值为0。初始化完父类初始化子类,此时money为4。执行打印时,调用的是父类中的属性,所以值为2。

单分派与多分派

方法的接收者和方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。

单分派和多分派演示

  1. /**
  2. * 控制台打印
  3. * father choose 360
  4. * son choose qq
  5. * @author Wang Chinda
  6. * @date 2020/4/2
  7. * @see
  8. * @since 1.0
  9. */
  10. public class Dispatch {
  11. static class QQ{}
  12. static class _360{}
  13. public static class Father {
  14. public void hardChoice(QQ arg) {
  15. System.out.println("father choose qq");
  16. }
  17. public void hardChoice(_360 arg) {
  18. System.out.println("father choose 360");
  19. }
  20. }
  21. public static class Son extends Father {
  22. @Override
  23. public void hardChoice(QQ arg) {
  24. System.out.println("son choose qq");
  25. }
  26. @Override
  27. public void hardChoice(_360 arg) {
  28. System.out.println("son choose 360");
  29. }
  30. }
  31. public static void main(String[] args) {
  32. Father father = new Father();
  33. Father son = new Son();
  34. father.hardChoice(new _360());
  35. son.hardChoice(new QQ());
  36. }
  37. }

指令演示

  1. 0 new #2 <com/chinda/invoke/Dispatch$Father>
  2. 3 dup
  3. 4 invokespecial #3 <com/chinda/invoke/Dispatch$Father.<init>>
  4. 7 astore_1
  5. 8 new #4 <com/chinda/invoke/Dispatch$Son>
  6. 11 dup
  7. 12 invokespecial #5 <com/chinda/invoke/Dispatch$Son.<init>>
  8. 15 astore_2
  9. 16 aload_1
  10. 17 new #6 <com/chinda/invoke/Dispatch$_360>
  11. 20 dup
  12. 21 invokespecial #7 <com/chinda/invoke/Dispatch$_360.<init>>
  13. 24 invokevirtual #8 <com/chinda/invoke/Dispatch$Father.hardChoice>
  14. 27 aload_2
  15. 28 new #9 <com/chinda/invoke/Dispatch$QQ>
  16. 31 dup
  17. 32 invokespecial #10 <com/chinda/invoke/Dispatch$QQ.<init>>
  18. 35 invokevirtual #11 <com/chinda/invoke/Dispatch$Father.hardChoice>
  19. 38 return

注意:invokevirtual #11 <com/chinda/invoke/Dispatch$Father.hardChoice> 静态分派指向的是Father::hardChoice()方法,但动态分派时,将方法指向到实际类型中的目标方法,即Son::hardChoice()。

基于栈的解释器执行过程

代码演示一

  1. public int calc() {
  2. int a = 100;
  3. int b = 200;
  4. int c = 300;
  5. return (a + b) * c;
  6. }

指令集

  1. 0 bipush 100
  2. 2 istore_1
  3. 3 sipush 200
  4. 6 istore_2
  5. 7 sipush 300
  6. 10 istore_3
  7. 11 iload_1
  8. 12 iload_2
  9. 13 iadd
  10. 14 iload_3
  11. 15 imul
  12. 16 ireturn

局部变量表、操作数栈深度

指令集概念模型

代码演示二

  1. public void inc() {
  2. int i = 1;
  3. i = i++;
  4. int j = i++;
  5. int k = i + ++i * i++;
  6. }

指令集

  1. 0 iconst_1
  2. 1 istore_1
  3. 2 iload_1
  4. 3 iinc 1 by 1
  5. 6 istore_1
  6. 7 iload_1
  7. 8 iinc 1 by 1
  8. 11 istore_2
  9. 12 iload_1
  10. 13 iinc 1 by 1
  11. 16 iload_1
  12. 17 iload_1
  13. 18 iinc 1 by 1
  14. 21 imul
  15. 22 iadd
  16. 23 istore_3
  17. 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. 深度分析:面试腾讯,阿里面试官都喜欢问的String源码,看完你学会了吗?

    前言 最近花了两天时间,整理了一下String的源码.这个整理并不全面但是也涵盖了大部分Spring源码中的方法.后续如果有时间还会将剩余的未整理的方法更新到这篇文章中.方便以后的复习和面试使用.如果 ...

  2. MyBatis学习02

    3.增删改查实现 select select标签是mybatis中最常用的标签之一 select语句有很多属性可以详细配置每一条SQL语句 SQL语句返回值类型.[完整的类名或者别名] 传入SQL语句 ...

  3. Jmeter测试Websocket接口

    前言 websocket是什么? WebSocket 协议在2008年诞生,2011年成为国际标准.所有浏览器都已经支持了. 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器 ...

  4. Python【集合】、【函数】、【三目运算】、【lambda】、【文件操作】

    set集合: •集合的创建; set_1 = set() #方法一 set_1 = {''} #方法二 •set是无序,不重复的集合; set_1 = {'k1','k2','k3'} set_1.a ...

  5. Fiddler 4 (利用Fiddler模拟恶劣网络环境)

    1.模拟弱网环境 打开Fiddler,Rules->Performance->勾选 Simulate Modem Speeds,勾选之后访问网站会发现网络慢了很多 解决办法去掉勾选的地方网 ...

  6. PyQt(Python+Qt)学习随笔:QListView的wordWrap属性

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QListView的wordWrap属性与QTableView的wordWrap属性功能完全相同,用 ...

  7. 使用Fiddle修改请求数据

    修改请求数据 以淘宝网为例 命令行中输入bpu 及要拦截的请求地址,如bpu https://s.taobao.com/search 在搜索栏中输入"面包机" 可以看到拦截到的请求 ...

  8. 稀疏矩阵三元组表快速转置(C语言实现)

    本来准备昨天下午写的,但是因为去参加360众测靶场的考核耽搁了,靶场的题目还是挺基础的. 继续学习吧. 使用黑色墨水在白纸上签名就像由像素点构成的稀疏矩阵.如图4所示. 图4 手写体签名 [问题]请将 ...

  9. Leetcode学习笔记(2)

    题目1 ID面试题 01.04 给定一个字符串,编写一个函数判定其是否为某个回文串的排列之一. 回文串是指正反两个方向都一样的单词或短语.排列是指字母的重新排列. 回文串不一定是字典当中的单词. 示例 ...

  10. float和double有什么区别?

    float和double在游戏行业肯定是用的很多的,虽然这是个很基础的问题,但是面试时被问到还是感觉说的不是很好. 所以还是总结一下: float 单精度浮点数在机内占 4 个字节,用 32 位二进制 ...