一、方法调用

指令名称

描述

invokestatic

用于调用静态方法

invokespecial

用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的default方法

invokevirtual

用于调用非私有实例方法

invokeinterface

用于调用接口方法

invokedynamic

用于调用动态方法

1.1 虚方法调用和非虚方法调用

虚方法调用:可以被子类重写的方法调用,需要运行时才能确定具体的调用类型。接口方法调用(invokeinterface 指令)和非私有实例方法调用(invokevirtual 指令)都属于虚方法调用。

非虚方法调用:被invokestatic和invokespecial指令调用的方法,在解析阶段可以确定唯一的调用版本。静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这五种方法调用在类加载的时候就可以把符号引用解析为该方法的直接引用。

1.2 静态绑定和动态绑定

静态绑定:包括用于调用静态方法的 invokestatic 指令,和用于调用构造器、私有实例方法以及超类非私有实例方法的 invokespecial 指令。如果虚方法调用指向一个标记为 final 的方法,那么 Java 虚拟机也可以静态绑定该虚方法调用的目标方法。

动态绑定:Java 虚拟机需要根据调用者的动态类型,来确定虚方法调用的目标方法。

1.3 java重写和jvm重写的区别

java的重写:指的是方法名相同并且参数类型也相同的方法之间的关系。

Java虚拟机的重写:除了方法名和参数类型之外,返回类型也必须一致。

对于 Java 语言中重写而 Java 虚拟机中非重写的情况,编译器会通过生成桥接方法来实现 Java 中的重写语义。

eg:当一个声明类型为 Merchant,实际类型为 NaiveMerchant 的对象,调用 actionPrice 方法时,字节码里的符号引用指向的是 Merchant.actionPrice(double,Customer) 方法。Java 虚拟机将动态绑定至 NaiveMerchant 类的桥接方法之中,并且调用其 actionPrice(double,Customer) 方法。

interface Customer {boolean isVIP();
}class Merchant {public Number actionPrice(double price, Customer customer) {return Double.doubleToLongBits(2.0);}
}public class NaiveMerchant extends Merchant {@Overridepublic Double actionPrice(double price, Customer customer) {return 3.0;}public static void main(String[] args) {Merchant naiveMerchant = new NaiveMerchant();Number price =naiveMerchant.actionPrice(1.0d,null);}}

javap -v 反编译如下

public class NaiveMerchant extends Merchantminor version: 0major version: 55flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #5                          // NaiveMerchantsuper_class: #9                         // Merchantinterfaces: 0, fields: 0, methods: 4, attributes: 1
Constant pool:#1 = Methodref          #9.#21         // Merchant."<init>":()V#2 = Double             3.0d#4 = Methodref          #22.#23        // java/lang/Double.valueOf:(D)Ljava/lang/Double;#5 = Class              #24            // NaiveMerchant#6 = Methodref          #5.#21         // NaiveMerchant."<init>":()V#7 = Methodref          #9.#25         // Merchant.actionPrice:(DLCustomer;)Ljava/lang/Number;#8 = Methodref          #5.#26         // NaiveMerchant.actionPrice:(DLCustomer;)Ljava/lang/Double;#9 = Class              #27            // Merchant#10 = Utf8               <init>#11 = Utf8               ()V#12 = Utf8               Code#13 = Utf8               LineNumberTable#14 = Utf8               actionPrice#15 = Utf8               (DLCustomer;)Ljava/lang/Double;#16 = Utf8               main#17 = Utf8               ([Ljava/lang/String;)V#18 = Utf8               (DLCustomer;)Ljava/lang/Number;#19 = Utf8               SourceFile#20 = Utf8               NaiveMerchant.java#21 = NameAndType        #10:#11        // "<init>":()V#22 = Class              #28            // java/lang/Double#23 = NameAndType        #29:#30        // valueOf:(D)Ljava/lang/Double;#24 = Utf8               NaiveMerchant#25 = NameAndType        #14:#18        // actionPrice:(DLCustomer;)Ljava/lang/Number;#26 = NameAndType        #14:#15        // actionPrice:(DLCustomer;)Ljava/lang/Double;#27 = Utf8               Merchant#28 = Utf8               java/lang/Double#29 = Utf8               valueOf#30 = Utf8               (D)Ljava/lang/Double;
{public NaiveMerchant();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method Merchant."<init>":()V4: returnLineNumberTable:line 12: 0public java.lang.Double actionPrice(double, Customer);descriptor: (DLCustomer;)Ljava/lang/Double;flags: (0x0001) ACC_PUBLICCode:stack=2, locals=4, args_size=30: ldc2_w        #2                  // double 3.0d3: invokestatic  #4                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;6: areturnLineNumberTable:line 15: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=4, locals=3, args_size=10: new           #5                  // class NaiveMerchant3: dup4: invokespecial #6                  // Method "<init>":()V7: astore_18: aload_19: dconst_110: aconst_null11: invokevirtual #7                  // Method Merchant.actionPrice:(DLCustomer;)Ljava/lang/Number;14: astore_215: returnLineNumberTable:line 19: 0line 20: 8line 21: 15public java.lang.Number actionPrice(double, Customer);descriptor: (DLCustomer;)Ljava/lang/Number;flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETICCode:stack=4, locals=4, args_size=30: aload_01: dload_12: aload_33: invokevirtual #8                  // Method actionPrice:(DLCustomer;)Ljava/lang/Double;6: areturnLineNumberTable:line 12: 0
}

