安卓面试必备的JVM虚拟机制详解,看完之后简历上多一个技能( 六 )


上面所说的 invokespecial、invokeinterface 也被称为虚方法调用或者说动态绑定 , 相比于直接能定位方法的静态绑定而言 , 虚方法调用更加耗时 。 JVM 采用了一种空间换时间的策略来实现动态绑定 。 它为每个类生成一张方法表 , 用于快速定位目标方法 , 这个发生在类加载的准备阶段 。 方法表本质上是一个数组 , 它有两个特性 , 首先是子类方法表中包含父类方法表中所有的方法 , 其次是子类方法在方法表中的索引 , 与它所重写的父类方法的索引值相同 。 我们知道 , 方法调用指令中的符号引用会在执行之前解析为实际引用 。 对于静态绑定的方法调用而言 , 实际引用将指向具体的方法 , 对于动态绑定而言 , 实际引用则是方法表的索引值 。
JVM 也提供了内联缓存来加快动态绑定 , 它能够缓存虚方法调用中调用者的动态类型 , 以及该类型所对应的目标方法 。
JVM 是如何实现反射的?反射呢是 Java 语言中一个相当重要的特性 , 它允许正在运行的 Java 程序观测 , 甚至是修改程序的动态行为 。 表现为两点 , 一是对于任意一个类 , 都能知道这个类的所有属性和方法 , 二是对于任意一个对象 , 都能调用它的任意属性和方法 。
反射的使用还是比较简单的 , 涉及的 API 分为三类 , Class、Member(Filed、Method、Constructor)、Array and Enumerated 。 我当时是直接扒 Oracle 官方文档看的 , 讲的很详细 。
我对反射的好奇是来源于 , 经常会听说反射影响性能 , 那么性能开销在哪以及如何优化?
在此之前 , 我先讲讲 JVM 是如何实现反射的 。
我们可以直接 new Exception 来查看方法调用的栈轨迹 , 在调用 Method.invoke() 时 , 是去调用 DelegatingMethodAccessorImpl 的 invoke , 它的实际调用的是 NativeMethodAccessorImpl 的 invoke 方法 。 前者称为委派实现 , 后者称为本地实现 。 既然委派实现的具体实现是一个本地实现 , 那么为啥还需要委派实现这个中间层呢?其实 , Java 反射调用机制还设立了另一种动态生成字节码的实现 , 成为动态实现 , 直接使用 invoke 指令来调用目标方法 。 之所以采用委派实现 , 是在本地实现和动态实现直接做切换 。 依据注释信息 , 动态实现比本地实现相比 , 其运行效率要快上 20 倍 。 这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换 , 但由于生产字节码比较耗时 , 仅调用一次的话 , 反而是本地实现要快上三四倍 。 考虑到很多反射调用仅会执行一次 , JVM 设置了阈值 15 , 在 15 之下使用本地实现 , 高于 15 时便开始动态生成字节码采用动态实现 。 这也被称为 Inflation 机制 。
在反手说一下反射的性能开销在哪呢?平时我们会调用 Class.forName、Class.getMethod、以及 Method.invoke 这三个操作 。 其中 , Class.forName 会调用本地方法 , Class.getMethod 则会遍历该类的公有方法 , 如果没有匹配到 , 它还将遍历父类的公有方法 , 可想而知 , 这两个操作都非常耗时 。 下面就是 Method.invoke 调用本身的开销了 , 首先是 invoke 方法的参数是一个可变长参数 , 也就是构建一个 Object 数组存参数 , 这也同时带来了基本数据类型的装箱操作 , 在 invoke 内部会进行运行时权限检查 , 这也是一个损耗点 。 普通方法调用可能有一系列优化手段 , 比如方法内联、逃逸分析 , 而这又是反射调用所不能做的 , 性能差距再一次被放大 。
优化反射调用 , 可以尽量避免反射调用虚方法、关闭运行时权限检查、可能需要增大基本数据类型对应的包装类缓存、如果调用次数可知可以关闭 Inflation 机制 , 以及增加内联缓存记录的类型数目 。
JVM 是如何实现泛型的?Java 中的泛型不过是一个语法糖 , 在编译时还会将实际类型给擦除掉 , 不过会新增一个 checkcast 指令来做编译时检查 , 如果类型不匹配就抛出 ClassCastException 。
不过呢 , 字节码中仍然存在泛型参数的信息 , 如方法声明里的 T foo(T) , 以及方法签名 Signature 中的 "(TT;)TT" , 这些信息可以通过反射 Api getGenericXxx 拿到 。
除此之外 , 需要注意的是 , 泛型结合数组会有一些容易忽视的问题 。 数组是协变且具体化的 , 数组会在运行时才知道并检查它们的元素类型约束 , 可能出现编译时正常但运行时抛出 ArrayStoreException , 所以尽可能的使用列表 , 这就是 Effective Java 中推荐的列表优先于数组的建议 。 这在我们看集合源码时也能发现的到 , 比如 ArrayList , 它里面存数据是一个 Object[] , 而不是 E[] , 只不过在取的时候进行了强转 。 还有就是利用通配符来提升 API 的灵活性 , 简而言之即 PECS 原则 , 上取下存 。 典型的案例即 Collections.copy 方法了: