Java JDK17 ZGC环境下JIT反优化引发请求延迟问题咨询
JDK17+ZGC下JIT反优化导致请求变慢的问题分析
问题背景
在JDK17环境中使用ZGC时,出现部分请求执行耗时过长的情况。查看GC日志发现,jit,compilation操作的线程ID对应用户线程(4236),且执行时间与慢请求发生时间完全吻合。需要解答以下问题:
- 为何
jit,compilation会在用户线程中执行? - 它为何会阻塞用户线程?
- JIT反优化(日志中
made not entrant)是否消耗大量时间?期间具体执行了哪些操作? - 如何避免该问题?
相关GC日志
注:579为Java-Compiler线程,4236为用户线程
[2024-03-27T10:26:37.675+0800][579 ][jit,compilation] 27257 3 java.net.InetAddress::getAllByName (388 bytes) made not entrant [2024-03-27T10:26:38.572+0800][579 ][jit,compilation] 46702 4 Script_1711505411710_68000::execute0 (112 bytes) [2024-03-27T10:26:38.576+0800][579 ][jit,compilation] 39531 3 Script_1711505411710_68000::execute0 (112 bytes) made not entrant [2024-03-27T10:26:38.716+0800][4236][jit,compilation] 42949 ! 4 com.google.common.cache.LocalCache$Segment::get (122 bytes) made not entrant [2024-03-27T10:26:38.716+0800][4236][jit,compilation] 35244 4 com.google.common.cache.LocalCache::isExpired (57 bytes) made not entrant [2024-03-27T10:26:38.716+0800][4236][jit,compilation] 28530 4 com.google.common.cache.LocalCache$Segment::recordRead (29 bytes) made not entrant
问题解答
1. 为何jit,compilation会在用户线程中执行?
常规JIT编译由专门的Java Compiler后台线程(如日志中的579)异步完成,但JIT反优化(deoptimization)操作必须在触发它的用户线程中同步执行。当JVM检测到已编译的优化代码存在错误(比如类型推测失效、方法被动态覆盖、缓存逻辑变更导致代码假设不成立),需要撤销优化并回退到解释执行或重新编译时,这个过程涉及当前用户线程的栈帧调整、执行状态恢复,无法由后台线程代劳,只能在当前执行该代码的用户线程中完成。
2. 为何反优化会阻塞用户线程?
反优化是同步阻塞操作:
- 用户线程在执行业务代码时触发反优化后,必须暂停当前业务逻辑,优先完成反优化流程。
- 流程中需要调整调用栈帧(将编译代码的栈帧转换为解释执行格式)、标记方法为
not entrant(禁止新线程进入该编译代码),如果有其他线程正在执行该已编译方法,还需等待这些线程退出。 - 只有等所有反优化操作完成,用户线程才能继续执行业务代码,因此直接导致请求耗时变长。
3. JIT反优化的耗时与具体操作
耗时情况
反优化的耗时取决于三个核心因素:
- 被反优化方法的调用深度:调用栈越深,需要调整的栈帧越多,耗时越长。
- 单次触发的反优化方法数量:如日志中同时处理LocalCache相关的多个方法,耗时会累加。
- 并发执行该方法的线程数:如果有其他线程正在执行该编译代码,需要等待这些线程退出,会额外增加阻塞时间。
单次反优化通常耗时几毫秒到几十毫秒,但如果频繁触发,会显著拉低请求的平均响应时间。
具体操作
当日志中出现made not entrant标记时,JVM会执行以下步骤:
- 标记方法状态:将该编译后的方法标记为
not entrant,禁止新线程进入该编译代码执行。 - 栈帧转换:扫描当前用户线程的调用栈,找到所有调用该方法的栈帧,将其从编译代码的栈帧格式转换为解释执行的栈帧格式,或替换为未优化的方法版本。
- 清理缓存:清除该方法关联的JIT编译缓存、类型推测结果等,避免后续复用错误的优化代码。
- 触发重新编译(可选):如果条件允许,JVM会通知后台编译线程重新编译该方法的正确版本。
4. 如何避免或缓解该问题
- 减少动态代码变更:避免运行时动态修改类、生成新类(如频繁热部署、动态代理),这类操作极易触发JIT反优化。
- 调整JIT编译参数:
- 确保
-XX:+TieredCompilation开启(JDK17默认开启),合理调整编译层级,减少层级切换引发的反优化; - 调高
-XX:CompileThreshold参数,提高方法触发JIT编译的调用次数阈值,避免频繁编译-反编译; - 添加
-XX:+PrintDeoptimizationDetails参数,打印详细反优化日志,精准定位触发反优化的根源。
- 确保
- 优化业务代码:
- 热点方法中避免使用易触发类型推测失败的逻辑(如频繁类型转换、泛型擦除后的类型判断);
- 针对日志中涉及的Guava Cache,减少修改缓存过期策略、频繁移除缓存条目等操作,或改用更稳定的缓存实现。
- 提前预热核心代码:系统启动阶段,对核心业务方法进行预热调用,触发JIT编译并稳定代码状态,避免运行期因首次编译或反优化影响请求。
内容的提问来源于stack exchange,提问作者yiming xie
相关产品推荐
相关产品推荐

