0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Java中使用try catch会严重影响性能?

倩倩 来源:CSDN 作者:CSDN 2022-09-08 10:51 次阅读


前言

不知道从何时起,传出了这么一句话:Java中使用try catch 会严重影响性能。

然而,事实真的如此么?我们对try catch 应该畏之如猛虎么?

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

一、JVM 异常处理逻辑

Java 程序中显式抛出异常由athrow指令支持,除了通过 throw 主动抛出异常外,JVM规范中还规定了许多运行时异常会在检测到异常状况时自动抛出(效果等同athrow), 例如除数为0时就会自动抛出异常,以及大名鼎鼎的 NullPointerException

还需要注意的是,JVM 中 异常处理的catch语句不再由字节码指令来实现(很早之前通过 jsr和 ret指令来完成,它们在很早之前的版本里就被舍弃了),现在的JVM通过异常表(Exception table 方法体中能找到其内容)来完成 catch 语句;很多人说try catch 影响性能可能就是因为认识还停留于上古时代。

1.我们编写如下的类,add 方法中计算 ++x; 并捕获异常。

publicclassTestClass{
privatestaticintlen=779;
publicintadd(intx){
try{
//若运行时检测到x=0,那么jvm会自动抛出异常,(可以理解成由jvm自己负责athrow指令调用)
x=100/x;
}catch(Exceptione){
x=100;
}
returnx;
}
}

2.使用javap 工具查看上述类的编译后的class文件

#编译
javacTestClass.java
#使用javap查看add方法被编译后的机器指令
javap-verboseTestClass.class

忽略常量池等其他信息,下边贴出add 方法编译后的 机器指令集:

publicintadd(int);
descriptor:(I)I
flags:ACC_PUBLIC
Code:
stack=2,locals=3,args_size=2
0:bipush100//加载参数100
2:iload_1//将一个int型变量推至栈顶
3:idiv//相除
4:istore_1//除的结果值压入本地变量
5:goto11//跳转到指令:11
8:astore_2//将引用类型值压入本地变量
9:bipush100//将单字节常量推送栈顶<这里与数值100有关,可以尝试修改100后的编译结果:iconst、bipush、ldc>
10:istore_1//将int类型值压入本地变量
11:iload_1//int型变量推栈顶
12:ireturn//返回
//注意看from和to以及targer,然后对照着去看上述指令
Exceptiontable:
fromtotargettype
058Classjava/lang/Exception
LineNumberTable:
line6:0
line9:5
line7:8
line8:9
line10:11
StackMapTable:number_of_entries=2
frame_type=72/*same_locals_1_stack_item*/
stack=[classjava/lang/Exception]
frame_type=2/*same*/

再来看 Exception table

751faf10-2f1f-11ed-ba43-dac502259ad0.png

from=0to=5。指令 0~5 对应的就是 try 语句包含的内容,而targer = 8 正好对应 catch 语句块内部操作。

个人理解,from 和 to 相当于划分区间,只要在这个区间内抛出了type 所对应的,“java/lang/Exception” 异常(主动athrow 或者 由jvm运行时检测到异常自动抛出),那么就跳转到target 所代表的第八行。

若执行过程中,没有异常,直接从第5条指令跳转到第11条指令后返回,由此可见未发生异常时,所谓的性能损耗几乎不存在;

如果硬是要说的话,用了try catch 编译后指令篇幅变长了;goto 语句跳转会耗费性能,当你写个数百行代码的方法的时候,编译出来成百上千条指令,这时候这句goto的带来的影响显得微乎其微。

如图所示为去掉try catch 后的指令篇幅,几乎等同上述指令的前五条。

752a35ac-2f1f-11ed-ba43-dac502259ad0.png

综上所述:“Java中使用try catch 会严重影响性能” 是民间说法,它并不成立。 如果不信,接着看下面的测试吧。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

二、关于JVM的编译优化

其实写出测试用例并不是很难,这里我们需要重点考虑的是编译器的自动优化,是否会因此得到不同的测试结果?

本节会粗略的介绍一些jvm编译器相关的概念,讲它只为更精确的测试结果,通过它我们可以窥探 try catch 是否会影响JVM的编译优化。

