现实里可能没有完美无缺的代码。如果有,那么,过来,我写一段代码给你看。
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 heap http://object.int[] i = { 1337, 424242 };
关键点
当你在深入 N.O.P.E. 分支时,要小心使用装箱类型。你可能会给 GC 制造很大的压力,因为它必须一直清理你的烂摊子。
有一个特别有效的办法对此进行优化,即使用某些基本类型,并为它创建一个巨大的一维数组,以及相应的定位变量来明确指出编码后的对象放在数组的哪个位置。
LGPL 授权的 trove4j 库实现了基本数据类型的集合,它看起来比 int[] 要好些。
总结:
正如你所看到的,提高应用程序的性能有时不需要做大量的工作。这篇文章中的大多数建议,其实只需要稍微的努力就可以将它们应用到代码中。
但通常最重要的建议是很编程语言无关的:
在你知道有必要之前,不要优化
使用分析器来找到真正的瓶颈
首先解决最大的瓶颈问题
欢迎加入 51软件测试大家庭,在这里你将获得【最新行业资讯】,【免费测试工具安装包】,【软件测试技术干货】,【面试求职技巧】... 51与你共同学习,一起成长!期待你的加入: QQ 群: 755431660