逻辑谬误_Java性能的9个谬误

逻辑谬误

Java性能被誉为“黑暗艺术”。 部分原因在于平台的复杂性,这使得在很多情况下都难以推理。 但是,从历史上看,Java性能技术也趋向于由一群民间智慧而不是应用统计和经验推理组成。 在本文中,我希望解决其中一些最令人震惊的技术童话。

1. Java很慢

在所有最过时的Java Performance谬论中,这可能是最明显的。

当然,在90年代和2000年代初期,Java有时可能会变慢。

但是从那时起,我们在虚拟机和JIT技术方面已有10多年的改进,而Java的整体性能现在却以惊人的速度Swift增长。

在六个单独的Web性能基准测试中 ,Java框架在前24名中排名第22。

JVM对配置文件的使用仅优化了常用的代码路径,而对这些代码路径进行了优化已获得了回报。 现在,在许多情况下(而且还在不断增加),JIT编译的Java代码与C ++一样快。

尽管如此,可能由于对Java平台的早期版本有经验的人的负面历史偏见,仍将Java视为慢速平台。

我们建议您保持客观,并在得出结论之前评估最新的绩效结果。

2.一行Java意味着任何孤立的事物

考虑以下短代码行:

MyObject obj =新的MyObject();

对于Java开发人员而言,很明显,此代码必须分配一个对象并运行适当的构造函数。

由此,我们可能会开始推理性能边界。 我们知道必须进行一些有限的工作,因此我们可以根据我们的假设来尝试计算性能影响。

这是一种认知上的偏见,可以使我们陷入先验的思考,即任何工作都需要完成。

实际上,javac和JIT编译器都可以优化死代码。 在使用JIT编译器的情况下,甚至可以基于分析数据来推测性地优化代码。 在这种情况下,代码行根本不会运行,因此对性能的影响为零。

此外,在某些JVM(如JRockit)中,JIT编译器甚至可以分解对象操作,从而即使代码路径未完全消失也可以避免分配。

这个故事的寓意是,在处理Java性能时上下文非常重要,过早的优化可能会产生与直觉相反的结果。 为了获得最佳结果,请勿尝试过早优化。 相反,请始终构建代码并使用性能调整技术来定位和纠正性能热点。

3.一个微基准意味着您认为它所做的

正如我们在上面看到的,推理一小段代码比分析整体应用程序性能的准确性差。

尽管如此,开发人员还是喜欢编写微基准测试。 某些人从修补平台的某些低级方面获得的内在乐趣似乎是无止境的。

理查德·费曼(Richard Feynman)曾经说过:“第一个原则是,你绝不能愚弄自己,而你是最容易愚弄的人。” 没有什么比编写Java微基准测试更真实的了。

编写好的微基准非常困难。 Java平台复杂而复杂,许多微基准只能成功地测量瞬态效应或平台的其他意外方面。

例如,幼稚的微基准测试经常会最终测量计时子系统或垃圾回收,而不是它试图捕获的效果。

只有真正需要的开发人员和团队才可以编写微基准。 这些基准应该完整地发布(包括源代码),并且应具有可复制性,并经过同行评审和深入审查。

Java平台的许多优化意味着单个运行的统计很重要。 一个基准测试必须运行多次,并汇总结果才能获得真正可靠的答案。

如果您认为必须编写微基准测试,那么最好的起点是阅读Georges,Buytaert,Eeckhout的论文“统计严格的Java性能评估”。 如果不正确处理统计信息,很容易被误导。

周围有完善的工具和社区(例如Google的Caliper)-如果您绝对必须编写微基准,那么就不要自己写微基准-您需要同行的观点和经验。

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

在开发人员(以及整个人类)中,一个非常熟悉的认知谬误是假设他们控制的系统部分是重要的部分。

在Java性能中,这由Java开发人员证明,算法质量是导致性能问题的主要原因。 开发人员会考虑代码,因此他们自然会考虑算法。

