深入理解java虚拟机(十四)正确利用 JVM 的方法内联

时间:2022-08-27 23:04:03

在IntelliJ IDEA里面Ctrl+Alt+M用来拆分方法。选中一段代码,敲下这个组合,非常简单。Eclipse也用类似的快捷键,使用 Alt+Shift+M。我讨厌长的方法,提起这个下面这个方法我就觉得太长了:

  1. public void processOnEndOfDay(Contract c) {
  2. if (DateUtils.addDays(c.getCreated(), 7).before(new Date())) {
  3. priorityHandling(c, OUTDATED_FEE);
  4. notifyOutdated(c);
  5. log.info("Outdated: {}", c);
  6. } else {
  7. if (sendNotifications) {
  8. notifyPending(c);
  9. }
  10. log.debug("Pending {}", c);
  11. }
  12. }

首先,它有个条件判断可读性很差。先不管它怎么实现的,它做什么的才最关键。我们先把它拆分出来:

  1. public void processOnEndOfDay(Contract c) {
  2. if (isOutDate(c)) {
  3. priorityHandling(c, OUTDATED_FEE);
  4. notifyOutdated(c);
  5. log.info("Outdated: {}", c);
  6. } else {
  7. if (sendNotifications) {
  8. notifyPending(c);
  9. }
  10. log.debug("Pending {}", c);
  11. }
  12. }
  13. private boolean isOutDate(Contract c) {
  14. return DateUtils.addDays(c.getCreated(), 7).before(new Date());
  15. }

很明显,这个方法不应该放到这里:

  1. public void processOnEndOfDay(Contract c) {
  2. if (c.isOutDate()) {
  3. priorityHandling(c, OUTDATED_FEE);
  4. notifyOutdated(c);
  5. log.info("Outdated: {}", c);
  6. } else {
  7. if (sendNotifications) {
  8. notifyPending(c);
  9. }
  10. log.debug("Pending {}", c);
  11. }
  12. }

注意到什么不同吗?我的IDE把isOutdated方法改成Contract的实例方法了,这才像样嘛。不过我还是不爽。这个方法做的事太杂了。一个分支在处理业务相关的逻辑priorityHandling,以及发送系统通知和记录日志。另一个分支在则根据判断条件做系统通知,同时记录日志。我们先把处理过期合同拆分成一个独立的方法.

  1. public void processOnEndOfDay(Contract c) {
  2. if (c.isOutDate()) {
  3. handleOutdated(c);
  4. } else {
  5. if (sendNotifications) {
  6. notifyPending(c);
  7. }
  8. log.debug("Pending {}", c);
  9. }
  10. }
  11. private void handleOutdated(Contract c) {
  12. priorityHandling(c, OUTDATED_FEE);
  13. notifyOutdated(c);
  14. log.info("Outdated: {}", c);
  15. }

有人会觉得这样已经够好了,不过我觉得两个分支并不对称令人扎眼。handleOutdated方法层级更高些,而else分支更偏细节。软件应该清晰易读,因此不要把不同层级间的代码混到一起。这样我会更满意:

  1. public void processOnEndOfDay(Contract c) {
  2. if (c.isOutDate()) {
  3. handleOutdated(c);
  4. } else {
  5. stillPending(c);
  6. }
  7. }
  8. private void stillPending(Contract c) {
  9. if (sendNotifications) {
  10. notifyPending(c);
  11. }
  12. log.debug("Pending {}", c);
  13. }
  14. private void handleOutdated(Contract c) {
  15. priorityHandling(c, OUTDATED_FEE);
  16. notifyOutdated(c);
  17. log.info("Outdated: {}", c);
  18. }

这个例子看起来有点装,不过其实我想证明的是另一个事情。虽然现在不太常见了,不过还是有些开发人员不敢拆分方法,担心这样的话影响运行效率。他们不知道JVM其实是个非常棒的软件(它其实甩Java语言好几条街),它内建有许多非常令人惊讶的运行时优化。首先短方法更利于JVM推断。流程更明显,作用域更短,副作用也更明显。如果是长方法JVM可能直接就跪了。第二个原因则更重要:

