关于Java性能的9个谬论(2)

发表于:2013-05-14来源:InfoQ作者:Ben Evans点击数: 标签:性能
所以当评估是否真的有必要加入缓存时,应该先计划收集一些基本的使用统计信息(比如命中率和未命中率等),以此证明缓存层带来的真正价值。 6.所有应

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

  6.所有应用都需要关注Stop-The-World问题

  Java平台存在一个无法改变的事实:为运行垃圾收集,所有应用线程必须周期性停顿。有时这被当作Java的一个严重缺点,即使没有任何真凭实据。

  实证研究表明,如果数字数据(如价格波动)变化的频率超过200毫秒一次,人就无法正常感知了。

  应用主要是给人用的,因此我们有一个有用的经验法则,200毫秒或低于200毫秒的Stop-The-World(STW)通常是没有影响的。有些应用可能有更高的要求(如流媒体),但很多GUI应用是不需要的。

  少数应用(比如低延迟交易或机械控制系统)无法接受200毫秒的停顿。除非编写的就是这类应用,否则用户基本感觉不到垃圾收集器的影响。

  值得一提的是,在应用线程数量超过物理核数的任何系统中,操作系统必须控制对CPU的分时访问。Stop-The-World听着可怕,但实际上任何应用(不管是JVM还是其他应用)都要面对稀缺计算资源的争用问题。

  如果不去测量,JVM对应用性能有何附加影响是不清楚的。

  总之,请打开GC日志,以此来确定停顿时间是否真的影响了应用。通过分析日志来确定停顿时间,这里既可以手工分析,也可以利用脚本或工具分析。然后再判定它们是否真的给应用于带来了问题。最重要的是,问自己一个关键的问题:确实有用户抱怨吗?

  7.手写对象池适合一大类应用

  认为Stop-The-World停顿在某种程度上是不好的,应用开发团队的一个常见反应就是在Java堆内实现自己的内存管理技术。这往往会归结为实现一个对象池(甚至是全面的引用计数),而且需要使用了领域对象的任何代码都参与进来。

  这种技术几乎总是具有误导性的。它基于过去的认知,那时对象分配非常昂贵,而修改对象则廉价的多。现在的情况已经完全不同了。

  现在的硬件在分配时非常高效;最新的桌面或服务器硬件,内存带宽至少是2到3GB。这是一个很大的数字,除非专门编写的应用,否则要充分利用这么大的带宽还真不容易。

  一般来说,正确实现对象池非常困难(尤其是有多个线程工作时),而且对象池还带来了一些负面的要求,使这种技术不是一个通用的良好选择:

  所有接触到对象池代码的开发者必须了解对象池,而且能正确处理

  哪些代码知道对象池,哪些代码不知道对象池,其界限必须让大家知道,并且写在文档中

  这些额外的复杂性要保持更新,而且定期复审

  如果有一条不满足,悄然出现问题(类似于C 中的指针复用)的风险就又回来了

  总之,只有GC停顿不能接受,而且调校和重构也未能将停顿减小到可以接受的水平时,才能使用对象池。

  8.在垃圾收集中,相对于Parallel Old,CMS总是更好的选择

  Oracle JDK默认使用一个并行的Stop-The-World收集器来收集老年代,即Parallel Old收集器。

  Concurrent-Mark-Sweep (CMS)是一个备选方案,在大部分垃圾收集周期,它允许应用线程继续运行,但这是有代价的,而且有一些注意事项。

  允许应用线程与垃圾收集线程一起运行,不可避免地带来一个问题:应用线程修改了对象图,可能会影响对象的存活性。这种情况必须在事后加以清理,因此CMS实际上有两个STW阶段(通常非常短)。

  这会带来一些后果:

  必须将所有应用线程带到安全点,每次Full GC期间会停顿两次;

  尽管垃圾收集与应用同时执行,但应用的吞吐量会降低(通常是50%);

  在使用CMS进行垃圾收集时,JVM所用的簿记信息(和CPU周期)远高于其他的并行收集器。

  这些代价是不是物有所值,取决于应用的情况。但是天下没有免费的午餐。CMS收集器在设计上值得称道,但它不是万能的。

  所以在确定CMS是正确的垃圾收集策略之前,首先应该确认Parallel Old的STW停顿确实不能接受,而且已经无法调校。最后,我重点强调一下,所有指标必须从与生产系统等价的系统中获得。

  9.增加堆的大小可以解决内存问题

  当应用陷入困境,并且怀疑是GC的问题时,很多应用团队的反应就是增加堆的大小。在某些情况下,这样做可以快速见效,而且为我们留出了时间来考虑更周详的解决方案。然而,如果没有充分理解性能问题的原因,这种策略反而会让事情变得更糟糕。

  考虑一个编码非常糟糕的应用程序,它正在产生很多领域对象 (它们的生存时间很有代表性,比如说是2-3秒)。如果分配率高到一定程度,垃圾收集会频繁进行,这样领域对象会被提升到老年代。领域对象几乎是一进入年老代,生存时间就结束了,从而直接死亡,但它们直到下一次Full GC时才会被回收。

  如果增加了应用的堆大小,我们所做的不过是增加了相对短命的对象进入和死亡所用的空间。这会导致Stop-The-World停顿时间更长,对应用并无益处。

  在修改堆大小或者调校其他参数之前,理解对象的分配和生存时间的动态是很有必要的。没有测量性能数据就盲目行动,只会使情况更糟糕。在这里,垃圾收集器的老年代分布情况特别重要。

  结论

  当谈到Java的性能调校时,直觉常常起误导作用。我们需要实验数据和工具来帮助我们将平台的行为可视化并加强理解。

  垃圾收集就是最好的例子。对于调校或者生成指导调校的数据而言,GC子系统拥有无限的潜力;但是对于产品应用而言,不使用工具很难理解所产生数据的意义。

  默认情况下,运行任意Java进程(包括开发环境和产品环境),应该至少总是使用如下参数:

  -verbose:gc(打印GC日志)

  -Xloggc:(更全面的GC日志)

  -XX:+PrintGCDetails(更详细的输出)

  -XX:+PrintTenuringDistribution(显示JVM所使用的将对象提升进入老年代的年龄阈值)

  然后使用工具来分析日志,这里可以利用手写的脚本,可以用图生成,还可以使用GCViewer(开源的)或jClarity Censum这样的可视化工具。

原文转自:http://www.infoq.com/cn/articles/9_Fallacies_Java_Performance