在实践中,当处理一系列实际性能问题时,发现算法设计是根本问题,不到10%的时间。

取而代之的是,垃圾回收,数据库访问和配置错误都比算法更容易导致应用程序运行缓慢。

大多数应用程序只处理相对少量的数据,因此,即使是严重的算法效率低下,也通常不会导致严重的性能问题。 可以肯定的是,我们承认算法是次优的。 尽管如此,它们相对于来自应用程序堆栈其他部分的其他更为显着的性能影响而言,它们所添加的低效程度很小。

因此,我们最好的建议是使用经验的生产数据来发现性能问题的真正原因。 测量; 不要猜!

5.缓存解决了所有问题

“计算机科学中的每个问题都可以通过添加另一个间接级别来解决”

这位程序员的格言令人惊讶地普遍存在,特别是在Web开发人员中,这种格言归因于David Wheeler(并且要归功于Internet,至少应归功于另外两名计算机科学家)。

当面对一个现有的,了解甚少的体系结构时,这种谬误通常是由于分析瘫痪引起的。

开发人员通常不会选择将其隐藏在一个令人生畏的现成系统中,而会选择在其前面隐藏一个缓存并希望达到最佳效果,从而将其隐藏起来。 当然,这种方法只会使整个体系结构复杂化,并使下一个寻求了解生产现状的开发人员的处境更糟。

庞大而庞大的体系结构一次只能写成一行,一个子系统。 但是,在许多情况下,更简单,重构的体系结构性能更高-并且几乎总是更容易理解。

因此,当您评估是否确实需要缓存时,请计划收集基本的使用统计信息(丢失率,命中率等),以证明缓存层确实在增加价值。

6.所有应用都需要关注世界停止

Java平台存在的事实是,所有应用程序线程必须定期停止以允许垃圾回收运行。 有时,即使没有任何实际证据,这也被视为严重的弱点。

经验研究表明,人类通常无法感知到每200毫秒一次发生的数字数据变化(例如价格变动)的发生频率更高。

因此,对于以人类为主要用户的应用程序,一个有用的经验法则是,通常无需关注200毫秒或更短的Stop-The-World(STW)暂停。 某些应用程序(例如流视频)需要的GC抖动要低于此抖动,但许多GUI应用程序则不需要。

在少数应用程序(例如低延迟交易或机械控制系统)中,200ms的暂停是不可接受的。 除非您的应用程序很少,否则您的用户不太可能会感觉到垃圾回收器的任何影响。

还值得一提的是,在应用程序线程多于物理内核的任何系统中,操作系统调度程序都必须进行干预以分时访问CPU。 Stop-The-World听起来很吓人,但实际上,每个应用程序(无论是否是JVM)都必须处理对稀缺计算资源的竞争访问。

如果不进行衡量,就不清楚JVM的方法是否会对应用程序性能产生任何有意义的附加影响。

总之,通过打开GC日志来确定暂停时间是否实际上在影响您的应用程序。 分析日志(手动或使用脚本或工具)以确定暂停时间。 然后确定这些是否真的对您的应用程序域造成了问题。 最重要的是,问自己一个最棘手的问题:实际上有用户抱怨吗?

7.手动滚动对象池适用于各种应用程序

对于普遍认为Stop-The-World暂停很糟糕的一种普遍React是,应用程序组在Java堆中发明自己的内存管理技术。 通常,这归结为实现对象池(甚至是完整的引用计数)方法,并要求使用域对象的任何代码都参与其中。

这种技术几乎总是被误导。 它通常起源于遥远的过去,在那儿对象分配昂贵且可变性被认为是无关紧要的。 现在世界已经大不一样了。

现代硬件的分配效率极高。 在最新的台式机或服务器硬件上,内存带宽至少为2至3GB。 这是一个很大的数字。 在专业用例之外,要使真正的应用程序饱和那么多的带宽并不容易。

