锁升级本身不感知cgroups,但资源争用会放大其开销:jvm锁升级在用户态完成,不读取cgroups配置;而内存和cpu限制会加剧gc、调度紧张、上下文切换,导致更频繁触发重量级锁及自旋失效。

锁升级本身不感知 cgroups,但资源争用会放大其开销
Java 的 synchronized 锁升级(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)完全由 JVM 在用户态基于对象头 Mark Word 状态完成,不依赖系统调用,也不读取 cgroups 配置。它“看不见”容器内存或 CPU 限制。但当你把应用放进 Docker 并设了 --memory=512m 或 --cpus=0.5,实际运行环境就变了:GC 更频繁、线程调度更紧张、上下文切换更多——这些都会让锁竞争场景更容易触发重量级锁,也让轻量级锁的自旋成本显著升高。
内存受限时偏向锁失效加速,Mark Word 状态抖动更明显
偏向锁依赖 JVM 启动时开启 -XX:+UseBiasedLocking(JDK15+ 默认关闭),且要求对象未被 GC 回收过。在内存吃紧的容器里:
-
memory.max设得过小(比如仅比 RSS 高 10%),会导致频繁 minor GC,大量短生命周期对象快速晋升又回收,Mark Word来不及稳定在“偏向”状态就被重置 - OOM Killer 若杀掉 GC 线程或触发
Full GC,会强制所有线程进入安全点(safepoint),此时所有偏向锁批量撤销,日志中出现大量Bias Revocation - 使用
jstat -gc <pid></pid>观察YGCT(Young GC 时间)是否突增;配合jmap -histo:live <pid></pid>看是否有异常对象堆积
CPU 配额不足让自旋锁退化更快,os::is_MP() 判断仍为 true 但效果打折
轻量级锁依赖 CAS 自旋,而自旋是否值得,取决于“等待时间是否小于线程挂起+唤醒开销”。cgroups 下的 CPU 限制会让这个预估彻底失准:
-
--cpus=0.5或cpu.max=50000 100000导致线程即使没被阻塞,也常因配额耗尽被内核调度器暂停,自旋白白消耗配额却等不到锁释放 - JVM 仍认为是多核(
os::is_MP()返回 true),继续走自旋逻辑,但实际效果接近忙等,加剧 CPU time 片争夺 - 可临时加
-XX:PreBlockSpin=10降低默认自旋次数(JDK8),或直接禁用轻量级锁:-XX:-UseHeavyMonitors强制走重量级锁路径(仅调试用) - 用
perf record -e cycles,instructions,context-switches -p <pid></pid>对比容器内外的上下文切换频次,超 10k/s 是明显信号
真正要监控的不是锁状态,而是 cgroups 实时指标与 JVM safepoint 日志
别花时间 dump 所有对象头看 Mark Word 位,那对性能问题没帮助。重点盯住两组数据交叉验证:
- 容器层:
cat /sys/fs/cgroup/memory.current和cat /sys/fs/cgroup/cpu.stat中的nr_throttled(被限频次数)、throttled_time(总限频时长)——若throttled_time持续增长,说明 CPU 配额已成瓶颈,锁升级只是表象 - JVM 层:启用
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1,观察 “vmop” 是否频繁卡在RevokeBias或ThreadDump,同时看 “time” 列是否远高于 10ms - 关键组合:当
throttled_time > 0且 safepoint 平均停顿 > 50ms,基本可断定是 cgroups 限制造成 JVM 运行节奏紊乱,锁行为只是副作用
最易被忽略的一点:Docker 默认不开启 memory.swappiness=0,容器内存压力下仍会 swap,而 swap in/out 会直接导致线程长时间阻塞,让所有锁都变成“重量级”——这不是锁的问题,是内存配置漏项。











