《Java 虚拟机原理与性能调优》4.3 反优化与 OSR:代码什么时候退回解释器
加载一个插件类,热了五分钟的接口调用突然退回解释器。本节用 -XX:+PrintCompilation 抓 marked for deoptimization 与 uncommon trap,用 JFR 的 jdk.Deoptimization 事件看 class_check 的 bci、原因与动作,再解释 OSR 为什么在循环中途换代码,以及 libraryd 插件热加载该怎么避开反优化。
tag
加载一个插件类,热了五分钟的接口调用突然退回解释器。本节用 -XX:+PrintCompilation 抓 marked for deoptimization 与 uncommon trap,用 JFR 的 jdk.Deoptimization 事件看 class_check 的 bci、原因与动作,再解释 OSR 为什么在循环中途换代码,以及 libraryd 插件热加载该怎么避开反优化。
同样的罚金计算,走单态调用是 0.74 ns/op,走四实现的多态调用是 3.44 ns/op。本节用 -XX:+PrintInlining 读内联与拒绝原因,用 -XX:-DoEscapeAnalysis 对照 GC 次数,再用 JMH 1.37 跑单态、双态与巨态分派,最后落到 libraryd 的 FinePolicy 策略设计。
同一个 FinePolicy.fineCents,启动时跑在解释器里,几十毫秒后已经被 C2 编译。本节用 -XX:+PrintCompilation 的真实输出拆解列含义与 tier 3/4 迁移,用 jcmd Compiler.queue、Compiler.codecache 看编译队列和代码缓存,并对照 -XX:TieredStopAtLevel=1 的峰值与启动耗时。
80 万个 LoanEvent 在 1.03 秒内分配完,折合 2.5 GB/s。本节用 -Xlog:gc+tlab=debug 读 TLAB totals、用 gc+plab 看 PLAB、用 gc+region 定位 Humongous,再用 JFR 的 jdk.ObjectAllocationSample 找分配热点,最后讨论 libraryd 每请求创建 LoanEvent 的取舍。
一个 Loan 到底占多少内存?字段宽度相加是 36 字节,JOL 说是 48。本节用 JOL 0.17 打印 Book、Small、Padded、int[3]、Loan 与 FinePolicy 的对象布局,解释 mark word、压缩类指针、字段重排与 8 字节对齐,再用 -XX:-UseCompressedOops 对照,最后核算 100 万 Loan 的缓存预算。
线上 RSS 比 -Xmx 多出 1 GB,钱花在哪?本节用 MemoryShape 复现 libraryd 的线程与内存形状,跑 -XX:NativeMemoryTracking=summary 与 jcmd VM.native_memory、VM.metaspace、GC.heap_info,逐项解释 reserved/committed/used,并给出堆、元空间、直接内存与线程栈的预算表。
方法调用在 JVM 里就是一个栈帧。本节用 javap -c 的真实输出拆开局部变量表、操作数栈、动态链接与异常表,让五种调用指令各出现一次,并逐指令走完 FinePolicy.DEFAULT.fineCents(20) 的 1000 分计算过程。
class 文件变成可用的类要经过加载、链接、初始化。本节用 -Xlog:class+load 与 jcmd VM.classloaders 的输出拆开这三步,实测 Class.forName 与 loadClass 的初始化差异、两个 URLClassLoader 加载同一 jar 的类身份冲突,并落到 libraryd 的 PluginClassLoader 与 ServiceLoader。
用 javap -v -p 和 xxd 拆开 Book 与 FinePolicy 的 class 文件:magic 与 major 69、常量池的描述符与两槽常量、字段与方法表、Code/LineNumberTable/BootstrapMethods/Record 属性,并解释 record 的 toString/equals/hashCode 为何是 invokedynamic。
工具不是越多越好,每个工具只回答一类问题。本节给出 jcmd help 的真实命令清单与开销分级,实测 8 秒 JFR 记录的 summary、热点与分配视图,展示 GC 日志字段和 ManagementFactory 快照代码,并划清 JMC、MAT、async-profiler 的边界。
同一份代码在两个堆大小下会给出不同结论。本节固定 JDK、CPU、堆、GC 与启动参数,用 VM.flags 和 GC.heap_info 实测 256 MB、1 GB、2 GB 与 -Xms≠-Xmx 的差异,验证 ActiveProcessorCount 对 GC/JIT 线程数的影响,并给出 collect.sh。
libraryd 的延迟抬升不能靠猜。本节把「现象→假设→实验→证据→结论」拆成可执行步骤,用 jcmd 的 VM.info、VM.flags、GC.heap_info 真实输出演示字段怎么读,并给出症状到第一工具的对照表与四条研究路径的分工。