关于Java性能的9个谬论

Java的性能有某种黑魔法之称。部分原因在于Java平台非常复杂,很多情况下问题难以定位。然而在历史上还有一种趋势,人们靠智慧和经验来研究Java性能,而不是靠应用统计和实证推理。在这篇文章中,我希望拆穿一些最荒谬的技术神话。

如转载,请标明出处

1.Java很慢

关于Java的性能有很多谬论,这一条是最过时的,可能也是最为明显的。

确实,在上世纪90年代和本世纪初处,Java有时是很慢。

然而从那以后,虚拟机和JIT技术已经有了十多年的改进,Java的整体性能现在已经非常好了。

在6个独立的Web性能基准测试中,Java框架在24项测试中有22项位列前四。

尽管JVM利用性能剖析仅优化常用的代码路径,但这种优化效果很明显。很多情况下,JIT编译的Java代码和C++一样快,而且这样的情况越来越多了。

尽管如此,依然有人认为Java平台很慢,这或许源自体验过Java平台早期版本的人的历史偏见。

在下结论之前,我们建议保持客观的态度,并且评估一下最新的性能结果。

什么对象应被回收

一般地,一个对象没有被引用,说明这个对象“已死”,可以被回收。那么如何标记这个对象是否被引用?一般包含如下两种算法:

  • 引用计数算法
    对象被引用一次,就将引用计数器加1;相反,就减1。这种算法简单且效率高,但是无法解决循环引用的问题。
  • 可达性分析算法
    Java中采用的是这种算法。从一系列被称为“GC
    Roots”的对象为起点开始寻找,当一个对象到GC
    Roots
    无任何引用链时,这个对象就可视为不可达的,因此可以回收。
    GC Roots主要存在全局性的引用和执行上下文中:
  • 线程栈引用的对象
  • 本地方法栈引用的对象
  • 类静态属性引用的对象
  • 常量引用的对象

2.可以孤立地看待单行Java代码

考虑下面这行短小的代码:

MyObject obj = new MyObject();

对Java开发者而言,看似很明显,这行代码一定会分配一个对象并调用适当的构造器。

我们也许可以据此推出性能边界了。我们认为这行代码一定会导致执行一定量的工作,基于这种推定,就可以尝试计算其性能影响了。

其实这种认识是错误的,它让我们先入为主地认为,不管什么工作,在任何情况下都会进行。

事实上,javac和JIT编译器都能够将死代码优化掉。就JIT编译器而言,基于性能剖析数据,甚至可以通过预测将代码优化掉。在这样的情况下,这行代码根本不会运行,所以不会影响性能。

此外,在某些JVM中——比如JRockit——JIT编译器甚至可以将对象上的操作分解,这样即便代码路径还有效,分配操作也可以避免。

这里的寓意是,在处理Java性能问题时,上下文非常重要,过早的优化有可能产生违反直觉的结果。所以最好不好过早优化。相反,应该总是构建代码,并且使用性能调校技术来定位性能热点,然后加以改进。

垃圾回收算法

算法名称 特点 说明
标记-清除(Mark-Sweep) 效率不高;产生内存碎片 一般老年代使用
复制算法 没有内存碎片;但得预留一部分内存;需要依赖其他内存(分配担保) 一般新生代使用;
标记-整理(Mark-Compact) 一般老年代使用
分代收集算法

3.微基准测试和你想象的一样

正如我们上面看到的那样,检查一小段代码不如分析应用的整体性能来的准确。

尽管如此,开发者还是喜欢编写微基准测试。似乎对平台底层的某些方面进行修修补补会带来无穷的乐趣。

理查德·费曼曾经说过:“不要欺骗自己,你自己正是最容易被欺骗的人。”这句话用来说明编写Java微基准测试这件事是再合适不过了。

编写良好的微基准测试极其困难。Java平台非常复杂,而且很多微基准测试只能用于测量瞬时效应,或是Java平台的其他意想不到的方面。

例如,如果没有经验,编写的微基准测试往往就是测一下时间或垃圾收集,却没有抓住真正的影响因素。

只有那些有实际需求的开发者和开发团队才应该编写微基准测试。这些基准测试应该完全公开(包括源代码),而且是可以复现的,还应接受同行评审及进一步的审查。

Java平台的很多优化表明统计运行和单次运行对结果影响很大。要得到真实可靠的答案,应该将一个单独的基准测试运行多次,然后把结果汇总到一起。

