Java应用程序,不管他们部署到的应用程序服务器,往往会遇到同样的问题集。作为Java EE调谐器,我已经暴露于各种环境,并对常见问题提出了一些意见。在这方面,我看到我的角色与汽车修理师的角色相似:你告诉你的技工,引擎是啁啾的; 那么他会问你一系列问题,指导你量化啁啾的性质,位置和情况。从这些信息中,他形成了一个关于问题的一些可能原因的好主意。
我会分享我在现场遇到的一些常见问题及其症状。这些步骤将使我们能够使代码运行顺利并避免性能问题。
1)使用StringBuilder
这应该是几乎所有Java代码中的默认值。尽量避免+运算符。当然,你可能会认为这只是StringBuilder的语法糖,如下所示:
String x=“a” + args.length + “b” ;
...编译
0 新 的java .lang .StringBuilder [16]
3 DUP
4 LDC < 字符串 “ 一 ”> [18]
6 invokespecial 的java .lang .StringBuilder(java的.lang .String)[20]
9 aload_0 [参数] 10 arraylength 11 invokevirtual 的java .lang .StringBuilder .append(INT):的java .lang .StringBuilder [23] 14 LDC < 字符串 “ b ”> [27] 16 invokevirtual 的java .lang .StringBuilder .append(java的.lang .String):java的.lang 。 StringBuilder的 [29] 19 invokevirtual 的java .lang .StringBuilder 的ToString():的java .lang .String [32] 22 astore_1 [X]
但是,如果以后,您需要使用可选部分修改您的String?
String x=“a” + args.length + “b” ;
if(args.length==1)x=x + args [ 0 ];
现在你将有一秒钟 StringBuilder ,只是不必要地消耗你的堆的记忆,给你的GC施加压力。写这个:
StringBuilder x=newStringBuilder(“a”);X。append(args.length);X。append(“b”);if(args.length==1);X。append(args [ 0 ]);
在上面的例子中,如果你使用了显式的StringBuilder实例,或者你依靠Java编译器为你创建隐式实例,这可能是完全不相干的。但请记住,我们在NOPE分行。每个CPU周期,我们都浪费在像GC那样愚蠢的东西,或者分配一个StringBuilder的默认容量,我们浪费了N x O x P次。
根据经验,总是使用StringBuilder而不是+运算符。如果可以,请保持StringBuilder引用跨多个方法,如果您的String更复杂的构建。
为了大声哭泣,如果还有StringBuffer引用,请用StringBuilder替换它们。你真的几乎不需要在正在创建的字符串上进行同步。
2)内存不足错误
困扰企业应用程序的最常见问题之一是可怕的OutOfMemoryError。错误通常是以下之一:
应用程序服务器崩溃。
性能下降。
一个似乎无休止的重复垃圾回收循环几乎停止处理,通常会导致应用程序服务器崩溃。
无论症状如何,在性能恢复正常之前,您很可能需要重新启动应用程序服务器。
内存不足错误的原因
在尝试解决内存不足错误之前,首先了解如何发生错误是有益的。如果JVM在其进程内存空间(包括堆中的所有区域)以及永久内存空间中的内存不足,并且进程尝试创建新的对象实例,则会执行垃圾回收器以尝试释放足够的内存允许新对象的创建。如果垃圾收集器无法释放足够的内存来容纳新对象,那么它会抛出一个OutOfMemoryError。
内存不足错误最常见于Java内存泄漏。从以前的讨论中回顾一下,Java内存泄漏是维护对未使用对象的延续引用的结果:您完成了使用对象,但是由于一个或多个其他对象仍然引用该对象,因此垃圾收集器无法回收其内存。因此,该对象所占用的内存从可用堆中丢失。这些类型的内存泄漏通常发生在Web请求期间,而一个或两个泄露的对象可能不会使应用程序服务器崩溃,10,000或20,000个请求可能会发生。此外,泄漏的大多数对象不是简单的对象,如整数或双精度,而是代表堆内的子图。例如,您可能会无意中持有Person对象,并且该Person对象具有一个Profile对象,该对象具有多个PerformanceReview对象,每个对象都维护数据集。而不是丢失Person对象占用的100个字节的内存,您将丢失可能占用500 KB或更多内存的整个子图。
为了识别此问题的根源,您需要确定是否存在真实的内存泄漏,或者是否将其他内容显示为OutOfMemoryError。
做出这样的决定时,我使用以下两种技巧:
分析深度内存统计信息。
检查堆的增长模式。
所有JVM(如Sun和IBM)的JVM调优过程都不相同,但存在一些共同点。
3)避免正则表达式
正则表达式比较便宜便宜。但是,如果您在NOPE分支机构,那么他们是最糟糕的事情。如果绝对必须在计算密集型代码段中使用正则表达式,至少缓存“模式”引用,而不是重新编译它们:
staticFinalPattern HEAVY_REGEX=Patternpile(“(((X)* Y)* Z)*”);
但是如果你的正则表达式真的很傻,就像
String [] parts=ipAddress。split(“\\”);
那么你真的更好地采用普通 char[] 或基于索引的操纵。例如,这个完全不可读的循环也是一样的:
intlength=ipAddress。length();toffset=0 ;intpart=0 ;for(inti=0 ; i < length ; i ++){
if(i==length - 1 || ipAddress.charAt(i + 1)=='。'){
parts [part]=ipAddress.substring(offset,i + 1);
部分+ +;
offset=i + 2 ;
}}
...这也显示了为什么你不应该做任何过早的优化。与split() 版本相比 ,这是不可维护的。
4)不要使用iterator()
现在,这个建议真的不是一般用例,而只适用于NOPE分支。不过,你应该考虑一下。编写Java-5风格的foreach循环很方便。你可以完全忘记循环的内部,并写道:
对于(String value:strings){
//在这里做一些有用的事情}
但是,每次遇到这个循环时,如果字符串是一个Iterable,您将创建一个新的Iterator实例。如果您使用的是ArrayList,那么将在堆上分配一个3个int的对象:
privateClassItr implementsIterator < E > {
intcursor;
相反,您可以编写以下等效循环和“浪费” int 堆栈上的单个 值,这是很便宜的:
intsize=strings。size();for(inti=0 ; i < size ; i ++){
字符串值:strings.get(i); //在这里游戏做一些有用的事情
...或者,如果您的列表没有真正改变,您甚至可以在其阵列版本上操作:
对于(String value:stringArray){
//在这里做一些有用的事情}
5.使用原语和堆栈
上面的例子来自jOOQ,它使用了很多泛型,因此被强制使用包装类型为byte,short,int和long,至少在Java 10和项目Valhalla中可以使用泛型之前。但你可能没有这个约束在您的代码,所以你应该采取一切措施替换:
//去堆 整数i=817598 ;
… 有了这个:
//在堆栈上保留inti=817598 ;
使用数组时,事情会变得更糟。替换为:
//三堆对象!整数[] I={ 1337,424242 };
… 有了这个:
//一个堆对象。INT [] I={ 1337,424242};
当您深入您的NOPE分支时,您应该非常警惕使用包装类型。有可能你会在你的GC上造成很大的压力,因为它必须一直踢在一起清理你的混乱。
一个特别有用的优化可能是使用一些原始类型并创建它的大的一维数组,以及一些分隔符变量来指示编码对象在数组上的位置。
一个优秀的原始集合库,比您的平均int []更复杂一点,它是与LGPL一起使用的trove4j。
这个规则有一个例外:布尔值和字节有足够的值被JDK完全缓存。你可以写:
Boolean a1=true ; // ...语法糖为:Boolean a2=Boolean .valueOf(true);字节 b1=(字节)123 ; // ...语法糖为:字节 b2=字节 .valueOf((字节)123);
其他整数基元类型的低值也同样如此,包括char,short,int和long。但是,只有当你自动打包它们,或者调用TheType.valueOf()时,不是当你调用构造函数时!
结论
为了有效地诊断性能问题,您需要了解问题症状如何映射底层问题的根本原因。如果您可以将问题分解到应用程序代码,那么您需要将问题转发给应用程序支持代理,但如果问题出在环境中,则解决问题是在您的控制之内。
问题的根源在很大程度上取决于许多因素,但是一些指标可以在诊断问题和完全消除其他问题时增加信心。我希望本文可以作为您的Java环境的开始故障排除指南,您可以随着问题出现,自定义您的环境。