美文网首页
让你月薪飙升的秘籍:Java性能调优的9个实用技巧

让你月薪飙升的秘籍:Java性能调优的9个实用技巧

作者: 37f08a1d07fb | 来源:发表于2018-11-05 17:47 被阅读44次

    现实里可能没有完美无缺的代码。如果有,那么,过来,我写一段代码给你看。

    Java已经成为了编程语言的骄子。Java 技术具有卓越的通用性、高效性、平台移植性和安全性,广泛应用于PC、数据中心、游戏控制台、科学超级计算机、移动电话和互联网,越来越多的企业在数据结构、算法分析、软件开发等研究设计时,都选择以Java语言作为载体。这说明Java语言已经是人们构建软件系统时主要使用的一种语言。如何让Java程序运行是一回事,而如何让它们跑的快又是另外一回事了......

    下面我整理了一些Java性能调优的一些技巧,在此和大家浅浅的交流一下。

    Java性能优化的重要性:

    代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。

    代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的。

    代码优化的目标是:

    减小代码的体积

    提高代码运行的效率

    在我们分享基于Java的性能调优技巧之前,让我们先讨论一下这些通用的性能调优技巧。

    通用性能调优的4个实用技巧

    1. 在必要之前,先不要优化

    这可能是最最重要的性能调优技巧之一。你应该遵循常见的最佳实践,并尝试有效地实现你的用例。但这并不意味着在证明它是必要之前,替换任何标准库或构建复杂的优化。

    在大多数情况下,过早的优化占用了大量的时间,使得代码难以读取和维护。更糟糕的是,这些优化通常不会带来任何好处,因为你花费了大量时间来优化应用程序的非关键部分。

    那么,你如何证明你需要优化某些东西呢?

    首先,你需要确定应用程序代码的速度,例如,为所有API调用指定一个最大响应时间,或者指定在特定时间范围内导入的记录数量。完成之后,你可以度量应用程序的哪些部分太慢而需要改进。当这样做之后,那么请继续看第二个调优技巧。

    2. 使用分析器来找到真正的瓶颈

    在你遵循第一条建议,并确定你的应用程序的某些部分的确需要改进之后,问自己从哪里开始?

    你可以用两种方法来解决这个问题:

    你可以看一下你的代码,从看起来可疑或者你觉得它可能会产生问题的部分开始。

    或者使用分析器,获取代码中每个部分的行为和性能的详细信息。

    至于为什么应该总是遵循第二种方法。

    答案应该很明显,基于分析器的方法能让你更好地理解代码的性能含义,并允许你关注最关键的部分。如果你曾经使用过分析器,你将会惊讶于代码的哪些部分造成了性能问题。然而,很多时候,你的第一次猜想会把你引向错误的方向。

    3. 为整个应用程序创建性能测试套件

    这是另一个帮助你避免许多意想不到问题的一般技巧,这些问题通常发生在性能改进部署到生产环境之后。你应该经常定义测试整个应用程序的性能测试套件,并在你完成性能改进之前和之后运行它。

    这些额外的测试运行将帮助你识别更改的功能和性能方面的影响,并确保你不会发布一个弊大于利的更新。如果你的任务运行于应用程序的多个不同部分比如数据库或缓存,这一点尤其重要。

    4. 首先解决最大的瓶颈问题

    在创建了测试套件并使用分析器对应用程序进行分析之后,你就有了一个需要提高性能的问题列表,这很好,但它仍然不能回答你应该从哪里开始的问题。你可以从那些可以快速搞定的开始,亦或者从最重要的问题开始。

    当然前者很诱人,因为这很快就能出结果。有时,可能需要说服其他团队成员或你的管理层,性能分析是值得的。

    但总的来说,我建议首先着手处理最重要的性能问题。这将为你提供最大的性能改进,而且你可能只需要修复这些问题中的几个就可以解决你的性能需求。

    在了解通用性能调优技巧之后,让我们再来仔细看看一些特定于Java的调优技巧。

    Java性能调优的5个技巧

    1. 使用 StringBuilder

    几乎所有Java代码中你都应该考虑这个问题。避免使用+号。你可能会认为 StringBuilder 只是个语法糖,比如:

    String x = "a" + args.length + "b";

    会编译成

    但是之后你需要根据条件来修改字符串,会发生什么事情呢?

    你现在会有第二个 StringBuilder,这个 StringBuilder 本来没有存在的必要,它会消耗堆内存,给 GC 增加负担。你应该这样写:

    2. 避免正则表达式

    正则表达式相对便宜和方便。但是如果你在 N.O.P.E 分支 ,那很糟糕了。如果你必须在计算机密集的代码段中使用正则表达式,至少把 Pattern 的引用缓存下来,避免每次都对其重新编译:

    static final Pattern HEAVY_REGEX =

    Pattern.compile("(((X)*Y)*Z)*");

    但是如果你的正则表达式真的很简单,就像

    String[] parts = ipAddress.split("\\.");

    然后你真的最好诉诸普通的 char[] 或基于索引的操作。例如下面一段代码做了同样的事情:

    这也说明了为什么你不应该过早进行优化。与 split() 的版本相比,这简直不可维护。

    正则表达式很有用,但需要代价。如果你在 N.O.P.E 分支 ,就必须避免正则表达式的代价。

    3. 不要使用 iterator()

    这个建议不太适用于常规用例,只适用于 N.O.P.E. 分支,但你也可以用用看。编写 Java-5 风格的 foreach 循环很方便。 你可以完全忽略循环内部变量,并编写:

    for (String value : strings) {

    // Do something useful here}

    然而,每当你运行到循环内部时,如果 string 是一个 Iterable,你就要创建一个新的 Iterator 实例。如果你正在使用 ArrayList,这将会在堆上分配一个含 3 个 int 的对象:

    private class Itr implements Iterator<E> {

    int cursor;

    int lastRet = -1;

    int expectedModCount = modCount;

    // ...

    相反,你可以编写以下代码——等价循环体,并且在栈上仅“浪费”一个 int 值,开销低:

    int size = strings.size();for (int i = 0; i < size; i++) {

    String value : strings.get(i);

    // Do something useful here}

    … 或者,你可以选择不改变链表,在数组版本上使用同样的操作:

    for (String value : stringArray) {

    // Do something useful here}

    关键点

    从可写性和可读性以及从 API 设计的角度来看,Iterators、Iterable 和 foreach 循环都是非常有用的。但它们在堆上为每次单独的迭代创建一个小的新实例。 如果你运行这个迭代许多次,又想避免创建这个无用的实例,可以使用基于索引的迭代。

    4. 不要调用这些方法

    一些方法简单但开销不小。在N.O.P.E.分支示例中,我们没有在叶节点上使用这样的方法,但你可能使用到了。我们假设 JDBC 驱动程序需要耗费大量资源来计算 ResultSet.wasNull() 的值。你可能会用下列代码开发 SQL 框架:

    if (type == Integer.class) {

    result = (T) wasNull(rs,

    Integer.valueOf(rs.getInt(index)));

    }

    // And then...static final <T> T wasNull(ResultSet rs, T value) throws SQLException {

    return rs.wasNull() ? null : value;

    }

    此处逻辑每次都会在你从结果集中获得一个 int 之后立即调用 ResultSet.wasNull()。但getInt() 的约定是:

    返回: 列的数目;如果这个值是 SQL NULL,这个值将返回 0。

    因此,对上述问题的简单但可能有效的改进将是:

    static final <T extends Number> T wasNull(

    ResultSet rs, T value

    ) throws SQLException {

    return (value == null ||

    (value.intValue() == 0 && rs.wasNull()))

    ? null : value;

    }

    因此,这不需要过多考虑。

    关键点

    不要在算法的“叶节点”中调用开销昂贵的方法,而是缓存该调用,或者如果方法规约允许则规避之。

    5. 使用基本类型和栈

    上面的例子大量使用了泛型。泛型会强制对 byte、short、int 和 long 这些类型进行装箱 —— 至少在这之前:泛型会在 Java 10 和 Valhalla 项目中实现专业化。不过现在你的代码里并没实现这种约束,所以你得采取措施:

    // Goes to the heapInteger i = 817598;

    … 替换为下面这个:

    // Stays on the stackint i = 817598;

    如果你使用数组的话,情况不太妙:

    // Three heap objects!Integer[] i = { 1337, 424242 };

    … 替换成这个:

    // One heaphttp://object.int[] i = { 1337, 424242 };

    关键点

    当你在深入 N.O.P.E. 分支时,要小心使用装箱类型。你可能会给 GC 制造很大的压力,因为它必须一直清理你的烂摊子。

    有一个特别有效的办法对此进行优化,即使用某些基本类型,并为它创建一个巨大的一维数组,以及相应的定位变量来明确指出编码后的对象放在数组的哪个位置。

    LGPL 授权的 trove4j 库实现了基本数据类型的集合,它看起来比 int[] 要好些。

    总结:

    正如你所看到的,提高应用程序的性能有时不需要做大量的工作。这篇文章中的大多数建议,其实只需要稍微的努力就可以将它们应用到代码中。

    但通常最重要的建议是很编程语言无关的:

    在你知道有必要之前,不要优化

    使用分析器来找到真正的瓶颈

    首先解决最大的瓶颈问题

    ​欢迎加入  51软件测试大家庭,在这里你将获得【最新行业资讯】,【免费测试工具安装包】,【软件测试技术干货】,【面试求职技巧】... 51与你共同学习,一起成长!期待你的加入: QQ                      群:                     755431660 

    相关文章

      网友评论

          本文标题:让你月薪飙升的秘籍:Java性能调优的9个实用技巧

          本文链接:https://www.haomeiwen.com/subject/iozsxqtx.html