方法内联

如果JVM监测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身。比如说下面这个:

  1. private int add4(int x1, int x2, int x3, int x4) {
  2. return add2(x1, x2) + add2(x3, x4);
  3. }
  4. private int add2(int x1, int x2) {
  5. return x1 + x2;
  6. }

可以肯定的是运行一段时间后JVM会把add2方法去掉,并把你的代码翻译成:

  1. private int add4(int x1, int x2, int x3, int x4) {
  2. return x1 + x2 + x3 + x4;
  3. }

注意这说的是JVM,而不是编译器。javac在生成字节码的时候是比较保守的,这些工作都扔给JVM来做。事实证明这样的设计决策是非常明智的:

JVM更清楚运行的目标环境 ,CPU,内存,体系结构,它可以更积极的进行优化。 JVM可以发现你代码运行时的特征,比如,哪个方法被频繁的执行,哪个虚方法只有一个实现,等等。 旧编译器编译的.class在新版本的JVM上可以获取更快的运行速度。更新JVM和重新编译源代码,你肯定更倾向于后者。

我们对这些假设做下测试。我写了一个小程序,它有着分治原则的最糟实现的称号。add128方法需要128个参数并且调用了两次add64方法——前后两半各一次。add64也类似,不过它是调用了两次add32。你猜的没错,最后会由add2方法来结束这一切,它是干苦力活的。有些数字我给省略了,免得亮瞎了你的眼睛:

  1. public class ConcreteAdder {
  2. public int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) {
  3. return add64(x1, x2, x3, x4, ... more ..., x63, x64) +
  4. add64(x65, x66, x67, x68, ... more ..., x127, x128);
  5. }
  6. private int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) {
  7. return add32(x1, x2, x3, x4, ... more ..., x31, x32) +
  8. add32(x33, x34, x35, x36, ... more ..., x63, x64);
  9. }
  10. private int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32) {
  11. return add16(x1, x2, x3, x4, ... more ..., x15, x16) +
  12. add16(x17, x18, x19, x20, ... more ..., x31, x32);
  13. }
  14. private int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16) {
  15. return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16);
  16. }
  17. private int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) {
  18. return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8);
  19. }
  20. private int add4(int x1, int x2, int x3, int x4) {
  21. return add2(x1, x2) + add2(x3, x4);
  22. }
  23. private int add2(int x1, int x2) {
  24. return x1 + x2;
  25. }
  26. }

不难发现,调用add128方法最后一共产生了127个方法调用。太多了。作为参考,下面这有个简单直接的实现版本:

  1. public class InlineAdder {
  2. public int add128n(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) {
  3. return x1 + x2 + x3 + x4 + ... more ... + x127 + x128;
  4. }
  5. }

最后再来一个使用了抽象类和继承的实现版本。127个虚方法调用开销是非常大的。这些方法需要动态分发,因此要求更高,所以无法进行内联。

  1. public abstract class Adder {
  2. public abstract int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128);
  3. public abstract int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64);
  4. public abstract int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32);
  5. public abstract int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16);
  6. public abstract int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8);
  7. public abstract int add4(int x1, int x2, int x3, int x4);
  8. public abstract int add2(int x1, int x2);
  9. }