前端编译与优化 :我们最常见的前端编译器是 javac,它的优化更偏向于代码结构上的优化,它主要是为了提高程序员的编码效率,不怎么关注执行效率优化;例如,数据流和控制流分析、解语法糖等等。

后端编译与优化 :后端编译包括 “即时编译[JIT]” 和 “提前编译[AOT]”,区别于前端编译器,它们最终作用体现于运行期,致力于优化从字节码生成本地机器码的过程(它们优化的是代码的执行效率)。

1. 分层编译

PS * JVM 自己根据宿主机决定自己的运行模式, “JVM 运行模式”;[客户端模式-Client、服务端模式-Server],它们代表的是两个不同的即时编译器,C1(Client Compiler)C2 (Server Compiler)

PS:分层编译分为:“解释模式”、“编译模式”、“混合模式”;

解释模式下运行时,编译器不介入工作;

编译模式模式下运行,会使用即时编译器优化热点代码,有可选的即时编译器[C1 或 C2]

混合模式为:解释模式和编译模式搭配使用。

753e5d2a-2f1f-11ed-ba43-dac502259ad0.png

如图,我的环境里JVM 运行于 Server 模式,如果使用即时编译,那么就是使用的:C2 即时编译器。

2. 即时编译器

了解如下的几个 概念:

1. 解释模式

它不使用即时编译器进行后端优化

强制虚拟机运行于 “解释模式” -Xint

禁用后台编译 -XX:-BackgroundCompilation

2. 编译模式

即时编译器会在运行时,对生成的本地机器码进行优化,其中重点关照热点代码。

#强制虚拟机运行于"编译模式"
-Xcomp
#方法调用次数计数器阈值,它是基于计数器热点代码探测依据[Client模式=1500,Server模式=10000]
-XX:CompileThreshold=10
#关闭方法调用次数热度衰减,使用方法调用计数的绝对值,它搭配上一配置项使用
-XX:-UseCounterDecay
#除了热点方法,还有热点回边代码[循环],热点回边代码的阈值计算参考如下:
-XX:BackEdgeThreshold=方法计数器阈值[-XX:CompileThreshold]*OSR比率[-XX:OnStackReplacePercentage]
#OSR比率默认值:Client模式=933,Server模式=140
-XX:OnStackReplacePercentag=100

所谓 “即时”,它是在运行过程中发生的,所以它的缺点也也明显:在运行期间需要耗费资源去做性能分析,也不太适合在运行期间去大刀阔斧的去做一些耗费资源的重负载优化操作。

3. 提前编译器:jaotc

它是后端编译的另一个主角,它有两个发展路线,基于Graal [新时代的主角] 编译器开发,因为本文用的是 C2 编译器,所以只对它做一个了解;

第一条路线 :与传统的C、C++编译做的事情类似,在程序运行之前就把程序代码编译成机器码;好处是够快,不占用运行时系统资源,缺点是"启动过程" 会很缓慢;

第二条路线 :已知即时编译运行时做性能统计分析占用资源,那么,我们可以把其中一些耗费资源的编译工作,放到提前编译阶段来完成啊,最后在运行时即时编译器再去使用,那么可以大大节省即时编译的开销;这个分支可以把它看作是即时编译缓存;

遗憾的是它只支持 G1 或者 Parallel 垃圾收集器,且只存在JDK 9 以后的版本,暂不需要去关注它;JDK 9 以后的版本可以使用这个参数打印相关信息:[-XX:PrintAOT]

三、关于测试的约束

执行用时统计

System.naoTime() 输出的是过了多少时间[微秒:10的负9次方秒],并不是完全精确的方法执行用时的合计,为了保证结果准确性,测试的运算次数将拉长到百万甚至千万次。

编译器优化的因素

上一节花了一定的篇幅介绍编译器优化,这里我要做的是:对比完全不使用任何编译优化,与使用即时编译时,try catch 对的性能影响。

  • 通过指令禁用 JVM 的编译优化,让它以最原始的状态运行,然后看有无 try catch 的影响。
  • 通过指令使用即时编译,尽量做到把后端优化拉满,看看 try catch 十有会影响到 jvm的编译优化。

关于指令重排序

目前尚未可知 try catch 的使用影响指令重排序;

