《Java 并发编程·性能诊断与事故》14.1 availableProcessors 与 CPU 配额
limits.cpu=2 的 Pod 里 JVM 到底看到几个核?本节用 cgroup v2 的 cpu.max 讲清 availableProcessors 的取值来源,在宿主与 colima 容器中实测 0.5/1/2/4/10/16 核下的 commonPool 并行度,并证明 -XX:ActiveProcessorCount 只能模拟核数、模拟不了 CFS 节流。
tag
limits.cpu=2 的 Pod 里 JVM 到底看到几个核?本节用 cgroup v2 的 cpu.max 讲清 availableProcessors 的取值来源,在宿主与 colima 容器中实测 0.5/1/2/4/10/16 核下的 commonPool 并行度,并证明 -XX:ActiveProcessorCount 只能模拟核数、模拟不了 CFS 节流。
把 GC 从 G1 换成 ZGC 之后 p99 没降,问题往往不在收集器:先用同一负载对照 G1、ZGC 与分代 Shenandoah 的暂停与尾延迟,再给出低延迟下的四条并发设计约束——压分配速率、正确复用对象、避开大对象、处理伪共享,并附症状到参数动作的对照表。
把 G1 的并发标记拆成写屏障、并发线程和停顿三段:用 SATB 预写屏障解释为什么改引用也要记账,用 ConcGCThreads=1 与 6 的对照日志证明标记线程只是把同一份 CPU 工作摊到更多核上,再用 JFR 的 jdk.GCPhaseConcurrent 聚合各阶段耗时,给出业务线程侧的观测口径。
从一次「GC 次数高就该加大堆」的误判出发,用 ThreadMXBean.getThreadAllocatedBytes 与 JFR 分配事件实测 9.1 GB/s 的分配速率,拆解 TLAB 的 512 KB 填充与 184 字节/次的来源,再用三组逃逸分析对照说明标量替换为什么让分配计数归零,并纠正 HotSpot 并不做栈上分配。
把 150 个/秒的负载打进 4 线程、64 槽的有界池,按秒采集队列填充率、拒绝率、活跃占比与端到端 p99,用可运行的状态机验证持续时间窗口、连续次数与分级告警,并指出服务内 p99 只有 55 ms 而端到端已 792 ms 的指标盲区。
以 200 ms 临界区把 4 个线程打成 BLOCKED 为场景,说明 ThreadMXBean 能给出状态、锁与持有者却给不出请求归属,ThreadLocal 又无法跨线程读取,于是用线程 ID 注册表把阻塞归因到 requestId,并给出结构化日志、采样策略与实测的扫描开销。
从一次「active=8、queue=0 却仍被判定池满」的误判出发,逐条拆解 ThreadPoolExecutor 的 active、pool、queue、completed、taskCount 各自在数什么、哪些是快照哪些是估值,给出拒绝计数器的自埋写法,并用 120 个任务的实跑时序数据核对 getTaskCount 的分解式与跨 getter 的不一致。
热点到底是锁还是别的东西?本节先用 ThreadMXBean 与 JFR 定位锁竞争——实测 blockedCount 只认 synchronized、ReentrantLock 要去看 jdk.ThreadPark;再把单锁改成 16 路分片与 LongAdder,给出本机 8 线程下吞吐、p50、p99、p999 的真实对照,并说明 p99 为什么可能骗人、无锁化的边界在哪。
分布式锁的本质是一段有期限的授权,看门狗只能降低概率,不能消除“租约过期但旧持有者仍在写”。本节用不依赖 Redis/ZooKeeper/etcd 的单机 ReentrantLock 加人造时钟复现旧持有者复活,实测 fencing token 把过期写入挡在存储侧,并给出时钟漂移、看门狗失效与续期失败的边界。
Java 侧锁顺序正确,事务里的 SQL 顺序却反了;或者 Java 锁与连接池交叉等待,JVM 死锁检测完全看不见。本节用纯 JDK 的行锁替身与连接池复现两类叠加死锁,给出真实线程转储判据、未在本机验证的 pg_locks 与 innodb status 读法,以及三者的耦合边界。
线程池不 shutdown、ThreadLocal 滞留大对象、线程上下文类加载器钉住插件加载器,是三种不同的泄漏。本节用 100 个未关闭的池复现 200 个常驻 worker,用 200 次 URLClassLoader 热部署复现 200 个无法回收的加载器,并用 jcmd GC.class_histogram 与 jmap -histo:live 做前后对照。
重试不会创造容量,只会把拒绝率换算成更高的到达率。本节在 8 线程、64 队列、2000/s 到达的构造场景里实测:3 次重试把尝试数放大 2.53 倍,却只多完成 0.9% 的请求;同时对比 CallerRunsPolicy 占用提交线程 9 ms、DiscardPolicy 静默丢掉 48% 的任务,并给出拒绝不重试的落地写法。
无界队列不会避免过载,只会把过载从拒绝变成堆增长。本节用 2 个 worker、1000/s 到达、64 KB 消息体在 -Xmx256m 下真实跑出 OOM,给出堆积速率与堆增长的算式,并用 jcmd GC.class_histogram 定位到占着约 145 MiB 的 byte[],最后给出有界队列与水位拒绝的落地写法。
把过期读与半成品对象两起构造事故收进同一张归因表,给出从现象、违规的 happens-before 规则、三级证据到修复验证的完整链路,附可复用的自旋线程检测器与回归测试,并如实记录回归测试 2200 万次读取 0 命中的能力边界与本机复现数据。
构造构造器注册回调导致的 this 逸出,用逐次计数的命中统计复现半成品对象;再用四个 JCStress 用例分别测量非 final 字段、final 字段、this 逸出与 volatile 发布的命中率,给出本机真实样本数、两轮频率与修复写法。
从停不掉的停机标志出发,构造非 volatile 标志导致的无限循环,用线程转储、JFR 采样与 PrintCompilation 定位到 C2 把字段读提升到循环外,并在本机用 30 次独立 JVM 运行逐档对照 Xint、C1、C2 与 volatile 修复的复现率。
从构造的负库存现象出发,给出把 1024 并发缩到 2 个 actor、库存缩到 1 的四步最小化路径;用本机两轮 JCStress 命中率与 StressSeed 说明概率复现的边界,并给出可阻断 CI 的回归测试、Maven profile 与 nightly 成本预算。
从库存扣减的读改写解释线性化点,用 Lincheck 的模型检验系统地枚举线程交错;给出 Kotlin + Maven 的最小工程、场景生成参数与失败报告读法,并如实标注本机未安装 Kotlin 运行时、2.44 在 Maven Central 不存在(最新 2.39),全部结果未在本机执行。
从库存扣减的读改写竞态出发,拆解 JCStress 0.16 的 @JCStressTest、@State、@Actor、@Arbiter、@Outcome 与 Expect 分工,给出自定义结果类型的完整示例;并在本机 JDK 25 复现 proc=full 缺失导致的 TestList 空指针,再实跑两轮记录 FORBIDDEN 命中率。
事件循环、虚拟线程、平台线程池同时存在时,事故往往发生在边界上:阻塞调用混进循环、上下文在换线程时丢失、jstack 看不到未挂载的虚拟线程。本节用本机实测标出三种边界与 trace 断点,并给出线程转储、JFR 事件与边界策略的具体判读方法。