在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。符号引用存储在 class 文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。在执行使用了符号引用的字节码前,Java 虚拟机需要解析这些符号引用,并替换为实际引用。

1.4 解析符号引用

在C中查找符合名字及描述符的方法。
如果没有找到,在C的父类中继续搜索,直至Object类。
如果没有找到,在C所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的

对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找。

在I中查找符合名字及描述符的方法。
如果没有找到,在Object类中的公有实例方法中搜索。
如果没有找到,则在I的超接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。

经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。

1.5 方法表

在类加载的准备阶段,在类的方法区中构造与该类相关联的方法表。方法表分invokevirtual使用的虚方法表(virtual method table,vtable)和invokeinterface使用的接口方法表(interface method table,itable)。方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。

方法表满足两个特质:

其一,子类方法表中包含父类方法表中的所有方法。

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。

eg:

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 {public void hardChoice(QQ arg) {System.out.println("son choose qq");}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());}
}

其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。

为了程序实现方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的虚方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。

在解析虚方法调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。

eg:


abstract class Passenger {abstract void passThroughImmigration();@Overridepublic String toString() { return null;}
}
class ForeignerPassenger extends Passenger {@Overridevoid passThroughImmigration() { /* 外国人通道 */ }
}
class ChinesePassenger extends Passenger {@Overridevoid passThroughImmigration() { /* 中国人通道 */ }void visitDutyFreeShops() { /* 逛免税店 */ }
}

相应的虚方法表:

1.6 内联缓存

内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。对于内联缓存来说,有对应的单态内联缓存、多态内联缓存和超多态内联缓存。

单态内联缓存,顾名思义,便是只缓存了一种动态类型以及它所对应的目标方法。它的实现非常简单:比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。

多态内联缓存,则缓存了多个动态类型及其目标方法。它需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法。

为了节省内存空间,Java 虚拟机只采用单态内联缓存。对于内联缓存中的内容,我们有两种思路。

一是替换单态内联缓存中的纪录

在最坏情况下,我们用两种不同类型的调用者,轮流执行该方法调用,那么每次进行方法调用都将替换内联缓存。也就是说,只有写缓存的额外开销,而没有用缓存的性能提升。

另外一种选择则是劣化为超多态状态

这也是 Java 虚拟机的具体实现方式。处于这种状态下的内联缓存,实际上放弃了优化的机会。它将直接访问方法表,来动态绑定目标方法。与替换内联缓存纪录的做法相比,它牺牲了优化的机会,但是节省了写缓存的额外开销。

eg:


