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

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

public void processOnEndOfDay(Contract c) {
		if (DateUtils.addDays(c.getCreated(), 7).before(new Date())) {
			priorityHandling(c, OUTDATED_FEE);
			notifyOutdated(c);
			log.info("Outdated: {}", c);
		} else {
			if (sendNotifications) {
				notifyPending(c);
			}
			log.debug("Pending {}", c);
		}
	}

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

public void processOnEndOfDay(Contract c) {
		if (isOutDate(c)) {
			priorityHandling(c, OUTDATED_FEE);
			notifyOutdated(c);
			log.info("Outdated: {}", c);
		} else {
			if (sendNotifications) {
				notifyPending(c);
			}
			log.debug("Pending {}", c);
		}
	}

	private boolean isOutDate(Contract c) {
		return DateUtils.addDays(c.getCreated(), 7).before(new Date());
	}

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

public void processOnEndOfDay(Contract c) {
		if (c.isOutDate()) {
			priorityHandling(c, OUTDATED_FEE);
			notifyOutdated(c);
			log.info("Outdated: {}", c);
		} else {
			if (sendNotifications) {
				notifyPending(c);
			}
			log.debug("Pending {}", c);
		}
	}

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

public void processOnEndOfDay(Contract c) {
		if (c.isOutDate()) {
			handleOutdated(c);
		} else {
			if (sendNotifications) {
				notifyPending(c);
			}
			log.debug("Pending {}", c);
		}
	}

	private void handleOutdated(Contract c) {
		priorityHandling(c, OUTDATED_FEE);
		notifyOutdated(c);
		log.info("Outdated: {}", c);
	}

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

public void processOnEndOfDay(Contract c) {
		if (c.isOutDate()) {
			handleOutdated(c);
		} else {
			stillPending(c);
		}
	}

	private void stillPending(Contract c) {
		if (sendNotifications) {
			notifyPending(c);
		}
		log.debug("Pending {}", c);
	}

	private void handleOutdated(Contract c) {
		priorityHandling(c, OUTDATED_FEE);
		notifyOutdated(c);
		log.info("Outdated: {}", c);
	}

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

方法内联

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

private int add4(int x1, int x2, int x3, int x4) {
		return add2(x1, x2) + add2(x3, x4);
	}

	private int add2(int x1, int x2) {
		return x1 + x2;
	}

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

private int add4(int x1, int x2, int x3, int x4) {
		return x1 + x2 + x3 + x4;
	}

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

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

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

public class ConcreteAdder {

  public int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) {
    return add64(x1, x2, x3, x4, ... more ..., x63, x64) +
        add64(x65, x66, x67, x68, ... more ..., x127, x128);
  }

  private int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) {
    return add32(x1, x2, x3, x4, ... more ..., x31, x32) +
        add32(x33, x34, x35, x36, ... more ..., x63, x64);
  }

  private int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32) {
    return add16(x1, x2, x3, x4, ... more ..., x15, x16) +
        add16(x17, x18, x19, x20, ... more ..., x31, x32);
  }

  private int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16) {
    return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16);
  }

  private int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) {
    return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8);
  }

  private int add4(int x1, int x2, int x3, int x4) {
    return add2(x1, x2) + add2(x3, x4);
  }

  private int add2(int x1, int x2) {
    return x1 + x2;
  }

} 

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

public class InlineAdder {

    public int add128n(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) {
        return x1 + x2 + x3 + x4 + ... more ... + x127 + x128;
    }
}

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

public abstract class Adder {

  public abstract int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128);

  public abstract int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64);

  public abstract int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32);

  public abstract int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16);

  public abstract int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8);

  public abstract int add4(int x1, int x2, int x3, int x4);

  public abstract int add2(int x1, int x2);
} 

还有一个实现:

public class VirtualAdder extends Adder {

  @Override
  public int add128(int x1, int x2, int x3, int x4, ... more ..., int x128) {
    return add64(x1, x2, x3, x4, ... more ..., x63, x64) +
        add64(x65, x66, x67, x68, ... more ..., x127, x128);
  }

  @Override
  public int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) {
    return add32(x1, x2, x3, x4, ... more ..., x31, x32) +
        add32(x33, x34, x35, x36, ... more ..., x63, x64);
  }

  @Override
  public int add32(int x1, int x2, int x3, int x4, ... more ..., int x32) {
    return add16(x1, x2, x3, x4, ... more ..., x15, x16) +
        add16(x17, x18, x19, x20, ... more ..., x31, x32);
  }

  @Override
  public int add16(int x1, int x2, int x3, int x4, ... more ..., int x16) {
    return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16);
  }

  @Override
  public int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) {
    return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8);
  }

  @Override
  public int add4(int x1, int x2, int x3, int x4) {
    return add2(x1, x2) + add2(x3, x4);
  }

  @Override
  public int add2(int x1, int x2) {
    return x1 + x2;
  }
} 

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

具体的环境信息:

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

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

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

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

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

文章原文来源于:

