在本文中,我将介绍一些Java性能优化提示。我将特别看看你的Java程序中的某些操作。这些提示只适用于特定的高性能场景,因此不需要在此方法中编写所有代码,因为速度的差异将很小。然而,在热代码路径中,它们可能会产生很大的差异。
使用Profiler!
在执行任何优化之前,任何开发人员必须做的第一项任务是检查他们对性能的假设是否正确。也许他们认为的部分代码是缓慢的部分,实际上掩盖了真正的缓慢的部分,从而导致任何改进的影响都可以忽略不计。他们还必须有一个比较点,才能知道他们的改进是否改善了任何事情,如果是这样,多少。
实现这两个目标的最简单方法是使用分析器。分析器将为您提供工具,以查找代码的哪一部分实际上是缓慢的,以及它花费了多长时间。我可以推荐的一些profiler是VisualVM(免费)和JProfiler(付费 - 完全值得)。
掌握了这些知识,您可以放心,您正在优化代码的正确部分,并且您的更改具有可衡量的效果。
回顾一下思考问题的方法
在尝试微型优化特定代码路径之前,值得考虑的是目前的方法。有时,根本的方法可能是有缺陷的,即使你付出了巨大的努力并且通过执行所有可能的优化使其运行速度提高了25%,改变方法(使用更好的算法)可能会导致数量级或更多的性能增加。当数据的规模变化时,经常会发生这种情况 - 现在编写一个能够很好地解决问题的解决方案很简单,但是当你获得真实的数据时,它就开始崩溃了。
有时这可能会像您更改存储数据的数据结构一样简单。要使用一个有创意的示例,如果您的数据访问模式大多是随机访问,并且使用LinkedList,则只需切换到ArrayList即可。速度提升。对于大数据集和性能敏感的工作,至关重要的是您选择数据形状和正在执行的操作的正确数据结构。
总是值得回顾一下,考虑您正在优化的代码是否已经有效,并且由于写入速度慢,还是因为采用的方法是次优的,是否缓慢。
Streams API与Trusty For Loop
流是Java语言的一个很好的补充,让您轻松地将错误的模式从循环提升为通用的,更可重用的代码块,并具有一致的保证。但这种方便不是免费的; 存在与使用流相关联的性能成本。值得庆幸的是,这个成本似乎并不算太高 - 通常的操作速度要慢几十个百分点到10-30%,但这是值得注意的。
99%的使用Streams的性能损失超过了代码的明确度增加所致。但是,对于那个1%的时间,也许你正在热循环中使用流,值得注意的是性能折衷。对于任何非常高吞吐量的应用程序尤其如此,来自流API的内存分配增加(根据此StackOverflow 文章,每个过滤器增加了88个字节的已用内存)可能会导致足够的内存压力,从而需要更频繁的GC运行,从而导致重的击中性能。
并行流是另一回事,尽管它们易于使用,但它们只能在罕见情况下使用,只有在对并行操作和串行操作进行分析以确认并行操作实际上更快之后。在较小的数据集(流计算的成本决定什么构成较小的数据集)中,分割工作的成本,在其他线程上进行调度,并在处理流后将其重新拼接在一起会使运行速度加快并行计算
您还必须考虑代码运行的执行环境的类型,如果它正在运行已经很大的并行化环境(例如网站),那么您甚至不可能并行加速运行流。实际上,在负载下,这可能比非并行执行更糟。这是因为工作负载的并行性很有可能已经尽可能地使用剩余的CPU内核,这意味着您可以在不增加可用计算能力的情况下支付分割数据的成本。
我执行的基准测试样本。这testList
是一个10万个元素数组,数字1到100,000转换成String,混洗。
总而言之,流是对代码维护和可读性的巨大胜利,对绝大多数情况的性能影响可以忽略不计,但是您应该意识到,在极少数情况下,您真的需要将额外的性能从一个紧循环。
日期运输和操纵
不要低估将日期字符串解析为日期对象的成本,并将日期对象格式化为日期字符串。想象一下,您有一个百万个对象的列表(直接的字符串或表示某个项目的对象,其中的一个日期字段由字符串支持),您必须对它们的日期进行调整。在这个日期被表示为字符串的上下文中,您首先必须将其从该字符串解析为Date对象,更新Date对象,然后将其格式化为字符串。如果日期已经表示为Unix时间戳(或Date对象,因为它实际上只是一个Unix时间戳的包装),所有您需要做的是执行简单的加法或减法操作。
根据我的测试结果,只需操作日期对象比起必须从/解析并将其格式化到字符串的速度高出500倍。即使切出解析步骤也可以加快〜100倍。这可能看起来像一个有创意的例子,但我相信你已经看到日期被存储在数据库中的字符串或作为API响应中的字符串返回的情况。
// 〜800,000 op / s public void dateParsingWithFormat (DateState state) throws ParseException { Date date = (“20-09-2017 00:00:00”); date = new Date(da()+ 24 * ); (日期);}// 〜3,200,000 op / s public void dateLongWithFormat (DateState state) { long newTime = + 24 * ; (new Date(newTime));}// 〜400,000,000 op / s public long dateLong (DateState state) { long newTime = + 24 * ; 返回 newTime;}总而言之,始终意识到解析和格式化日期对象的成本,除非您需要正确的字符串,否则将其表示为Unix时间戳更好。
字符串操作
字符串操作可能是任何程序中最常见的操作之一。然而,如果错误地执行,它可能是一个昂贵的操作,这就是为什么我在这些Java性能优化提示中专注于字符串操作。我会列出下面的一些常见的陷阱。不过,我想指出,这些问题只能表现在非常快的代码路径或相当数量的字符串中,99%的情况下,以下任何一个都不重要。但是当他们这样做的时候,他们可以成为一个表演杀手。
使用S时,一个简单的连接就可以了
一个非常简单的S调用的速度比将手动连接到字符串中慢100倍。大多数时候这是很好的,因为我们还在谈论我的机器上大约100万次操作p / s,但是在数百万元素的紧缩循环中,性能的损失可能会很大。
在那里你一个实例应该使用字符串格式化而不是串联在高性能环境中,但是,调试日志记录。进行以下两次调试日志调用:
记录仪。debug(“的值为:” + x);logger.debug("the value is: %d", x);
起初似乎反直觉的第二个实例在生产环境中可能更快。由于不太可能,您将在生产服务器上启用调试日志记录,所以首先会导致一个新的字符串被分配,然后再也不会使用(因为日志永远不会被输出)。第二个需要加载常量字符串,然后将跳过格式化步骤。
//〜1,300,000 op / s public String stringFormat(){ String foo = “foo” ; String formattedString = String .format(“%s =%d”,foo,2); return formattedString;}//〜115,000,000 op / s public String stringConcat(){ String foo = “foo” ; String concattedString = foo + “=” + 2 ; return concattedString;}不在循环中使用字符串构建器
如果您不在循环中使用字符串构建器,则会抛出很多潜在的性能。追加到循环中的字符串的天真的实现将是使用+ =将新的字符串部分附加到旧的字符串。这种方法的问题是它会导致每循环循环中分配一个新的字符串,并且需要将旧的字符串复制到新的字符串中。这是一个昂贵的操作,在自我,甚至没有额外的垃圾收集压力考虑到创建和丢弃这么多的字符串。使用StringBuilder将限制内存分配的数量,导致大的性能加速。在我使用StringBuilder的测试中,导致加速大于500x。如果在构造StringBuilder时可以至少对结果字符串的大小有一个很好的猜测,则设置正确的大小(这意味着内部缓冲区不需要调整大小,从而导致每次分配和复制)可能导致进一步10%加速。
另外要注意,(几乎)总是使用StringBuilder而不是StringBuffer。StringBuffer设计用于在多线程环境中使用,因此具有内部同步,即使仅在单线程环境中使用,执行同步的成本也必须支付。如果你需要从多个线程附加一个字符串(比如在一个日志记录实现中),那就是使用StringBuffer而不是StringBuilder的几种情况之一。
// 〜11操作p / s public String stringappendLoop () { String s = “” ; for(int i = 0 ; i < 10 _000; i ++){ if(s.length()> 0)s + = “,” ; s + = “bar” ; } 返回 S;}// 〜7,000操作p / s public String stringAppendBuilderLoop () { StringBuilder sb = new StringBuilder(); for(int i = 0 ; i < 10 _000; i ++){ if(()> 0)(“,”); (“bar”); } return ();}在循环外使用StringBuilder
这是我在互联网上推荐的东西,似乎是有道理的。但是我的测试表明,实际上比使用+ =使用StringBuilder的速度要慢3倍 - 即使不在循环中。即使在这个上下文中使用+ =被javac转换成StringBuilder调用,似乎比直接使用StringBuilder要快得多,这让我感到惊讶。
如果有人知道为什么会这样,我很乐意在评论中听到。
//〜20,000,000次操作p / s public String stringAppend(){ String s = “foo” ; s + = “,bar” ; s + = “,baz” ; s + = “,qux” ; s + = “,bar” ; s + = “,bar” ; s + = “,bar” ; s + = “,bar” ; s + = “,bar” ; s + = “,bar” ; s + = “,baz” ; s + = “,qux” ; s + = “,baz” ; s + = “,qux” ; s + = “,baz” ; s + = “,qux” ; s + = “,baz” ; s + = “,qux” ; s + = “,baz” ; s + = “,qux” ; s + = “,baz” ; s + = “,qux” ; 返回 S;}//〜7,000,000次操作p / s public String stringAppendBuilder(){ StringBuilder sb = new StringBuilder(); SB。append(“foo”); SB。append(“,bar”); SB。append(“,bar”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); SB。append(“,baz”); SB。append(“,qux”); return ();}总而言之,字符串创建具有明确的开销,应尽可能避免在循环中。这可以通过在循环内使用StringBuilder轻松实现。
我希望这篇文章为您提供了一些有用的Java性能优化提示。再次,我想强调,这篇文章中的所有信息对大多数代码执行并不重要,如果您可以将字符串格式化为每秒100万次,或者每秒8000万次,则无任何差异只做了几次。但是在这些关键的热门路线中,您可能实际上要做好几百万次,因为80倍的加速可以节省长时间工作的时间。
本文只是深入了解优化Java应用程序以实现高性能的深层次。
我附上了一个zip文件,其中包含了我用来写这篇文章的所有基准和数据,看看下面的完整基准运行的输出。所有这些结果都在带有i5-6500的桌面上运行。该代码在Windows 10上使用JDK 1.8.0_144,VM 25.144-b01运行。
基准 模式Cnt分数误差单位Da thrpt 10 398,180,671。307 ± 861095。156 ops / sDaWithFormat thrpt 10 3,205,290。937 ± 17495。643 ops / thrpt 10 829,339。030 ± 5872。319 ops / thrpt 10 1,676。135 ± 33。174 ops / InLoop thrpt 10 543。856 ± 2。759 ops / thrpt 10 95。626 ± 0。480 ops / Parallel thrpt 10 128。419 ± 1。185 ops / thrpt 10 1,637。747 ± 11。793 ops / InLoop thrpt 10 432。207 ± 1。098 ops / Parrallel thrpt 10 8,068。937 ± 260。249 ops / thrpt 10 22,572,323。301 ± 181309。750 ops / Builder thrpt 10 6,983,217。796 ± 54212。734 ops / BuilderLoop thrpt 10 7,202。648 ± 117。500 ops / BuilderLoopSized thrpt 10 8,106。539 ± 135。040 ops / Loop thrpt 10 11。111 ± 0。373 ops / LoopPlus thrpt 10 11。624 ± 0。056 ops / LoopPlusDouble thrpt 10 11。593 ± 0。084 ops / thrpt 10 11,6474,840。767 ± 766799。557 ops / thrpt 10 6,404。057 ± 97。817 ops / thrpt 10 1,384,875。999 ± 7025。733 ops / s1、具有1-5工作经验的,面对目前流行的技术不知从何下手,
需要突破技术瓶颈的。2、在公司待久了,过得很安逸,
但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的。
3、如果没有工作经验,但基础非常扎实,对java工作机制,
常用设计思想,常用java开发框架掌握熟练的。
4、觉得自己很牛B,一般需求都能搞定。
但是所学的知识点没有系统化,很难在技术领域继续突破的。
5. 群号:高级架构群 606187239备注好信息!
6.阿里Java高级大牛直播讲解知识点,分享知识,
多年工作经验的梳理和总结,带着大家全面、
科学地建立自己的技术体系和技术认知!