public abstract class Passenger {abstract void passThroughImmigration();public static void main(String[] args) {Passenger a = new ChinesePassenger();Passenger b = new ForeignerPassenger();long current = System.currentTimeMillis();for (int i = 1; i <= 2_000_000_000; i++) {if (i % 100_000_000 == 0) {long temp = System.currentTimeMillis();System.out.println(temp - current);current = temp;}Passenger c = (i < 1_000_000_000) ? a : b;//      Passenger c = (i % 2)==0? a : b;c.passThroughImmigration();}}
}
class ChinesePassenger extends Passenger {@Override void passThroughImmigration() {}
}
class ForeignerPassenger extends Passenger {@Override void passThroughImmigration() {}
}

为了消除方法内联的影响,使用以下命令。

java -XX:CompileCommand='dontinline,*.passThroughImmigration' Passenger

java -XX:CompileCommand='dontinline,*.passThroughImmigration' Passenger
CompileCommand: dontinline *.passThroughImmigration
258                   //缓存动态类型
262
255
284
274
259
257
254
257
257
371                  //缓存失效,劣化为超多态
366
370
367
368
376
419
433
385
426

1.7 方法内联

在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。

以 getter/setter 为例,如果没有方法内联,在调用 getter/setter 时,程序需要保存当前方法的执行位置,创建并压入用于 getter/setter 的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。而当内联了对 getter/setter 的方法调用后,上述操作仅剩字段访问。

在 C2 中,方法内联是在解析字节码的过程中完成的。每当碰到方法调用字节码时,C2 将决定是否需要内联该方法调用。如果需要内联,则开始解析目标方法的字节码。

静态方法调用

eg:

public static boolean flag = true;
public static int value0 = 0;
public static int value1 = 1;public static int foo(int value) {int result = bar(flag);if (result != 0) {return result;} else {return value;}
}public static int bar(boolean flag) {return flag ? value0 : value1;
}

foo方法的IR图(代码中间表示)(内联前)

在编译 foo 方法时,其对应的 IR 图中将出现对 bar 方法的调用,即上图中的 5 号 Invoke 节点。如果内联算法判定应当内联对 bar 方法的调用时,那么即时编译器将开始解析 bar 方法的字节码,并生成对应的 IR 图,如下图所示。

接下来,即时编译器便可以进行方法内联,把 bar 方法所对应的 IR 图纳入到对 foo 方法的编译中。具体的操作便是将 foo 方法的 IR 图中 5 号 Invoke 节点替换为 bar 方法的 IR 图。

将flag、value0、value1定义成final类型


public final static boolean flag = true;
public final static int value0 = 0;
public final static int value1 = 1;public static int foo(int value) {int result = bar(flag);if (result != 0) {return result;} else {return value;}
}public static int bar(boolean flag) {return flag ? value0 : value1;
}

foo的IR图(内联后)

进一步优化(死代码消除)

foo的IR图(优化后)

方法内联性能影响

强制进行方法内联,如下命令:

java -XX:CompileCommand='inline,*.passThroughImmigration' Passenger


CompileCommand: inline *.passThroughImmigration
86
152
152
159
165
179
145
146
150
144
331
246
235
235
232
234
235
235
233
234

参考链接:深入拆解Java虚拟机_JVM_Java底层-极客时间

jvm虚拟机浅谈(二)相关推荐

  1. 【JVM】浅谈双亲委派和破坏双亲委派

    转载自   [JVM]浅谈双亲委派和破坏双亲委派 一.前言 笔者曾经阅读过周志明的<深入理解Java虚拟机>这本书,阅读完后自以为对jvm有了一定的了解,然而当真正碰到问题的时候,才发现自 ...

  2. 浅谈二叉搜索树(BST)

    浅谈BST 前言 最近不顺心的事情有点多,再加上赶ptaL2 的题单,很久没做知识总结了 ,现在pta的题目告一段落,参考了某大佬 (某卷王) 总结的知识点,鸣谢大佬!总结一下BST问题的知识点,供以 ...

  3. 【JVM】浅谈对OOM的认识

    一张图区分Exception和Error 1.java.lang.StackOverflowError 这是栈溢出错误,如果一个线程所需用到栈的大小>配置允许最大的栈大小,那么jvm就会抛出St ...

  4. 浅谈二维码门禁在互联网的时代特色

