在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位的:

具体的环境信息:

看起来慢的机器上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. BFC以及margin的深入探究

    BFC(Block Formatting Context) 块级格式化上下文,它是指一个独立的块级渲染区域,只有block-level Box参与,该区域拥有一套渲染规则来约束块级盒子的布局,且与区域 ...

  2. 【转】java与.net比较学习系列(3) 基本数据类型和类型转换

    原文地址:https://www.cnblogs.com/mcgrady/p/3397874.html 阅读目录 一,整数类型 二,浮点数类型 三,字符类型 四,布尔类型 五,类型转换之自动转换 六, ...

  3. linux中find工具

    find 由于find具有强大的功能,所以它的选项也很多,其中大部分选项都值得我们花时间来了解一下.即使系统中含有网络文件系统( NFS),find命令在该文件系统中同样有效,只要你具有相应的权限. ...

  4. MaperReduce实验

    目录 MaperReduce实现WordCount程序二次排序 前期准备 1. 工程结构 2. 编写自定义NewKey类 3. 编写WCMapper类 4. 编写WCReduer类 5. 编写作业描述 ...

  5. 【原】Coursera—Andrew Ng机器学习—课程笔记 Lecture 11—Machine Learning System Design 机器学习系统设计

    Lecture 11—Machine Learning System Design 11.1 垃圾邮件分类 本章中用一个实际例子: 垃圾邮件Spam的分类 来描述机器学习系统设计方法.首先来看两封邮件 ...

  6. 认识Excel并创建一个excel(网址:http://poi.apache.org/)

    需要导入的jar包: package com.huawei.excel; import java.io.FileOutputStream; import org.apache.poi.hssf.use ...

  7. H5(1)

    css布局模型 清楚了CSS 盒模型的基本概念. 盒模型类型, 我们就可以深入探讨网页布局的基本模型了.布局模型与盒模型一样都是 CSS 最基本. 最核心的概念. 但布局模型是建立在盒模型基础之上,又 ...

  8. 适配iOS10 调取系统打电话功能

    [[UIApplication sharedApplication] openURL:[NSURL URLWithString: [NSString stringWithFormat:@"t ...

  9. 2-字符串模拟- URL映射

    问题描述 试题编号: 201803-3 试题名称: URL映射 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 URL 映射是诸如 Django.Ruby on Rails 等 ...

  10. activeMQ集群搭建及高可用

    三台服务器搭建如下的集群,达到了高可用.也同时达到了负载的目的: /****************************************************************** ...