这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
《Java 并发编程·性能诊断与事故》15.1 编码规范与静态检查
把并发编码规范落成可执行条目:锁对象 private final 且不暴露、线程命名、池必须关闭、ThreadLocal 必须 remove、跨模块锁顺序与层号,并给出扩展卷二 15.3 的 X/Y/Z 三类规则的可运行检查脚本,附本机 javac -Xlint 与 20 处命中的真实输出。
posts
这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
把并发编码规范落成可执行条目:锁对象 private final 且不暴露、线程命名、池必须关闭、ThreadLocal 必须 remove、跨模块锁顺序与层号,并给出扩展卷二 15.3 的 X/Y/Z 三类规则的可运行检查脚本,附本机 javac -Xlint 与 20 处命中的真实输出。
同样的 2 vCPU,为什么 requests-only 的 Pod 开 63 个线程、limits=2 的 Pod 每 100 ms 被冻结一次?本节用 cpu.max、cpu.stat 与实测间隔证明 availableProcessors 看不见 CFS 节流,给出 1/2/4/8 核四档 requests、limits、堆、线程池与探针的基线表和一份完整 Deployment。
容器 limit 256 MiB、堆只开了 128 MiB,进程却被 Killed,日志里一行 Java 异常都没有。本节拆开 MaxRAMPercentage 的默认 25%、-Xss 的平台差异和虚拟线程的栈归属,实跑 heap、Metaspace、native thread、OOMKilled 四种结局,并给出容器内存的参数表与核算方法。
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 的暂停与尾延迟,再给出低延迟下的四条并发设计约束——压分配速率、正确复用对象、避开大对象、处理伪共享,并附症状到参数动作的对照表。
在线推荐最常见的性能事故,是在请求链路里现算长周期特征,p99 会从几十毫秒涨到近一秒。本文拆开离线特征与在线轻量计算,用 Missing 和 L2 归一化守住输入契约,并给出可复现的 Go 测试与 p99 退化案例。在线服务只做查找、拼接与截断。
把 G1 的并发标记拆成写屏障、并发线程和停顿三段:用 SATB 预写屏障解释为什么改引用也要记账,用 ConcGCThreads=1 与 6 的对照日志证明标记线程只是把同一份 CPU 工作摊到更多核上,再用 JFR 的 jdk.GCPhaseConcurrent 聚合各阶段耗时,给出业务线程侧的观测口径。
从一次「GC 次数高就该加大堆」的误判出发,用 ThreadMXBean.getThreadAllocatedBytes 与 JFR 分配事件实测 9.2 GiB/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% 的任务,并给出拒绝不重试的落地写法。
推荐服务与业务服务的边界一旦模糊,推荐侧就会悄悄依赖内容库表结构,内容团队一次字段迁移就可能让首页空白。本文用候选合并函数说明如何只交换 ID 与分数,把“能不能推”留在业务侧,并给出可运行的 Go 测试与失败场景。边界清晰后,内容下线与运营插位都不必改模型服务。
无界队列不会避免过载,只会把过载从拒绝变成堆增长。本节用 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 发布的命中率,给出本机真实样本数、两轮频率与修复写法。