    在互联网发达的今天,二维码门禁应运而生,它通过利用物联网和云计算等新技术,实现对门禁系统的人员流动和行动轨迹进行一个精细化.实时性的管理模式.二维码门禁在互联网时代下,逐渐成为传统门禁的新发展点,也为 ...

  5. 浅谈二维和三维图像数据

    首先对于任何一个二维图像,加载入内存后都能看作一个二维像素数组.假如是一张8位图像,它的每个像素值可以用一个0~255的unsigned char表示,也就是说图像可以看成一个unsigned cha ...

  6. 小知识:浅谈二维码的生成和识别原理

    目录 前言 条形码 静态二维码 二进制生成图形码 二维码的定位 前言 不知不觉中,我们的生活到处充满了二维码.登录账户需要二维码:加好友需要二维码:共享单车需要二维码:商品包装上也有二维码:甚至连楼下 ...

  7. 浅谈二维码和一维码有何区别

    二维码和一维码的区别如下:外观:一维码:一维码是由纵向黑条和白条组成,黑白相间.而且条纹的粗细也不同,通常条纹下还会有英文字母或阿拉伯数字.二维码:二维码通常为方形结构,不单由横向和纵向的条码组成,而 ...

  8. 浅谈二维码的生成和识别原理

    前言 不知不觉中,我们的生活到处充满了二维码.登录账户需要二维码:加好友需要二维码:共享单车需要二维码:商品包装上也有二维码:甚至连楼下卖水果的阿姨手里都拿张二维码收款.那么,有没有想过这个二维码到底 ...

  9. 浅谈网络游戏的设计——服务器端编程 (3)

    关键词: 网络游戏 构思 策划 创意 竞争机制 竞争系统 本系列文章始终以浅谈二字开头,所以内容简单,不够深入,希望大家谅解. 但是,正如人类的学习过程一样,是一个由浅入深的过程.市面上很多昂贵的图书 ...

最新文章

  1. 关于程序多开的尝试。CreateMutex,OpenMuxtex,ReleaseMutex
  2. ue4 怎么传递变量到另一个蓝图_资深建模教你放置UE4蓝图节点,所以你就不要偷懒啦,认真点学...
  3. oracle登录账号和密码,oracle 登录账号与密码oracle按照中文排序
  4. 使用AutoMake轻松生成Makefile
  5. 发布 项目_项目发布会活动到底应该怎么办
  6. Spring Cloud Config统一管理微服务配置
  7. 驱动等待队列,poll和select编程
  8. golang之正则校验(验证某字符串是否符合正则表达式)
  9. 阶段3 1.Mybatis_06.使用Mybatis完成DAO层的开发_5 Mybatis中使用Dao实现类的执行过程分析-查询方法1...
  10. Vue打开动态html页面,vue.js中怎么打开新页面?
  11. 基于关系图卷积网络的源代码漏洞检测
  12. FlashBuilder找不到所需要的AdobeFlashPlayer调试器版本的解
  13. 用户体验要素的五个层次
  14. mongodb数据库扩展名_MongoDB文件型数据库
  15. 叩响港交所大门,KK集团能否成为“中国版秋叶原”?
  16. 分享一下杭州医院的看病流程(我去的杭州市第三人民医院)
  17. spring test如何设置DebuggingClassWriter.DEBUG_LOCATION_PROPERTY
  18. 由中序后序序列求前序序列
  19. Javascript中关于创建Object对象
  20. 1231:最小新整数——贪心

热门文章

  1. mysql下如何执行sql脚本
  2. 计算机应用能力优胜奖,计算机应用技能竞赛结果揭晓
  3. 网游巨头布局手机游戏:处于摸索阶段 多从PC端移植
  4. html字体模糊怎么变清晰,电脑WIN7字体模糊怎么变清晰详细步骤
  5. 完美世界3D格斗手游[格斗宝贝]今日公測
  6. Java内存区域与内存溢出异常
  7. 使用最新版本Android NDK-r21 编译 opencv-3.3.1 + opencv_contrib-3.3.1
  8. 大数据周刊专访Kyligence CTO李扬 | 我们让数据驱动你的每一个重要决策
  9. 常见的内网穿透工具简易使用教程
  10. Qt 打印时, 输出文件和行号 __FIEL__ __LINE__