扯什么trycatch性能问题?
你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?"老陈煞有其事地指着屏幕里的代码: for (int i = 0; i < 5000; i++) { try { dosth } catch (Exception e) { e.printStackTrace(); } } 复制代码
我探过头去看了眼代码,"那老陈你觉得该怎么改?"
"当然是把 try-catch 提到外面啊!"老陈脑子都不转一下,脱口而出。
"你是不是傻?且不说性能,这代码的目的明显是让循环内部单次调用出错不影响循环的运行,你其到外面业务逻辑不就变了吗!"
老陈挠了挠他的地中海,"好像也是啊!"
"回过头来,catch 整个 for 循环和在循环内部 catch,在不出错的情况下,其实性能差不多。" 我喝一口咖啡不经意地提到,准备在老陈前面秀一下。
"啥意思?"老陈有点懵地看着我,"try-catch是有性能损耗的,我可是看过网上资料的!"
果然,老陈上钩了,我二话不说直接打开 idea,一顿操作敲了以下代码:public class TryCatchTest { @Benchmark public void tryfor(Blackhole blackhole) { try { for (int i = 0; i < 5000; i++) { blackhole.consume(i); } } catch (Exception e) { e.printStackTrace(); } } @Benchmark public void fortry(Blackhole blackhole) { for (int i = 0; i < 5000; i++) { try { blackhole.consume(i); } catch (Exception e) { e.printStackTrace(); } } } } 复制代码
"BB 不如 show code,看到没,老陈,我把 try-catch 从 for 循环里面提出来跟在for循环里面做个对比跑一下,你猜猜两个差多少?"
"切,肯定 tryfor 性能好,想都不用想,不是的话我倒立洗头!"老陈信誓旦旦道。
我懒得跟他BB,直接开始了 benchmark,跑的结果如下:
可以看到,两者的性能(数字越大越好)其实差不多:fortry: 86,261(100359-14098) ~ 114,457(100359+14098)tryfor: 95,961(103216-7255) ~ 110,471(103216+7255)
我再调小(一般业务场景 for 循环次数都不会很多)下 for 循环的次数为 1000 ,结果也是差不多:
老陈一看傻了:"说好的性能影响呢?怎么没了?"
我直接一个javap,让老陈看看,其实两个实现在字节码层面没啥区别:
tryfor 的字节码
异常表记录的是 0 - 20 行,如果这些行里面的代码出现问题,直接跳到 23 行处理。
fortry 的字节码
差别也就是异常表的范围小点,包的是 9-14 行,其它跟 tryfor 都差不多。
所以从字节码层面来看,没抛错两者的执行效率其实没啥差别。
"那为什么网上流传着try-catch会有性能问题的说法啊?"老陈觉得非常奇怪。
这个说法确实有,在《Effective Java》这本书里就提到了 try-catch 性能问题:
并且还有下面一段话:
正所谓听话不能听一半,以前读书时候最怕的就是一知半解,因为完全理解选择题能选对,完全不懂蒙可能蒙对,一知半解必定选到错误的选项!
《Effective Java》书中说的其实是不要用 try-catch 来代替正常的代码,书中的举例了正常的 for 循环肯定这样实现:
但有个卧龙偏偏不这样实现,要通过 try-catch 拐着弯来实现循环:
这操作我只能说有点逆天,这两个实现的对比就有性能损耗了。
我们直接再跑下有try-catch 的代码和没 try-catch的 for 循环区别,代码如下:
结果如下:
+-差不多,直接看前面的分数对比,没有 try-catch 的性能确实好些,这也和书中说的 try-catch 会影响 JVM 一些特定的优化说法吻合,但是具体没有说影响哪些优化,我猜测可能是指令重排之类的。性能评测
话不多说,我们直接来开始今天的测试,本文我们依旧使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)来进行测试。
首先在 pom.xml 文件中添加 JMH 框架,配置如下: org.openjdk.jmh jmh-core {version}
完整测试代码如下:import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import java.util.concurrent.TimeUnit; /** * try - catch 性能测试 */ @BenchmarkMode(Mode.AverageTime) // 测试完成时间 @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 1 轮,每次 1s @Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s @Fork(1) // fork 1 个线程 @State(Scope.Benchmark) @Threads(100) public class TryCatchPerformanceTest { private static final int forSize = 1000; // 循环次数 public static void main(String[] args) throws RunnerException { // 启动基准测试 Options opt = new OptionsBuilder() .include(TryCatchPerformanceTest.class.getSimpleName()) // 要导入的测试类 .build(); new Runner(opt).run(); // 执行测试 } @Benchmark public int innerForeach() { int count = 0; for (int i = 0; i < forSize; i++) { try { if (i == forSize) { throw new Exception("new Exception"); } count++; } catch (Exception e) { e.printStackTrace(); } } return count; } @Benchmark public int outerForeach() { int count = 0; try { for (int i = 0; i < forSize; i++) { if (i == forSize) { throw new Exception("new Exception"); } count++; } } catch (Exception e) { e.printStackTrace(); } return count; } }
以上代码的测试结果为:
从以上结果可以看出,程序在循环 1000 次的情况下,单次平均执行时间为:循环内包含 try-catch 的平均执行时间是 635 纳秒 75 纳秒,也就是 635 纳秒上下误差是 75 纳秒;循环外包含 try-catch 的平均执行时间是 630 纳秒,上下误差 38 纳秒。
也就是说,在没有发生异常的情况下,除去误差值,我们得到的结论是:try-catch 无论是在 for 循环内还是 for 循环外,它们的性能相同,几乎没有任何差别。
try-catch的本质
要理解 try-catch 的性能问题,必须从它的字节码开始分析,只有这样我能才能知道 try-catch 的本质到底是什么,以及它是如何执行的。
此时我们写一个最简单的 try-catch 代码:public class AppTest { public static void main(String[] args) { try { int count = 0; throw new Exception("new Exception"); } catch (Exception e) { e.printStackTrace(); } } }
然后使用 javac 生成字节码之后,再使用 javap -c AppTest 的命令来查看字节码文件: javap -c AppTest 警告: 二进制文件AppTest包含com.example.AppTest Compiled from "AppTest.java" public class com.example.AppTest { public com.example.AppTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_0 1: istore_1 2: new #2 // class java/lang/Exception 5: dup 6: ldc #3 // String new Exception 8: invokespecial #4 // Method java/lang/Exception."":(Ljava/lang/String;)V 11: athrow 12: astore_1 13: aload_1 14: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V 17: return Exception table: from to target type 0 12 12 Class java/lang/Exception }
从以上字节码中可以看到有一个异常表:Exception table: from to target type 0 12 12 Class java/lang/Exception
参数说明:from:表示 try-catch 的开始地址;to:表示 try-catch 的结束地址;target:表示异常的处理起始位;type:表示异常类名称。
从字节码指令可以看出,当代码运行时出错时,会先判断出错数据是否在 from 到 to 的范围内,如果是则从 target 标志位往下执行,如果没有出错,直接 goto 到 return。也就是说,如果代码不出错的话,性能几乎是不受影响的,和正常的代码的执行逻辑是一样的。
业务情况分析
虽然 try-catch 在循环体内还是循环体外的性能是类似的,但是它们所代码的业务含义却完全不同,例如以下代码:public class AppTest { public static void main(String[] args) { System.out.println("循环内的执行结果:" + innerForeach()); System.out.println("循环外的执行结果:" + outerForeach()); } // 方法一 public static int innerForeach() { int count = 0; for (int i = 0; i < 6; i++) { try { if (i == 3) { throw new Exception("new Exception"); } count++; } catch (Exception e) { e.printStackTrace(); } } return count; } // 方法二 public static int outerForeach() { int count = 0; try { for (int i = 0; i < 6; i++) { if (i == 3) { throw new Exception("new Exception"); } count++; } } catch (Exception e) { e.printStackTrace(); } return count; } }
以上程序的执行结果为:
java.lang.Exception: new Exception
at com.example.AppTest.innerForeach(AppTest.java:15)
at com.example.AppTest.main(AppTest.java:5)
java.lang.Exception: new Exception
at com.example.AppTest.outerForeach(AppTest.java:31)
at com.example.AppTest.main(AppTest.java:6)
循环内的执行结果:5
循环外的执行结果:3
可以看出在循环体内的 try-catch 在发生异常之后,可以继续执行循环;而循环外的 try-catch 在发生异常之后会终止循环。
因此我们在决定 try-catch 究竟是应该放在循环内还是循环外,不取决于性能(因为性能几乎相同),而是应该取决于具体的业务场景。
例如我们需要处理一批数据,而无论这组数据中有哪一个数据有问题,都不能影响其他组的正常执行,此时我们可以把 try-catch 放置在循环体内;而当我们需要计算一组数据的合计值时,只要有一组数据有误,我们就需要终止执行,并抛出异常,此时我们需要将 try-catch 放置在循环体外来执行。
总结
本文我们测试了 try-catch 放在循环体内和循环体外的性能,发现二者在循环很多次的情况下性能几乎是一致的。然后我们通过字节码分析,发现只有当发生异常时,才会对比异常表进行异常处理,而正常情况下则可以忽略 try-catch 的执行。但在循环体内还是循环体外使用 try-catch,对于程序的执行结果来说是完全不同的,因此我们应该从实际的业务出发,来决定到 try-catch 应该存放的位置,而非性能考虑。
好了,我再总结下有关 try-catch 性能问题说法:try-catch 相比较没 try-catch,确实有一定的性能影响,但是旨在不推荐我们用 try-catch 来代替正常能不用 try-catch 的实现,而不是不让用 try-catch。for循环内用 try-catch 和用 try-catch 包裹整个 for 循环性能差不多,但是其实两者本质上是业务处理方式的不同,跟性能扯不上关系,关键看你的业务流程处理。虽然知道try-catch会有性能影响,但是业务上不需要避讳其使用,业务实现优先(只要不是书中举例的那种逆天代码就行),非特殊情况下性能都是其次,有意识地避免大范围的try-catch,只 catch 需要的部分即可(没把握全 catch 也行,代码安全执行第一)。
"好了,老陈你懂了没?"
"行啊yes,BB是一套一套的,走请你喝燕麦拿铁!" 老陈一把拉起我,我直接一个挣脱,"少来,我刚喝过咖啡,你那个倒立洗头,赶紧的!"我立马意识到老陈想岔开话题。
"洗洗洗,我们先喝个咖啡,晚上回去给你洗!"
晚上22点,老陈发来一张图片:
你别说,这头发至少比三毛多。
西藏旅行不去这座城市,你一定会后悔01。hr西藏的西藏,是山南提到西藏,你脑海里最先浮出的记忆是什么?或许是雄伟神圣的布达拉宫,这座以世界屋脊为基石的宫殿,在这天地交汇的地方,升起了关于信仰的图腾。布达拉宫或许是温
在广州,这个新晋的车友打卡地,你来过吗?疫情被隔离在家的时候,总是在想能出去走走该多好!等解封了之后,又在想夜市景区商场开业了就好了等到开业之后又在想,生活好无聊没什么新奇的!不知道,大家有没有这样的感受呢?过于单调的生
上海宝格丽住一晚要多少钱?金额吓退了不少人,连厕所都住不起近年来,旅游业一直是中国大力支持的行业。去年,由于不可抗力,陷入了低谷期,不仅在旅游业,许多与旅游业密切相关的行业也出现了低谷。然而,如今,旅游业已经经历了全面复苏,所有地区的景点
退休大妈爬昆明摩天岭海拔2450多米风景绝美令人神醉刚来昆明没有几天就开始在网上搜索昆明附近有什么山可以爬,因为以前一直居住在重庆,重庆是一座山城,几乎每到周末就要跟驴友一起出去爬山。所以来到昆明住了几天脚就开始闲不住了。经过一番搜
无锡太湖游横跨江浙两个省,中国第三淡水湖。传说陨石从天降,造成巨大冲击坑。百里太湖吳越地,天然湖泊风景区。光福景区在湖东,山水古镇湖湾港。梅桂花木香雪海,吳越文化传古今。阳羡景区在西岸,溶洞
乡村旅游建设中的传统民居与工艺乡村旅游建设中的传统民居与工艺随着乡村振兴,建设新农村,不少乡村结合当地风俗传统,建起了许多传统民居,利用当地旅游资源,以增加当地收入。而采用传统工艺,可以更好吸引人气,使人们通过
无锡融创怎么了?公告说1计划于10月15日开始的异界寻梦季主题活动正式取消,营业时间调整为10001800,期间游乐设备及演出均正常开放(以当天实际开放为准)。210月17日起无锡融创乐园(室外乐
印度百年吊桥为何重新开放仅5天就坍塌?印度古吉拉特邦莫尔比市的百年吊桥10月30日坍塌,导致至少141人遇难,另有至少177人已获救,失踪人数不明。印度警方已经逮捕9人,均与负责维护和运营该吊桥的公司有关联。事发当天,
国家3A级旅游景区,湛江1!等你来打卡好消息好消息湛江特呈岛被确定为国家3A级旅游景区啦湛江市文化广电旅游体育局关于确定湛江市特呈全域旅游岛景区为国家3A级旅游景区的公告特呈岛宛若湛江港湾一颗晶莹的翡翠它东临太平洋,西
济南近郊周末徒步首选之地藏龙涧(龙洞风景区)首游探路笔记路线我走的是浆水泉藏龙涧一线天步行栈道这条路线。1浆水泉上山,全程景色草甸森林,有点小时候剑侠情缘里的画面感,有小路,行进难度较低。整体走了一个小时左右。浆水泉入口处上山处景色1景
全国1。11亿个体工商户超八成无需缴税两年多来累计减税降费超万亿视频加载中21世纪经济报道记者王峰北京报道国务院新闻办公室11月1日举行国务院政策例行吹风会,介绍促进个体工商户发展条例有关情况。市场监管总局副局长蒲淳介绍,我国个体工商户焕发出强