重载Throwable.fillInStackTrace方法已提高Java性能这样的做法对法?

探讨通过重载Throwable的fillInStackTrace方法提高Java性能的做法及其潜在问题,尤其是在业务异常处理中的应用。

重载Throwable.fillInStackTrace方法已提高Java性能这样的做法对法?

总有哪么些人拿Java异常的性能问题说事,做法是为自定义异常重载Throwable.fillInStackTrace方法,让其返回this。这样做,在测试时确实会快很多,但有什么不妥的地方吗?
关注者
41
被浏览
3022
5 个回答

只要那个自定义异常类型是真的不需要stack trace的,我也会推荐覆写fillInStackTrace()为直接返回this。
爬栈是抛异常开销大的主要原因之一。

<- 但注意好前提就是了——要确定真的肯定绝对不需要stack trace的话。

一个简单的例子是,“滥用”异常来实现某些特殊控制流结构的场景,此时stack trace肯定是没用的,那个异常对象本身其实也没用,只有它的类型和抛出它带来的控制流跳转才有用,那就应该覆写fillInStackTrace()。

=======================================

然后从反面举个例子。HotSpot VM有个许多人觉得“匪夷所思”的优化,叫做fast throw:有些特定的隐式异常类型(NullPointerException、ArithmeticException( / 0)之类)如果在代码里某个特定位置被抛出过多次的话,HotSpot Server Compiler(C2)会透明的决定用fast throw来优化这个抛出异常的地方——直接抛出一个事先分配好的、类型匹配的异常对象。这个对象的message和stack trace都被清空。抛出这个异常的速度是非常快,不但不用额外分配内存,而且也不用爬栈;但反面就是可能正好是需要知道哪里出问题的时候看不到stack trace了。
从Sun JDK5开始要避免C2做这个优化还得额外传个VM参数:-XX:-OmitStackTraceInFastThrow。

覆写fillInStackTrace()为直接返回this就像是人肉做C2所做的那种优化…的一部分效果。反正肯定会有人抱怨这样不好的啦,是不是要顶住压力硬上就看到底在特定场景里带来的性能好处是不是真的那么重要了。
可以。但是最主要的是取决于你怎么使用Exception。
有种观点认为,业务失败异常流程应该基于Exception控制,在这样的项目里就会看到大量的基于业务定义的Exception类,比如UserNotFoundException,LoginFailException什么的。或者把Service层所有的异常分支都包装成一个ServiceException什么的。这种情况下,throw Exception 就成为一个很常见的事件,这时重载fillInStackTrace 是可以有效益的。
但是我看到的大多数情况,大家还是把Exception作为技术上的异常而不是业务上的异常。所以,理论上要用到异常的时候不多。要真是发生了Exception,由于超出预期,反而恰恰需要stack trace。
JDK7开始已经原生支持爬栈开关
重载fillInStackTrace在业务异常中很常见,但是如果一定要说弊端的话,如果想要stack的时候反而没有办法了,屏蔽异常栈主要是为了不执行private native Throwable fillInStackTrace(int dummy);这个方法而提高效率,出于这个目的考虑的话有更好的方案,动态决定需不需要异常栈——新增业务异常增加构造函数,用参数决定是否需要异常栈。调用Throwable的构造函数:
protected Throwable(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace);
参数writableStackTrace直接可以决定需不需要执行fillInStackTrace来提高性能。
如果楼主做的是业务系统的话,建议避免使用这种做法来提高性能,从其他方面考虑吧。像R大说的,不需要stacktrace信息的时候,可以覆盖这个方法达到提高异常性能的目的。不过,业务系统变化太快,今天不需要异常信息,说不定明天就需要了。我自己就遇见过这样的case。
### ### 识别堆栈中产生大量 GC 的方法调用 在提供的堆栈信息中,存在多个调用链,其中某些方法调用可能因频繁创建临时对象而引发大量垃圾回收(GC)。根据堆栈信息和引用内容,可以识别出以下关键调用路径及其对 GC 的影响。 --- ### ### 异常堆栈打印导致大量字符串对象生成 堆栈中多次出现 `Throwable.printStackTrace()` 的调用,例如: ``` java.lang.Throwable.printStackTrace(Throwable.java:686) java.lang.Throwable.printStackTrace(Throwable.java:753) com.amap.location.support.n.a.b(ALLog.java:98) ``` 该方法会生成完整的异常堆栈信息,涉及多个字符串拼接和格式化操作。每次调用都会创建新的字符串对象,这些对象生命周期短,会迅速进入新生代并触发 Minor GC。如果异常频繁抛出并打印堆栈,GC 频率会显著上升,影响性能[^1]。 ```java // 示例:异常堆栈打印生成大量字符串 try { // 业务逻辑 } catch (Exception e) { e.printStackTrace(); // 生成大量字符串对象 } ``` --- ### ### 日志记录中的字符串拼接操作 `com.amap.location.support.n.a.b(ALLog.java:98)` 和 `com.amap.location.support.n.a.a(ALLog.java:47)` 位于日志记录路径中。日志记录通常涉及字符串拼接、格式化等操作,若日志内容复杂或频繁写入,会导致大量临时字符串对象的创建,从而增加 GC 压力。 ```java // 示例:频繁日志记录生成大量字符串 logger.info("用户ID: " + userId + ",操作失败: " + errorMessage); ``` --- ### ### 数据库操作中的临时对象创建 堆栈中还涉及数据库操作路径: ``` com.amap.location.c.a.b.a(LocationDbHelper.java:337) com.amap.location.c.a.b.a(LocationDbHelper.java:157) ``` 这类方法可能涉及数据库记录的构建、查询参数的拼接等操作。若使用字符串拼接方式构建 SQL 语句或处理数据,同样会生成大量临时对象,从而增加 GC 负担。 ```java // 示例:拼接 SQL 语句生成字符串 String query = "SELECT * FROM users WHERE id = " + id; ``` --- ### ### 方法调用链中频繁的对象创建行为 在多个调用路径中,如: ``` com.amap.location.k.c.a.d(SaveController.java:254) com.amap.location.k.c.a.b(SaveController.java:244) ``` 这些方法可能涉及数据封装、对象转换等操作。若每次调用都创建新对象,而未复用已有对象或使用对象池机制,则会导致频繁的内存分配,从而增加 GC 触发频率。 ```java // 示例:频繁创建对象导致 GC for (int i = 0; i < 10000; i++) { Data data = new Data(); // 每次创建新对象 process(data); } ``` --- ### ### 优化建议与减少 GC 的方法 为减少 GC 压力,应优化以下行为: 1. **避免频繁打印异常堆栈** 使用日志框架控制日志级别,仅在必要时记录异常堆栈,避免在生产环境中频繁调用 `printStackTrace()`。 2. **使用 StringBuilder 替代字符串拼接** 在频繁拼接字符串的场景中,优先使用 `StringBuilder`,减少中间字符串对象的创建。 3. **复用对象或使用对象池** 对于频繁创建和销毁的对象,可采用对象池机制,避免重复分配内存。 4. **异步日志记录** 使用异步日志框架(如 Log4j2 的 AsyncAppender)将日志写入操作与主线程解耦,降低主线程的 GC 压力。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值