通常,对象池很难正确实现(特别是在有多个线程在工作时),并且存在一些负面要求,这使其成为通用的较差选择:

  • 所有接触代码的开发人员都必须意识到池并正确处理它
  • 必须知道并记录“可识别池”和“不可识别”代码之间的边界
  • 所有这些额外的复杂性必须保持最新状态,并定期进行审查
  • 如果以上任何一个失败,则将重新引入无提示损坏的风险(类似于C中的指针重用)

总而言之,仅当GC暂停是不可接受的,并且调优和重构的智能尝试无法将暂停减少到可接受的水平时,才应使用对象池。

8.与Parallel Old相比,CMS始终是更好的GC选择

默认情况下,Oracle JDK将使用并行的世界收集器来收集旧版本。

另一种选择是并发标记扫描(CMS)。 这使应用程序线程可以在整个GC周期的大部分时间内继续运行,但是这是有代价的,并且有很多警告。

允许应用程序线程与GC线程一起运行总是会导致应用程序线程以某种影响对象活动性的方式来改变对象图。 实际上,必须对此进行清理,因此CMS实际上具有两个(通常非常短的)STW阶段。

这有几个后果:

  1. 必须将所有应用程序线程带到安全点,并在每次完整收集时停止两次;
  2. 集合同时运行时,应用程序吞吐量降低了(通常降低了50%);
  3. JVM参与通过CMS收集垃圾的簿记(和CPU周期)的总量比并行收集要高得多。

根据应用环境的不同,这些价格可能是值得的,也可能不是。 但是没有免费的午餐。 CMS收集器是一项了不起的工程,但不是万能药。

因此,在断定CMS是您正确的GC策略之前,您应该首先确定Parallel Old的STW暂停是不可接受的,无法进行调整。 最后,(而且我对此压力还不够大),请确保所有指标都是在等同于生产的系统上获得的。

9.增加堆大小将解决您的内存问题

当应用程序出现问题并且怀疑有GC时,许多应用程序组将通过增加堆大小来做出响应。 在某些情况下,这可以产生快速胜利,并留出时间进行更深思熟虑的修复。 但是,如果不完全了解性能问题的原因,此策略实际上会使情况变得更糟。

考虑一个编码错误的应用程序,该应用程序产生了太多的域对象(典型的寿命是两到三秒)。 如果分配率足够高,则垃圾收集可能会发生得如此之快,以至于将域对象提升为使用期限(旧)的代。 一旦使用权,域对象几乎立即死亡,但是直到下一次完整收集时才将它们收集。

如果此应用程序的堆大小增加了,那么我们真正要做的就是为相对短命的域对象传播并消亡提供空间。 这可能会使Stop-The-World暂停的时间变长,而对应用程序无济于事。

在更改堆大小或调整其他参数之前,了解对象分配和生存期的动态至关重要。 不加估量行事会使事情变得更糟。 来自垃圾收集器的权属分配信息在这里尤为重要。

结论

当涉及到Java性能调整时,直觉常常会引起误解。 我们需要经验数据和工具来帮助我们可视化和理解平台的行为。

垃圾收集也许提供了最好的例子。 GC子系统在调优和产生数据以指导调优方面具有令人难以置信的潜力,但是对于生产应用程序,如果不借助工具,很难理解所产生的数据。

默认值应始终是至少运行带有以下标志的任何Java进程(处于开发或生产中):
-verbose:gc(打印GC日志)
-Xloggc :(用于更全面的GC日志记录)
-XX:+ PrintGCDetails(用于更详细的输出)
-XX:+ PrintTenuringDistribution(显示JVM假定的使用期限阈值)

然后使用一种工具来分析日志-手写脚本和一些图形生成,或者使用可视工具,例如(开放源代码)GCViewer或jClarity Censum。

翻译自: https://www.infoq.com/articles/9_Fallacies_Java_Performance/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

逻辑谬误

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值