http://www.javacodegeeks.com/2013/02/how-aggressive-is-method-inlining-in-jvm.html

http://it.deepinmind.com/java/2014/03/01/JVM的方法内联.html

时间: 2024-10-17 11:28:18

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

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

垃圾收集算法 一般来说,垃圾收集算法分为四类: 标记-清除算法 最基础的算法便是标记-清除算法(Mark-Sweep).算法分为"标记"和"清除"两个阶段:首先标记处需要收集的对象,在标记完成之后,再统一回收所有被标记的对象. 这是最简单的一种算法,但是缺点也是很明显的:一个是效率问题,标记和清除效率都不高.二是空间问题,清除之后会产生大量的空间碎片,导致之后分配大对象找不到足够的连续对象而不得不触发另一次垃圾收集动作.算法执行过程如下图. 复制算法 复制算法(Co

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

第4章 虚拟机性能监按与故障处理工具 jps 虚拟机进程状况工具 jstat 虚拟机统计信息监视工具 JVM Statistics Monitoring Tool jstat [ option vmid [interval[s|ms] [count]] jstat -gc 2764 250 20 其中option的选项: -class  -gc  -gccapacity -gcutil ...... jinfo java配置信息工具 jmap Java内在映射工具 用于生成堆转储快照 也可配置

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

1.Java语言的特性 Java代码经过编译器编译成Class文件(字节码)后,就需要虚拟机将其加载到内存里面执行字节码所定义的代码实现程序开发设定的功能. Java语言中类型的加载.连接(验证.准备.解析).初始化都是在程序运行期间内完成的与C++不同(C++在编译期就需要进行连接),这样也使得Java语言更具灵活性. 2.虚拟机类加载的过程 (1)虚拟机什么时候进行类的加载(类加载的时机) 1)遇到new.getstatic.putstatic或者invokestatic 这四条字节码指令的

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

JVM调优 - 工具 JConsole:Java监视与管理控制台 JConsole是一个机遇JMX(Java Management Extensions,即Java管理扩展)的JVM监控与管理工具,监控主要体现在:堆栈内存.线程.CPU.类.VM信息这几个方面,而管理主要是对JMX MBean(managed beans,被管理的beans,是一系列资源,包含对象.接口.设备等)的管理,不仅能查看bean的属性和方法信息,还能够在运行时修改属性或调用方法. 直接在jdk/bin目录下点击jcon

深入理解Java虚拟机之垃圾收集一

"生存还是死亡" 如何来判定对象是否存活?针对这个问题书中给出了两种算法,分别是引用计数算法和可达性分析算法 引用计数算法 该算法的思路简单并且易于实现.我们给对象中添加一个引用计数器,当有一个地方引用它时,引用计数器就加一,当引用失效时,计数器减一,当计数器为0时就说明该对象不可能再被引用. 客观的评价,该算法判定效率很高,在很多情况下都是一种不错的算法,但是,至少主流的Java虚拟机并没有采用采用这种算法.原因是该算法无法解决对象之间的循环引用问题. 什么是循环引用呢?笔者认为就是

深入理解java虚拟机---JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)(十二)

1.背景 2.为什么废弃永久代(PermGen) 3.深入理解元空间(Metaspace) 4.总结 ========正文分割线===== 一.背景 1.1 永久代(PermGen)在哪里? 根据,hotspot jvm结构如下(虚拟机栈和本地方法栈合一起了): 上图引自网络,但有个问题:方法区和heap堆都是线程共享的内存区域. 关于方法区和永久代: 在HotSpot JVM中,这次讨论的永久代,就是上图的方法区(JVM规范中称为方法区).<Java虚拟机规范>只是规定了有方法区这么个概念和

深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析

Java虚拟机深入理解系列全部文章更新中... 深入理解Java虚拟机-Java内存区域透彻分析 深入理解Java虚拟机-常用vm参数分析 深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲 深入理解Java虚拟机-如何利用JDK自带的命令行工具监控上百万的高并发的虚拟机性能 深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析 深入理解Java虚拟机-你了解GC算法原理吗 前面在学习JVM的知识的时候,一般都需要利用相关参数进行分析,而分析一般

深入理解java虚拟机第二版(四)虚拟机性能监控与故障处理工具

JDK的命令行工具(jps:虚拟机进程状况工具,jstat:虚拟机统计信息监视工具,jinfo:Java配置信息工具,jmap:Java内存映像工具,jhat:虚拟机堆转储快照分析工具,jstack:Java堆栈跟踪工具):可视化工具(JConsole,VisualVM) 一. JDK的命令行工具 jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程 jstat:JVM Statistics Monitoring Tool,用户收集HotSpot虚

深入理解java虚拟机系列(一):java内存区域与内存溢出异常

文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了开始.如果有什么错误或者遗漏,欢迎指出. 一.概述 先上一张图 这张图主要列出了Java虚拟机管理的内存的几个区域. 常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂,从上图就可以看出了.堆栈分法中所指的"栈"实际上只是虚拟机栈,或者说是虚拟机栈中的局部变量表部分.接下