还有一个实现:

  1. public class VirtualAdder extends Adder {
  2. @Override
  3. public int add128(int x1, int x2, int x3, int x4, ... more ..., int x128) {
  4. return add64(x1, x2, x3, x4, ... more ..., x63, x64) +
  5. add64(x65, x66, x67, x68, ... more ..., x127, x128);
  6. }
  7. @Override
  8. public int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) {
  9. return add32(x1, x2, x3, x4, ... more ..., x31, x32) +
  10. add32(x33, x34, x35, x36, ... more ..., x63, x64);
  11. }
  12. @Override
  13. public int add32(int x1, int x2, int x3, int x4, ... more ..., int x32) {
  14. return add16(x1, x2, x3, x4, ... more ..., x15, x16) +
  15. add16(x17, x18, x19, x20, ... more ..., x31, x32);
  16. }
  17. @Override
  18. public int add16(int x1, int x2, int x3, int x4, ... more ..., int x16) {
  19. return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16);
  20. }
  21. @Override
  22. public int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) {
  23. return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8);
  24. }
  25. @Override
  26. public int add4(int x1, int x2, int x3, int x4) {
  27. return add2(x1, x2) + add2(x3, x4);
  28. }
  29. @Override
  30. public int add2(int x1, int x2) {
  31. return x1 + x2;
  32. }
  33. }

受到我的另一篇关于@Cacheable 负载的文章的一些热心读者的鼓舞,我写了个简单的基准测试来比较这两个过度分拆的ConcreteAdder和VirtualAdder的负载。结果出人意外,还有点让人摸不着头脑。我在两台机器上做了测试(红色和蓝色的),同样的程序不同的是第二台机器CPU核数更多而且是64位的:

深入理解java虚拟机(十四)正确利用 JVM 的方法内联

具体的环境信息:

深入理解java虚拟机(十四)正确利用 JVM 的方法内联

看起来慢的机器上JVM更倾向于进行方法内联。不仅是简单的私有方法调用的版本,虚方法的版本也一样。为什么会这样?因为JVM发现Adder只有一个子类,也就是说每个抽象方法都只有一个版本。如果你在运行时加载了另一个子类(或者更多),你会看到性能会直线下降,因为无能再进行内联了。先不管这个了,从测试中来看,

这些方法的调用并不是开销很低,是根本就没有开销!

方法调用(还有为了可读性而加的文档)只存在于你的源代码和编译后的字节码里,运行时它们完全被清除掉了(内联了)。

我对第二个结果也不太理解。看起来性能高的机器B运行单个方法调用的时候要快点,另两个就要慢些。也许它倾向于延迟进行内联?结果是有些不同,不过差距也不是那么的大。就像 优化栈跟踪信息生成 那样——如果你为了优化代码性能,手动进行内联,把方法越搞越庞大,越弄越复杂,那你就真的错了。

ps:64bit 机器之所以运行慢有可能是因为 JVM 内联的要求的方法长度较长。

