核心是让jit编译器认为异常路径不可预测以避免fast throw并保留c2内联资格;通过移出异常点、用自定义异常替代内置异常、拆分含异常方法及配合jvm参数实现性能与可观测性平衡。

核心思路是:让 JIT 编译器认为异常路径“不可预测”或“不值得优化”,从而避免触发 Fast Throw,同时保留方法本身被 C2 内联的资格。关键不在于消灭异常,而在于打破 C2 对“高频+稳定位置+可复用模板”的判定链条。
把异常抛出点从热点路径中移出
高频异常往往集中在某个 getter、index 访问或类型转换上(如 list.get(i) 或 obj.toString())。若该调用位于一个被频繁执行的方法内部,C2 很容易将其识别为“稳定异常源”。重构时可将实际抛出逻辑下沉到一个独立、非热点、带 guard 的小方法中:
- 原写法:
return items[index].process(); // 可能 ArrayIndexOutOfBoundsException - 重构后:
if (index = items.length) throw new IndexOutOfBoundsException(...); return items[index].process();
这样既显式拦截了异常条件,又让原始方法体不再包含 athrow 字节码——C2 内联时不再因“存在异常出口”而降级决策,也规避了后续 Fast Throw 触发前提。
用受检异常或自定义异常替代内置异常
C2 的 Fast Throw 仅对有限几种内置异常生效(NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException 等)。若业务逻辑允许,可用轻量自定义异常替代:
- 新建
InvalidAccessEx继承RuntimeException - 在关键位置抛出
throw new InvalidAccessEx("index out of range")
该异常不会被 Fast Throw 复用,堆栈始终完整;更重要的是,C2 在分析调用链时,因无法预判其行为模式,更倾向保留内联机会,尤其当该方法本身无其他阻碍内联的特征(如 try-catch 块、synchronized)时。
拆分含异常逻辑的方法,控制内联深度
C2 对含 try-catch 的方法内联非常保守。若某方法既有核心计算逻辑,又包裹了异常处理(例如 try { compute(); } catch (NPE e) { fallback(); }),整个方法大概率不会被内联。应主动拆分:
-
computeCore():纯计算,无异常、无同步、短小(≤35 字节),高概率被内联 -
computeWithFallback():仅负责包装 try-catch 和 fallback 调用,本身不热,不参与内联竞争
这样既保障了热点路径的性能,又让异常处理逻辑脱离编译器优化视野,不再干扰 C2 对关键方法的内联判断。
配合 JVM 参数做轻量干预
重构之外,可用最小侵入方式辅助决策:
- 加
-XX:CompileCommand=exclude,com.example.Service::problematicMethod:临时排除已知问题方法的 C2 编译,防止它拖累整体内联队列 - 慎用
-XX:-OmitStackTraceInFastThrow:关闭 Fast Throw 虽能保堆栈,但会削弱性能且不解决内联抑制问题,仅作诊断用 - 观察
-XX:+PrintInlining输出,确认目标方法是否真正进入 C2 内联流程,而非卡在 C1 或解释执行
重构与参数配合,才能让高频异常场景下,代码既可观测、又不失性能。