如果读者感觉有必要编写微基准测试,Georges、Buytaert和Eeckhout等人的论文《利用严格的统计方法评测Java
性能(Statistically Rigorous Java Performance
Evaluation)》是个不错的开始。缺乏适当的统计分析,我们很容易被误导。

有很多开发好的工具以及围绕这些工具的社区(比如Google的Caliper)。如果确实有必要编写微基准测试,那也不要自己编写,这时需要的是同行的意见和经验。

HotSpot算法

4.算法慢是性能问题的最常见原因

在开发者之间有一个很常见的认知错误(普通大众也是如此),即认为系统中他们控制的那部分很重要。

在探讨Java性能时,这种认知错误也有所体现:Java开发者认为算法的质量是性能问题的主要原因。开发者考虑的是代码,因此他们自然会偏向于考虑自己的算法。

实际上在处理一系列现实中的性能问题时,人们发现算法设计是根本问题的几率不足10%。

相反,与算法相比,垃圾收集、数据库访问和配置错误导致应用程序缓慢的可能性更大。

大部分应用处理的数据量相对较小,因此,即使主要算法效率不高,通常也不会导致严重的性能问题。可以肯定,我们的算法不是最优的;尽管如此,算法带来的性能问题还是算小的,更多性能问题是应用栈的其他部分导致的。

因此我们的最佳建议是,使用实际生产数据来揭开性能问题的真正原因。要测量性能数据,而不是凭空猜测!

Stop-The-World

Java是通过GC
Roots来寻找标记不可达的对象,因此,在这个过程中,用户的线程是不能运行的,否则标记的不可达对象是不精确的,这种停顿所有用户线程而且必须停顿的过程形象得称为”Stop-The-World”,简称”GC停顿”。好的垃圾收集器的指标之一就是缩小GC停顿的时间。
为了加快定位程序中哪些地方存在对象引用,HotSpot定义了一个称为OopMap(Ordinary
Object
Pointer)的结构来预先存储这些位置,这样垃圾收集器就能更快得标记出不可达对象。

5.缓存可以解决所有问题

“计算机科学中的所有问题都可以通过引入一个中间层来解决。”

澳门新葡亰手机版,David Wheeler的这句程序员格言(在互联网上,这句话至少还被认为是其他两位计算机科学家说的)非常常见,尤其是在Web开发者之中很流行。

如果未能透彻理解现有的架构,而且分析也已停顿,往往就是“缓存可以解决所有问题”这种谬论抬头的时候了。

在开发者看来,与其处理吓人的现有系统,还不如在前面加一层缓存,将现有系统隐藏起来,以此期待最好的情况。无疑,这种方式只是让整体架构更复杂了,当下一个接手的开发者打算了解系统现状时,情况会更糟糕。

规模庞大、设计拙劣的系统往往缺乏整体的设计,是一次一行代码、一个子系统这样写出来的。然而很多情况下,简化并重构架构会带来更好的性能,而且几乎总是更容易让人理解。

所以当评估是否真的有必要加入缓存时,应该先计划收集一些基本的使用统计信息(比如命中率和未命中率等),以此证明缓存层带来的真正价值。

Safepoint & Saferegion

一般情况下,程序中存在引用的地方是很多的,如果每个地方都存入OopMap,GC的成本也会很高。因此只在某些特定的位置生成OopMap,这些位置称为Safepoint
在Safepoint,对象的引用链该改变的已经改变,暂时不会再发生变化,比如方法调用,循环跳转等;当一个线程运行到了一个方法内,那么此时不应该让这个用户线程停顿(因为方法执行后,引用可能发生变化,之前不可达,方法执行后又可达了),此时就可以把方法结束的点称为Safepoint。
在主动式中断中,当发生GC时,不是直接停顿用户线程,而是设置一个标记,当用户线程运行到Safepoint时,就主动去查看这个标记,若发现了停顿标记,就挂起自己;否则继续运行。不同的垃圾回收器定义的Safepoint可能不同。

Safepoint完美得解决了运行中的线程停顿问题,但是不运行的线程,比如sleep的线程就无法响应JVM的中断请求。因此就提出了Saferegion
Saferegion中的代码可能无法响应JVM中断请求,但是引用关系也不会改变,所以,这区域的代码并不会影响GC
Roots枚举的结果。JVM 发起GC时就可以忽略这些进入到Saferegion的线程了。
当一个线程离开Saferegion时,要检查是否完成了GC
Roots枚举或GC过程,若没有完,则不能离开直到上述过程结束。