我们这里的讨论有一个前提,当 try catch 的使用无法避免时,我们应该如何使用 try catch 以应对它可能存在的对指令重排序的影响。

  • 指令重排序发生在多线程并发场景,这么做是为了更好的利用CPU资源,在单线程测试时不需要考虑。不论如何指令重排序,都会保证最终执行结果,与单线程下的执行结果相同;
  • 虽然我们不去测试它,但是也可以进行一些推断,参考 volatile 关键字禁止指令重排序的做法:插入内存屏障;
  • 假定 try catch 存在屏障,导致前后的代码分割;那么最少的try catch代表最少的分割。
  • 所以,是不是会有这样的结论呢:我们把方法体内的 多个 try catch 合并为一个 try catch 是不是反而能减少屏障呢?这么做势必造成 try catch 的范围变大。

当然,上述关于指令重排序讨论内容都是基于个人的猜想,犹未可知 try catch 是否影响指令重排序;本文重点讨论的也只是单线程环境下的 try catch 使用影响性能。

四、测试代码

循环次数为100W ,循环内10次预算[给编译器优化预留优化的可能,这些指令可能被合并]

每个方法都会到达千万次浮点计算。

同样每个方法外层再循环跑多次,最后取其中的众数更有说服力。

publicclassExecuteTryCatch{

//100W
privatestaticfinalintTIMES=1000000;
privatestaticfinalfloatSTEP_NUM=1f;
privatestaticfinalfloatSTART_NUM=Float.MIN_VALUE;


publicstaticvoidmain(String[]args){
inttimes=50;
ExecuteTryCatchexecuteTryCatch=newExecuteTryCatch();
//每个方法执行50次
while(--times>=0){
System.out.println("times=".concat(String.valueOf(times)));
executeTryCatch.executeMillionsEveryTryWithFinally();
executeTryCatch.executeMillionsEveryTry();
executeTryCatch.executeMillionsOneTry();
executeTryCatch.executeMillionsNoneTry();
executeTryCatch.executeMillionsTestReOrder();
}
}

/**
*千万次浮点运算不使用trycatch
**/
publicvoidexecuteMillionsNoneTry(){
floatnum=START_NUM;
longstart=System.nanoTime();
for(inti=0;i< TIMES; ++i){
            num = num + STEP_NUM + 1f;
num=num+STEP_NUM+2f;
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("noneTrysum:"+num+"million:"+million+"nao:"+nao);
}

/**
*千万次浮点运算最外层使用trycatch
**/
publicvoidexecuteMillionsOneTry(){
floatnum=START_NUM;
longstart=System.nanoTime();
try{
for(inti=0;i< TIMES; ++i){
                num = num + STEP_NUM + 1f;
num=num+STEP_NUM+2f;
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}
}catch(Exceptione){

}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("oneTrysum:"+num+"million:"+million+"nao:"+nao);
}

/**
*千万次浮点运算循环内使用trycatch
**/
publicvoidexecuteMillionsEveryTry(){
floatnum=START_NUM;
longstart=System.nanoTime();
for(inti=0;i< TIMES; ++i){
            try{
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}catch(Exceptione){

}
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("evertTrysum:"+num+"million:"+million+"nao:"+nao);
}


/**
*千万次浮点运算循环内使用trycatch,并使用finally
**/
publicvoidexecuteMillionsEveryTryWithFinally(){
floatnum=START_NUM;
longstart=System.nanoTime();
for(inti=0;i< TIMES; ++i){
            try{
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}catch(Exceptione){

}finally{
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("finalTrysum:"+num+"million:"+million+"nao:"+nao);
}

/**
*千万次浮点运算,循环内使用多个trycatch
**/
publicvoidexecuteMillionsTestReOrder(){
floatnum=START_NUM;
longstart=System.nanoTime();
for(inti=0;i< TIMES; ++i){
            try{
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
}catch(Exceptione){}

try{
num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}catch(Exceptione){}

try{
num=num+STEP_NUM+1f;
num=num+STEP_NUM+2f;
}catch(Exceptione){}
try{

num=num+STEP_NUM+3f;
num=num+STEP_NUM+4f;
num=num+STEP_NUM+5f;
}catch(Exceptione){}
}
longnao=System.nanoTime()-start;
longmillion=nao/1000000;
System.out.println("orderTrysum:"+num+"million:"+million+"nao:"+nao);
}

}

五、解释模式下执行测试

设置如下JVM参数,禁用编译优化

-Xint
-XX:-BackgroundCompilation

754d832c-2f1f-11ed-ba43-dac502259ad0.png结合测试代码发现,即使百万次循环计算,每个循环内都使用了 try catch 也并没用对造成很大的影响。

唯一发现了一个问题,每个循环内都是使用 try catch 且使用多次。发现性能下降,千万次计算差值为:5~7 毫秒;4个 try 那么执行的指令最少4条goto ,前边阐述过,这里造成这个差异的主要原因是 goto 指令占比过大,放大了问题;当我们在几百行代码里使用少量try catch 时,goto所占比重就会很低,测试结果会更趋于合理。

六、编译模式测试

设置如下测试参数,执行10 次即为热点代码

-Xcomp
-XX:CompileThreshold=10
-XX:-UseCounterDecay
-XX:OnStackReplacePercentage=100
-XX:InterpreterProfilePercentage=33

执行结果如下图,难分胜负,波动只在微秒级别,执行速度也快了很多,编译效果拔群啊,甚至连 “解释模式” 运行时多个try catch 导致的,多个goto跳转带来的问题都给顺带优化了;由此也可以得到 try catch 并不会影响即时编译的结论。

75940c02-2f1f-11ed-ba43-dac502259ad0.png

我们可以再上升到亿级计算,依旧难分胜负,波动在毫秒级。

75ba48a4-2f1f-11ed-ba43-dac502259ad0.png

七、结论

try catch 不会造成巨大的性能影响,换句话说,我们平时写代码最优先考虑的是程序的健壮性,当然大佬们肯定都知道了怎么合理使用try catch了,但是对萌新来说,你如果不确定,那么你可以使用 try catch;

在未发生异常时,给代码外部包上 try catch,并不会造成影响。

举个栗子吧,我的代码中使用了:URLDecoder.decode,所以必须得捕获异常。

privateintgetThenAddNoJudge(JSONObjectjson,Stringkey){
if(Objects.isNull(json))
thrownewIllegalArgumentException("参数异常");
intnum;
try{
//不校验key是否未空值,直接调用toString每次触发空指针异常并被捕获
num=100+Integer.parseInt(URLDecoder.decode(json.get(key).toString(),"UTF-8"));
}catch(Exceptione){
num=100;
}
returnnum;
}

privateintgetThenAddWithJudge(JSONObjectjson,Stringkey){
if(Objects.isNull(json))
thrownewIllegalArgumentException("参数异常");
intnum;
try{
//校验key是否未空值
num=100+Integer.parseInt(URLDecoder.decode(Objects.toString(json.get(key),"0"),"UTF-8"));
}catch(Exceptione){
num=100;
}
returnnum;
}

publicstaticvoidmain(String[]args){
inttimes=1000000;//百万次

longnao1=System.nanoTime();
ExecuteTryCatchexecuteTryCatch=newExecuteTryCatch();
for(inti=0;i< times; i++){
            executeTryCatch.getThenAddWithJudge(newJSONObject(),"anyKey");
}
longend1=System.nanoTime();
System.out.println("未抛出异常耗时:millions="+(end1-nao1)/1000000+"毫秒nao="+(end1-nao1)+"微秒");


longnao2=System.nanoTime();
for(inti=0;i< times; i++){
            executeTryCatch.getThenAddNoJudge(newJSONObject(),"anyKey");
}
longend2=System.nanoTime();
System.out.println("每次必抛出异常:millions="+(end2-nao2)/1000000+"毫秒nao="+(end2-nao2)+"微秒");
}

调用方法百万次,执行结果如下:

75d052ac-2f1f-11ed-ba43-dac502259ad0.png

经过这个例子,我想你知道你该如何 编写你的代码了吧?可怕的不是 try catch 而是 搬砖业务不熟练啊。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2904

    浏览量

    103007
  • 编译器
    +关注

    关注

    1

    文章

    1577

    浏览量

    48638
  • JVM
    JVM
    +关注

    关注

    0

    文章

    152

    浏览量

    12130

原文标题:别被骗了,try-catch语句真的会影响性能吗?

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    STM32CUBEMX界面重影严重怎么解决?

    STM32CUBEMX 界面重影严重,有相同问题的吗
    发表于 03-14 07:13

    Java中的常用异常处理方法 java推荐

    IllegalArgumentException 类,IllegalStateException 类。捕获异常的方法使用 trycatch 关键字可以捕获异常,try/catch
    发表于 01-19 17:26

    Java捕获异常处理的常用方法

    IllegalArgumentException 类,IllegalStateException 类。捕获异常的方法使用 trycatch 关键字可以捕获异常,try/catch
    发表于 11-27 11:40

    如果IT设备坏了,严重影响日常的运作怎么办?

    如果IT设备坏了,严重影响日常的运作怎么办?
    发表于 12-07 15:12

    Sqlserver Try CatchCatch捕获到错误重试一次的方法分享

    CATCH…END CATCH是一体的,如果他们两者放入IF后面,不会因为两个BEGIN就会导致BEGIN CATCH…END CATCH不受IF管控2、这种
    发表于 11-10 17:44

    cubemx显示严重重影是为什么?

    cubemx显示严重重影,调整分辨率恢复一段时间后又出现重影
    发表于 08-04 11:27

    Java异常处理之trycatch,finally,throw,throws

    后,程序继续运行。 java的异常处理是通过5个关键字来实现的:trycatch、finally、throw、throws。 二:java异常类的层次结构 三。常见的异常类型 Exc
    发表于 09-27 11:17 0次下载
    <b class='flag-5'>Java</b>异常处理之<b class='flag-5'>try</b>,<b class='flag-5'>catch</b>,finally,throw,throws

    俄军尝试对叙利亚上空的美无人机实施干扰,“严重影响”美无人机行动

    据美国报道称,俄军正在积极尝试对叙利亚上空的美军无人机实施干扰,通过干扰全球卫星定位系统信号的发射破坏飞行行动。干扰行动“严重影响”美无人机行动,但尚不清楚到底有多严重
    发表于 07-22 11:58 538次阅读

    Java并行流存在的问题及解决办法详解

    对于从事Java开发的童鞋来说,相信对于Java8的并行流并不陌生,没错,我们常常用它来执行并行任务,但是由于并行流(parallel stream)采用的是享线程池,可能会对我们的性能造成
    发表于 04-03 15:55 12次下载

    TDD模式下Rx对Tx的严重影响怎么解决

    很难想象TDD模式下Rx对Tx造成了严重影响,若不是亲眼所见,我也是完全不相信的,按道理说这种事情只能发生在FDD模式下。近期在为朋友解决一款5.8GHz大功率无线网桥的射频问题时,发现Chain0
    的头像 发表于 09-14 17:39 4633次阅读
    TDD模式下Rx对Tx的<b class='flag-5'>严重影响</b>怎么解决

    芯片短缺已严重影响到比特币挖矿硬件分销链

    不但玩家买不到显卡,比特币矿商现同样面临芯片短缺问题,矿机已涨价 45% 据国外媒体报道,芯片短缺已经严重影响到了比特币挖矿硬件分销链。 矿机库存不足。据比特大陆网站上的信息,到 2021
    的头像 发表于 01-25 10:43 1926次阅读

    公司这套架构统一处理try...catch真香!

    软件开发springboot项目过程中,不可避免的需要处理各种异常,spring mvc 架构中各层会出现大量的try {...} catch {...} finally {...} 代码块,不仅
    的头像 发表于 02-27 10:47 335次阅读

    使用try-catch捕获异常会影响性能吗?

    “BB 不如 show code,看到没, 老王,我把 try-catch 从 for 循环里面提出来跟在for循环里面做个对比跑一下,你猜猜两个差多少?”
    的头像 发表于 04-01 11:08 853次阅读

    try catch应该在for循环里面还是外面?

    因为本身try catch 放在 for循环 外面 和里面 ,如果出现异常,产生的效果是不一样的。
    的头像 发表于 07-31 10:16 748次阅读
    <b class='flag-5'>try</b> <b class='flag-5'>catch</b>应该在for循环里面还是外面?

    为何高温会严重影响锂离子电池的安全性?

    为何高温会严重影响锂离子电池的安全性? 高温对锂离子电池的安全性有严重影响。锂离子电池在过高的温度下会发生一系列的热化学反应,可能导致电解液的分解、电极的氧化、电池内部结构的破坏以及更严重的情况下
    的头像 发表于 12-08 16:05 652次阅读