java性能优化实战常用代码优化法则,让代码保持最优状态
前面的文章,我们分别了解缓冲、缓存、池化对象、大对象复用、并行计算、锁优化、NIO等优化方法,它们对性能的提升往往是质的飞跃。
但语言本身对性能也是有影响的,比如就有很多公司就因为语言的特性由Java切换到Golang。对于Java语言来说,也有它的一套优化法则,这些细微的性能差异,经过多次调用和迭代,会产生越来越大的影响。
今天我们将集中讲解一些常用的代码优化法则,从而在编码中保持好的习惯,让代码保持最优状态。代码优化法则1。使用局部变量可避免在堆上分配
由于堆资源是多线程共享的,是垃圾回收器工作的主要区域,过多的对象会造成GC压力。可以通过局部变量的方式,将变量在栈上分配。这种方式变量会随着方法执行的完毕而销毁,能够减轻GC的压力。2。减少变量的作用范围
注意变量的作用范围,尽量减少对象的创建。如下面的代码,变量a每次进入方法都会创建,可以将它移动到if语句内部。publicvoidtest1(Stringstr){finalinta100;if(!StringUtils。isEmpty(str)){intbaa;}}3。访问静态变量直接使用类名
有的同学习惯使用对象访问静态变量,这种方式多了一步寻址操作,需要先找到变量对应的类,再找到类对应的变量,如下面的代码:publicclassStaticCall{publicstaticfinalintA1;voidtest(){System。out。println(this。A);System。out。println(StaticCall。A);}}
对应的字节码为:voidtest();descriptor:()Vflags:Code:stack2,locals1,argssize10:getstatic2FieldjavalangSystem。out:LjavaioPrintStream;3:aload04:pop5:iconst16:invokevirtual3MethodjavaioPrintStream。println:(I)V9:getstatic2FieldjavalangSystem。out:LjavaioPrintStream;12:iconst113:invokevirtual3MethodjavaioPrintStream。println:(I)V16:returnLineNumberTable:line5:0line6:9line7:16
可以看到使用this的方式多了一个步骤。4。字符串拼接使用StringBuilder
字符串拼接,使用StringBuilder或者StringBuffer,不要使用号。比如下面这段代码,在循环中拼接了字符串。publicStringtest(){Stringstr1;for(inti0;i10;i){stri;}returnstr;}
从下面对应的字节码内容可以看出,它在每个循环里都创建了一个StringBuilder对象。所以,我们在平常的编码中,显式地创建一次即可。5:iload26:bipush108:ificmpge3611:new3classjavalangStringBuilder14:dup15:invokespecial4MethodjavalangStringBuilder。init:()V18:aload119:invokevirtual5MethodjavalangStringBuilder。append:(LjavalangString;)LjavalangStringBuilder;22:iload223:invokevirtual6MethodjavalangStringBuilder。append:(I)LjavalangStringBuilder;26:invokevirtual7MethodjavalangStringBuilder。toString:()LjavalangString;29:astore130:iinc2,133:goto55。重写对象的HashCode,不要简单地返回固定值
在代码review的时候,我发现有开发重写HashCode和Equals方法时,会把HashCode的值返回固定的0,而这样做是不恰当的。
当这些对象存入HashMap时,性能就会非常低,因为HashMap是通过HashCode定位到Hash槽,有冲突的时候,才会使用链表或者红黑树组织节点。固定地返回0,相当于把Hash寻址功能给废除了。6。HashMap等集合初始化的时候,指定初始值大小
这个原则参见10案例分析:大对象复用的目标和注意点,这样的对象有很多,比如ArrayList,StringBuilder等,通过指定初始值大小可减少扩容造成的性能损耗。7。遍历Map的时候,使用EntrySet方法
使用EntrySet方法,可以直接返回set对象,直接拿来用即可;而使用KeySet方法,获得的是key的集合,需要再进行一次get操作,多了一个操作步骤。所以更推荐使用EntrySet方式遍历Map。8。不要在多线程下使用同一个Random
Random类的seed会在并发访问的情况下发生竞争,造成性能降低,建议在多线程环境下使用ThreadLocalRandom类。
在Linux上,通过加入JVM配置Djava。security。egdfile:dev。urandom,使用urandom随机生成器,在进行随机数获取时,速度会更快。9。自增推荐使用LongAddr
自增运算可以通过synchronized和volatile的组合,或者也可以使用原子类(比如AtomicLong)。
后者的速度比前者要高一些,AtomicLong使用CAS进行比较替换,在线程多的情况下会造成过多无效自旋,所以可以使用LongAdder替换AtomicLong进行进一步的性能提升。10。不要使用异常控制程序流程
异常,是用来了解并解决程序中遇到的各种不正常的情况,它的实现方式比较昂贵,比平常的条件判断语句效率要低很多。
这是因为异常在字节码层面,需要生成一个如下所示的异常表(Exceptiontable),多了很多判断步骤。Exceptiontable:fromtotargettype71720any202320any
所以,尽量不要使用异常控制程序流程。11。不要在循环中使用trycatch
道理与上面类似,很多文章介绍,不要把异常处理放在循环里,而应该把它放在最外层,但实际测试情况表明这两种方式性能相差并不大。
既然性能没什么差别,那么就推荐根据业务的需求进行编码。比如,循环遇到异常时,不允许中断,也就是允许在发生异常的时候能够继续运行下去,那么异常就只能在for循环里进行处理。12。不要捕捉RuntimeException
Java异常分为两种,一种是可以通过预检查机制避免的RuntimeException;另外一种就是普通异常。
其中,RuntimeException不应该通过catch语句去捕捉,而应该使用编码手段进行规避。
如下面的代码,list可能会出现数组越界异常。是否越界是可以通过代码提前判断的,而不是等到发生异常时去捕捉。提前判断这种方式,代码会更优雅,效率也更高。BADpublicStringtest1(ListStringlist,intindex){try{returnlist。get(index);}catch(IndexOutOfBoundsExceptionex){returnnull;}}GOODpublicStringtest2(ListStringlist,intindex){if(indexlist。size()index0){returnnull;}returnlist。get(index);}13。合理使用PreparedStatement
PreparedStatement使用预编译对SQL的执行进行提速,大多数数据库都会努力对这些能够复用的查询语句进行预编译优化,并能够将这些编译结果缓存起来。
这样等到下次用到的时候,就可以很快进行执行,也就少了一步对SQL的解析动作。
PreparedStatement还能提高程序的安全性,能够有效防止SQL注入。
但如果你的程序每次SQL都会变化,不得不手工拼接一些数据,那么PreparedStatement就失去了它的作用,反而使用普通的Statement速度会更快一些。14。日志打印的注意事项
我们在06案例分析:缓冲区如何让代码加速中了解了logback的异步日志,日志打印还有一些其他要注意的事情。
我们平常会使用debug输出一些调试信息,然后在线上关掉它。如下代码:logger。debug(xjjdog:topicisawesome);
程序每次运行到这里,都会构造一个字符串,不管你是否把日志级别调试到INFO还是WARN,这样效率就会很低。
可以在每次打印之前都使用isDebugEnabled方法判断一下日志级别,代码如下:if(logger。isDebugEnabled()){logger。debug(xjjdog:topicisawesome);}
使用占位符的方式,也可以达到相同的效果,就不用手动添加isDebugEnabled方法了,代码也优雅得多。logger。debug(xjjdog:{}isawesome,topic);
对于业务系统来说,日志对系统的性能影响非常大,不需要的日志,尽量不要打印,避免占用IO资源。15。减少事务的作用范围
如果的程序使用了事务,那一定要注意事务的作用范围,尽量以最快的速度完成事务操作。这是因为,事务的隔离性是使用锁实现的,可以类比使用13案例分析:多线程锁的优化中的多线程锁进行优化。Transactionalpublicvoidtest(Stringid){Stringvaluerpc。getValue(id);高耗时testDao。update(sql,value);}
如上面的代码,由于rpc服务耗时高且不稳定,就应该把它移出到事务之外,改造如下:publicvoidtest(Stringid){Stringvaluerpc。getValue(id);高耗时testDao(value);}TransactionalpublicvoidtestDao(Stringvalue){testDao。update(value);}
这里有一点需要注意的地方,由于SpringAOP的原因,Transactional注解只能用到public方法上,如果用到private方法上,将会被忽略,这也是面试经常问的考点之一。16。使用位移操作替代乘除法
计算机是使用二进制表示的,位移操作会极大地提高性能。左移相当于乘以2;右移相当于除以2;无符号右移相当于除以2,但它会忽略符号位,空位都以0补齐。inta2;intb(a)(a)(a);System。out。println(b);
注意:位移操作的优先级非常低,所以上面的代码,输出是1024。17。不要打印大集合或者使用大集合的toString方法
有的开发喜欢将集合作为字符串输出到日志文件中,这个习惯是非常不好的。
拿ArrayList来说,它需要遍历所有的元素来迭代生成字符串。在集合中元素非常多的情况下,这不仅会占用大量的内存空间,执行效率也非常慢。我曾经就遇到过这种批量打印方式造成系统性能直线下降的实际案例。
下面这段代码,就是ArrayList的toString方法。它需要生成一个迭代器,然后把所有的元素内容拼接成一个字符串,非常浪费空间。publicStringtoString(){IteratorEititerator();if(!it。hasNext())return〔〕;StringBuildersbnewStringBuilder();sb。append(〔);for(;;){Eeit。next();sb。append(ethis?(thisCollection):e);if(!it。hasNext())returnsb。append(〕)。toString();sb。append(,)。append();}}18。程序中少用反射
反射的功能很强大,但它是通过解析字节码实现的,性能就不是很理想。
现实中有很多对反射的优化方法,比如把反射执行的过程(比如Method)缓存起来,使用复用来加快反射速度。
Java7。0之后,加入了新的包java。lang。invoke,同时加入了新的JVM字节码指令invokedynamic,用来支持从JVM层面,直接通过字符串对目标方法进行调用。
如果你对性能有非常苛刻的要求,则使用invoke包下的MethodHandle对代码进行着重优化,但它的编程不如反射方便,在平常的编码中,反射依然是首选。
下面是一个使用MethodHandle编写的代码实现类。它可以完成一些动态语言的特性,通过方法名称和传入的对象主体,进行不同的调用,而Bike和Man类,可以是没有任何关系的。importjava。lang。invoke。MethodHandle;importjava。lang。invoke。MethodHandles;importjava。lang。invoke。MethodType;publicclassMethodHandleDemo{staticclassBike{Stringsound(){returndingding;}}staticclassAnimal{Stringsound(){returnwowwow;}}staticclassManextendsAnimal{OverrideStringsound(){returnhouhou;}}Stringsound(Objecto)throwsThrowable{MethodHandles。LookuplookupMethodHandles。lookup();MethodTypemethodTypeMethodType。methodType(String。class);MethodHandlemethodHandlelookup。findVirtual(o。getClass(),sound,methodType);Stringobj(String)methodHandle。invoke(o);returnobj;}publicstaticvoidmain(String〔〕args)throwsThrowable{StringstrnewMethodHandleDemo()。sound(newBike());System。out。println(str);strnewMethodHandleDemo()。sound(newAnimal());System。out。println(str);strnewMethodHandleDemo()。sound(newMan());System。out。println(str);}}19。正则表达式可以预先编译,加快速度
Java的正则表达式需要先编译再使用。
典型代码如下:PatternpatternPattern。compile({pattern});Matcherpatternpattern。matcher({content});
Pattern编译非常耗时,它的Matcher方法是线程安全的,每次调用方法这个方法都会生成一个新的Matcher对象。所以,一般Pattern初始化一次即可,可以作为类的静态成员变量。案例分析案例1:正则表达式和状态机
正则表达式的执行效率是非常慢的,尤其是贪婪模式。
下面介绍一个我在实际工作中对正则的一个优化,使用状态机完成字符串匹配。
考虑到下面的一个SQL语句,它的语法类似于NamedParameterJdbcTemplate,但我们对它做了增强。SQL接收两个参数:smallId和firstName,当firstName为空的时候,处在{}之间的语句将被抹去。selectfromUSERSwhereid:smallId{andFIRSTNAMElikeconcat(,:firstName,)}
可以看到,使用正则表达式可以很容易地实现这个功能。{(。?:(〔azAZ09〕)。?)}
通过定义上面这样一个正则匹配,使用Pattern的group功能便能提取到相应的字符串。我们把匹配到的字符串保存下来,最后使用replace函数,将它替换成空字符串即可。
结果在实际使用的时候,发现正则的解析速度特别慢,尤其是在SQL非常大的时候,这种情况下,可以使用状态机去优化。我这里选用的是ragel,你也可以使用类似javacc或者antlr之类的工具。它通过语法解析和简单的正则表达式,最终可以生成Java语法的代码。
生成的代码一般是不可读的,我们只关注定义文件即可。如下定义文件代码所示,通过定义一批描述符和处理程序,使用一些中间数据结构对结果进行缓存,只需要对SQL扫描一遍,即可获取相应的结果。pairStart{;pairEnd};namedQueryStringFull(:alnum)buffernamedQueryStringFull;pairBlock(pairStartanynamedQueryStringFullanypairEnd)pairBlockBeginpairBlockEnd;main:anypairBlockany;
把文件定义好之后,即可通过ragel命令生成Java语法的最终文件。ragelG2JoP。javaP。rl
完整的代码有点复杂,我已经放到了仓库中,你可以实际分析一下。
我们来看一下它的性能。从测试结果可以看到,ragel模式的性能是regex模式的3倍还多,SQL越长,效果越明显。BenchmarkModeCntScoreErrorUnitsRegexVsRagelBenchmark。ragelthrpt10691。224446。217opsmsRegexVsRagelBenchmark。regexthrpt10201。32247。056opsms案例2:HikariCP的字节码修改
在09案例分析:池化对象的应用场景中,我们提到了HikariCP对字节码的修改,这个职责是由JavassistProxyFactory类来管理的。Javassist是一个字节码类库,HikariCP就是用它对字节码进行修改。
如下图所示,这是工厂类的主要方法。
它通过generateProxyClass生成代理类,主要是针对Connection、Statement、ResultSet、DatabaseMetaData等jdbc的核心接口。
右键运行这个类,可以看到代码生成了一堆Class文件。Generatingcom。zaxxer。hikari。pool。HikariProxyConnectionGeneratingcom。zaxxer。hikari。pool。HikariProxyStatementGeneratingcom。zaxxer。hikari。pool。HikariProxyResultSetGeneratingcom。zaxxer。hikari。pool。HikariProxyDatabaseMetaDataGeneratingcom。zaxxer。hikari。pool。HikariProxyPreparedStatementGeneratingcom。zaxxer。hikari。pool。HikariProxyCallableStatementGeneratingmethodbodiesforcom。zaxxer。hikari。proxy。ProxyFactory
对于这一部分的代码组织,使用了设计模式中的委托模式。我们发现HikariCP源码中的代理类,比如ProxyConnection,都是abstract的,它的具体实例就是使用javassist生成的class文件。反编译这些生成的class文件,可以看到它实际上是通过调用父类中的委托对象进行处理的。
这么做有两个好处:第一,在代码中只需要实现需要修改的JDBC接口方法,其他的交给代理类自动生成的代码,极大地减少了编码数量。第二,出现问题时,可以通过checkException函数对错误进行统一处理。
另外,我们注意到ProxyFactory类中的方法,都是静态方法,而不是通过单例实现的。为什么这么做呢?这就涉及JVM底层的两个字节码指令:invokestatic和invokevirtual。
下面是两种不同类型调用的字节码。invokevirtualpublicfinaljava。sql。PreparedStatementprepareStatement(java。lang。String,java。lang。String〔〕)throwsjava。sql。SQLException;flags:ACCPRIVATE,ACCFINALCode:stack5,locals3,argssize30:getstatic59FieldPROXYFACTORY:LcomzaxxerhikariproxyProxyFactory;3:aload04:aload05:getfield3Fielddelegate:LjavasqlConnection;8:aload19:aload210:invokeinterface74,3InterfaceMethodjavasqlConnection。prepareStatement:(LjavalangString;〔LjavalangString;)LjavasqlPreparedStatement;15:invokevirtual69MethodcomzaxxerhikariproxyProxyFactory。getProxyPreparedStatement:(LcomzaxxerhikariproxyConnectionProxy;LjavasqlPreparedStatement;)LjavasqlPreparedStatement;18:returninvokestaticprivatefinaljava。sql。PreparedStatementprepareStatement(java。lang。String,java。lang。String〔〕)throwsjava。sql。SQLException;flags:ACCPRIVATE,ACCFINALCode:stack4,locals3,argssize30:aload01:aload02:getfield3Fielddelegate:LjavasqlConnection;5:aload16:aload27:invokeinterface72,3InterfaceMethodjavasqlConnection。prepareStatement:(LjavalangString;〔LjavalangString;)LjavasqlPreparedStatement;12:invokestatic67MethodcomzaxxerhikariproxyProxyFactory。getProxyPreparedStatement:(LcomzaxxerhikariproxyConnectionProxy;LjavasqlPreparedStatement;)LjavasqlPreparedStatement;15:areturn
大多数普通方法调用,使用的是invokevirtual指令,属于虚方法调用。
很多时候,JVM需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比,invokestatic指令,就属于静态绑定过程,能够直接识别目标方法,效率会高那么一点点。
虽然HikariCP的这些优化有点吹毛求疵,但我们能够从中看到HikariCP这些追求性能极致的编码技巧。小结
此外,学习Java规范,你还可以细读《阿里巴巴Java开发规范》,里面也有很多有意义的建议。
其实语言层面的性能优化,都是在各个资源之间的权衡(比如开发时间、代码复杂度、扩展性等)。这些法则也不是一成不变的教条,这就要求我们在编码中选择合适的工具,根据实际的工作场景进行灵活变动。
来看,7种悄悄偷走你睡眠的食物要少吃要想拥有良好的生活质量,必须先有充足的睡眠。现代人由于精神压力大,饮食不懂得节制等多层面因素影响下,失眠睡眠质量差的人数占比愈来愈多。今天我们就从饮食方面来看一下,睡眠不佳的人需要
央行12月20日将在香港招标发行50亿元6个月期人民币央票中新网12月14日电据央行网站消息,为丰富香港高信用等级人民币金融产品,完善香港人民币收益率曲线,根据中国人民银行与香港金融管理局签署的关于使用债务工具中央结算系统发行中国人民银行
鹿晗被爆疑似出轨多位女星,与关晓彤早已分手,有狗仔视频为证!鹿晗被爆疑似出轨多位女星,与关晓彤早已分手,有狗仔视频为证!对此,你们大家有什么看法?网友1娱乐圈什么都有可能!也许有些人会佩服鹿晗当初官宣关晓彤,但不知道关晓彤背后的政治背景有多
关口前移主动施策,终结结核病流行7400万人据统计,20002021年,全球对结核病的治疗对结核病和艾滋病双重感染者的治疗,共挽救了7400万人的生命。这也得益于新技术新药物和治疗方案的使用。过去3年,当大家聚焦
写给天堂里的母亲亲爱的娘您好!原谅我这么久才给您写信。不是不想写而是不敢写,因为一提起笔我就难以抑制自己悲痛的心情。然而,终究还是无法控制对您的思念,于是,我鼓足勇气写下这封信,让神明帮我寄往遥远
31年,京剧封神榜的剧照,妲己穿着露骨,纣王扮相吓人31年,京剧封神榜的剧照,妲己穿着露骨,纣王扮相吓人。这张照片拍摄于1931年,拍摄地点是天津。这是京剧封神榜的剧照。妲己由小杨月楼饰演,纣王由周信芳饰演。照片中的纣王,乍一看,有
研究发现未出生的婴儿能从羊水中尝到他们母亲吃的东西研究人员还认为,孕妇所吃的东西可能会影响她们出生后的新生儿的口味偏好,这可能会对培养健康的饮食习惯产生影响。这项研究最近发表在心理科学杂志上。人类通过味觉和嗅觉的结合来感知味道。这
高中历史西周的分封制分封制,即古汉语封建的原始含义古文献中之封建即分封制,有封邦建国或封土建国之意。是中国古代分封诸侯的制度。古代帝王的后裔和商的遗民以及立功的将士,让他们在地方作诸侯,分区管理,辅佐
若面对13世纪蒙古的是正处朝气的唐明清这种大一统王朝中原王朝能抗住蒙古的兵锋吗?13世纪的蒙古军算是纯粹冷兵器时代的究极了吧?(不包括之后又杂糅了火器的古代军队)铁木真能崛起纯粹就是宋金夏三国已经博弈百年消耗了太久了。换汉唐明铁木真
中世纪的黑死病,到底有多恐怖?在欧洲中世纪,人们常常用上帝之鞭这个词汇来形容一些人类无法抵抗的强大的东西。从字面上看,意思是上帝挥舞着鞭子鞭打人类。由此,可以推导出上帝对世界降下灾难的含义。黑死病在中世纪时被称
中国古代女子择偶标准(中华男性特征变化)1那么母系社会的女性会选择什么样的男性巫山云雨呢?从潜在择偶的心理学上讲分为美丽型或野兽型,不要惊讶!因为动物对美的追求实在是太根深蒂固了,性选择也是进化的一部分,反正女性就是出于本