深入理解java虚拟机(十四)正确利用 JVM 的方法内联的更多相关文章

  1. 深入理解java虚拟机(四)垃圾收集算法及HotSpot实现

    垃圾收集算法 一般来说,垃圾收集算法分为四类: 标记-清除算法 最基础的算法便是标记-清除算法(Mark-Sweep).算法分为“标记”和“清除”两个阶段:首先标记处需要收集的对象,在标记完成之后,再 ...

  2. 深入理解java虚拟机-第四章

    第4章 虚拟机性能监按与故障处理工具 jps 虚拟机进程状况工具 jstat 虚拟机统计信息监视工具 JVM Statistics Monitoring Tool jstat [ option vmi ...

  3. 《深入理解Java虚拟机》-----第5章 jvm调优案例分析与实战

    案例分析 高性能硬件上的程序部署策略 例 如 ,一个15万PV/天左右的在线文档类型网站最近更换了硬件系统,新的硬件为4个CPU.16GB物理内存,操作系统为64位CentOS 5.4 , Resin ...

  4. Java虚拟机(四):JVM类加载机制

    1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...

  5. 理解java虚拟机内存分配堆,栈和方法区

    栈:存放局部变量 堆:存放new出来的对象 方法区:存放类的信息,static变量,常量池(字符串常量) 在堆中,可以说是堆的一部分   创建了一个student类,定义了name属性, id静态变量 ...

  6. 深入理解Java虚拟机(十)——线程安全与锁优化

    什么是线程安全 当多个线程同时访问一个对象的时候,不需要考虑什么额外的操作就能获取正确的值,就是线程安全的. 线程安全的程度 1.不可变 不可变的对象一定是线程安全的,因为值始终只有一个. final ...

  7. 重读《深入理解Java虚拟机》四、虚拟机如何加载Class文件

    1.Java语言的特性 Java代码经过编译器编译成Class文件(字节码)后,就需要虚拟机将其加载到内存里面执行字节码所定义的代码实现程序开发设定的功能. Java语言中类型的加载.连接(验证.准备 ...

  8. java虚拟机(十四)--字节码指令

    字节码指令其实是很重要的,在之前学习String等内容,深入到字节码层面很容易找到答案,而不是只是在网上寻找答案,还有可能是错误的. PS:本文基于jdk1.8 首先写个简单的类: public cl ...

  9. 《深入理解Java虚拟机》(五)JVM调优 - 工具

    JVM调优 - 工具 JConsole:Java监视与管理控制台 JConsole是一个机遇JMX(Java Management Extensions,即Java管理扩展)的JVM监控与管理工具,监 ...

随机推荐

  1. Linux之搭建自己的根文件系统

    Hi!大家好,我是CrazyCatJack.又和大家见面了.今天给大家带来的是构建Linux下的根文件系统.希望大家看过之后都能构建出符合自己需求的根文件系统^_^ 1.内容概述 1.构造过程 今天给 ...

  2. 获取IOS应用的子目录

    在开发IOS应用时,我们经常需要将素材分类,并放入相应地子目录中. 在开发代码时,需要访问这些素材时,就需要获取对应的子目录路径.那么如何获取呢? 获取应用路径 首先,要找到应用所在的路径. NSSt ...

  3. 正则表达式在JS中的应用

    JavaScript表单验证email,判断一个输入量是否为邮箱email,通过正则表达式实现.//检查email邮箱function isEmail(str){       var reg = /^ ...

  4. easyUI之window

    window组件是一个可拖动.浮动的面板,用于显示信息.内容可用 href或ajax获取. window是一个显示窗口,同时也可以显示layout的功能(也就是创建复合的组合窗口),如 <div ...

  5. Unity3D NGUI自适应屏幕分辨率(2014&sol;4&sol;17更新)

    原地址:http://blog.csdn.net/asd237241291/article/details/8126619 原创文章如需转载请注明:转载自 脱莫柔Unity3D学习之旅 本文链接地址: ...

  6. &lbrack;BZOJ 1046&rsqb; &lbrack;HAOI2007&rsqb; 上升序列 【DP】

    题目链接:BZOJ - 1046 题目分析 先倒着做最长下降子序列,求出 f[i],即以 i 为起点向后的最长上升子序列长度. 注意题目要求的是 xi 的字典序最小,不是数值! 如果输入的 l 大于最 ...

  7. Chapter 2 Open Book——10

    I sent that, and began again. 我发送了它,然后又一次重新开始写了. Mom,Everything is great. Of course it's raining. I ...

  8. JDK动态代理和cglib代理详解

    JDK动态代理 先做一下简单的描述,通过代理之后返回的对象已并非原类所new出来的对象,而是代理对象.JDK的动态代理是基于接口的,也就是说,被代理类必须实现一个或多个接口.主要原因是JDK的代理原理 ...

  9. 训练赛-Eyad and Math

    题意:给你四个数,求出a^b是否小于c^d,是的话输出<,否则输出>; 思路:因为数据很大,所以我们需要降低数据的规模,比如用一个log10()函数,这就能解决了,注意,要用scanf输入 ...

  10. WebApi上传文件

    上网搜了下Web Api上传文件的功能,发现都写的好麻烦,就自己写了一个,比较简单,直接上传文件就可以,可以用Postman测试. 简单的举例 /// <summary> /// 超级简单 ...