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


使用双亲委派模型来组织类加载器之间的关系 , 有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系 。 比如 Object 类 , 无论哪个类加载器去加载 , 应用程序各种加载器环境中都是同一个类 , 同时也避免了重复加载 。 而且 , 双亲委派模型也保证了 Java 程序的稳定运作 。 比如在应用程序中你是不能直接使用 UnSafe 这一不安全操作的类的 。
双亲委派模型的实现相对简单 , 代码都集中在 ClassLoader 的 loadClass 方法中先检查是否已经被加载过了 , 如果没加载则先调用父加载器的 loadClass 方法 , 若父加载器为空则使用默认的启动类加载器作为父加载器 。 如果父加载器加载失败 , 抛出 ClassNotFoundException 异常 , 然后调用自己的 findClass 方法进行加载 。
编译器优化在公司内部 , 我是分享过一次关于编译优化的相关知识 。 课题是 “从 final '能够' 提升性能 , 谈编译优化” 。
对于 Java 代码的编译 , 分为前端编译和后端编译 。 前端编译是指通过 javac 工具 , 将 Java 代码转化为字节码的过程 。 既然 javac 负责字节码的生成 , 那肯定就会有一些通用的优化手段 。 比如常量折叠、自动装拆箱、条件编译等 , 其次还有 JDK9 使用 StringContactFactory 对 "+" 的重载提供的统一入口等 。 后端编译则指 JVM 内置的解释器和即时编译器(C1、C2) 。 JVM 在对代码执行的优化可以分为运行时优化和即时编译器(JIT)优化 。 运行时优化主要是解释执行和动态编译通用的一些机制 , 比如说锁机制(如偏斜锁)、内存分配机制(如 TLAB)等 。 除此之外 , 还有一些专门用于优化解释执行效率的 , 比如说模版解释器、内联缓存(inline cache , 用于优化虚方法调用的动态绑定) 。 JVM 的即时编译器优化是指将热点代码以方法为单位转化成机器码 , 直接运行在底层硬件之上 。 它采用了多种优化方式 , 包括静态编译器可以使用的如方法内联、逃逸分析 , 也包括基于程序的运行 profile 的投机性优化 。
下面我就主要讲一下方法内联和逃逸分析 。
方法内联 , 它指的是在编译的过程中遇到方法调用时 , 将目标方法的方法体纳入编译范围之中 , 并取代原方法调用的优化手段 。 方法内联不仅可以消除调用本身带来的性能开销 , 还可以进一步触发更多的优化 。 因此 , 它可以算是编译优化里最为重要的一环 。 以 getter/setter 为例 , 如果没有方法内联 , 在调用 getter/setter 时 , 程序需要保存当前方法的执行位置 , 创建并压入用于 getter/setter 的栈桢、访问字段、弹出栈桢 , 最后再恢复当前方法的执行 。 而当内联了对 getter/setter 的方法调用后 , 上述操作就只剩下字段访问了 。 但是即时编译器不会无限制的进行方法内联 , 它会根据方法的调用次数、方法体大小、Code cache 的空间等去决定是否要进行内联 。 比如即使是热点代码 , 如果方法体太大 , 也不会进行内联 , 因为会占用更多内存空间 。 所以平时编码中 , 尽可能使用小方法体 。 对于需要动态绑定的虚方法调用来说 , 即时编译器则需要先对虚方法调用进行去虚化 , 即转化为一个或多个直接调用 , 然后才能进行方法内联 。 说到这 , 你应该就明白 final/static 的好处了 。 所以尽量使用 final、private、static 关键字修饰方法 , 虚方法因为继承 , 会需要额外的类型检查才能知道实际上调用的是哪个方法 。
逃逸分析是判断一个对象是否被外部方法引用或外部线程访问的分析技术 , 即时编译器可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化 。 我们先看一下锁消除 , 如果即时编译器能够证明锁对象不逃逸 , 那么对该锁对象的加锁、解锁操作没有任何意义 , 因为其他线程并不能获得该锁对象 , 在这种情况下 , 即时编译器就可以消除对该不逃逸对象的加锁、解锁操作 。 比如 synchronized(new Object) 这种操作会被完全优化掉 。 不过一般不会有人这么写 , 事实上 , 逃逸分析的结果更多被用于将新建对象操作转换成栈上分配或者标量替换 。 我们知道 , Java 虚拟机中对象都是在堆上进行分配的 , 而堆上的内容对任何线程可见 , 与此同时 , JVM 需要对所分配的堆内存进行管理 , 并且在对象不再被引用时回收其所占据的内存 。 如果逃逸分析能够证明某些新建的对象不逃逸 , 那么 JVM 完全可以将其分配至栈上 , 并且在方法退出时 , 通过弹出当前方法的栈桢来自动回收所分配的内存空间 。 不过 , 由于实现起来需要更改大量假设了 “对象只能堆分配” 的代码 , 因此 HotSpot 虚拟机并没有采用栈上分配 , 而是使用了标量替换这么一项技术 。 所谓的标量 , 就是仅能存储一个值的变量 , 比如 Java 代码中的局部变量 。 标量替换这项优化技术 , 可以看成将原本对对象的字段的访问 , 替换成一个个的局部变量的访问 。


推荐阅读