Java 与 JVM
并发与内存模型、线程池与虚拟线程、类加载与 JIT、GC 调优与线上排查,以及构建工具与常用库。
从哪里开始
先读 JMM 与 happens-before 和 volatile、CAS 与 LongAdder,分清可见性、原子性与高竞争计数;再读线程池与虚拟线程;然后读 JVM 调优与线上排查,建立「先看指标、再下结论」的习惯。类加载 与 JIT 与逃逸分析 解释很多「同样的代码表现不同」的现象,按需阅读。
文章
01JMM 与 happens-before多线程读写为什么看不到新值、volatile 与锁各保证什么、final 与安全发布→02volatile、CAS 与 LongAddervolatile 上的 i++ 丢了 600 多万次更新、CAS 重试让计算执行近 6 倍、ABA 何时有害、LongAdder 为什么不能做限额→03两个线程各写各的变量,为什么互相拖慢相邻的两个计数器每次自增 41 纳秒、隔开后 7 纳秒;缓存行、手工填充与 @Contended 的开关、只读线程也被拖慢、被编译器合并的基准→04线程池参数怎么定任务接收顺序、按等待比估算起点、队列与拒绝策略、隔离与线上指标→05超时了、取消了,任务其实还在跑get 超时、cancel、CompletableFuture、shutdownNow 各自对正在运行的任务做了什么;请求登记表的单一终态与结构化并发→06队列设了上限,内存还是涨上去了攒批消费者的五个边界:批上限、队列之外的在途量、按截止时间刷出、批内失败与死信、关闭排空与毒丸→07自动保存显示成功了,最后一次修改却没存上脏标记的两个窗口、逐字段读取存下从未存在过的状态、版本化的不可变快照与按版本条件写入→08信号量有 2 个许可,两个线程却拿到了同一个连接许可只是计数:没借到也归还让上限悄悄变大、重复归还与归还后使用、损坏的资源,以及用租约管理归属→09加了 synchronized,计数还是少了两个实例的同步方法、新建或被重新赋值的锁对象、运行时字符串都锁不住;字面量与缓存内的包装对象全进程共用一把;按业务键取锁对象的表→10一个 parallelStream,拖慢了不相干的接口并行流与默认的 supplyAsync 共用按 CPU 数定的公共池;8 个请求并发时退化成串行;JDK 25 在 1—2 核下不再为异步任务新建线程→11从 AQS 看 ReentrantLock同步队列与条件队列实测、公平锁只剩百分之一的吞吐、tryLock 插队、读写锁与 StampedLock 的边界,以及何时放弃 synchronized→12虚拟线程迁移该不该迁移、JDK 24 之后 pinning 的变化、限流职责的转移,以及怎么验证收益→13JVM 调优与线上排查堆大小与 GC 暂停的实测权衡、三条排查线、堆转储的真实代价→14G1 与 ZGC同一负载下 G1、ZGC、Parallel 的停顿、吞吐与 CPU,G1 退化到 Full GC、ZGC 分配停顿与 System.gc() 实测→15JIT 与逃逸分析同一个方法每次调用分配 0 还是 24 字节:内联、逃逸、双态与超多态调用点的实测→16每次发版后的头几秒,接口都会超时同一个函数第 1 次调用 28 毫秒、稳定后 10 微秒;只用解释器时慢 40 倍;AOT 缓存让首次调用减半;先预热再接流量→17对象头少了 4 字节,为什么有的对象一点没变小JDK 25 压缩对象头:字段偏移、对齐之后谁变小谁不变,一个 HashMap 实测省 16.8% 的堆→18两百万条记录,一百种城市名,占了一百多兆每个 20 字符的字符串约 64 字节;归一后接近 0;G1 字符串去重只共享数组,仍剩 24 字节;多一个汉字变成 88 字节→19同一组数,换个顺序求和,结果就不一样了1000 万笔金额分 7 种块数得到 7 个和、float 累加到 2^24 就停住、Kahan 补偿与它失效的数据、long 过了 2^53 经过 double 丢低位→20轮询计数器溢出:Math.abs 也会返回负数Math.abs(MIN_VALUE) 仍为负、% 与被除数同号、计数器越过最大值时下标 -2、floorMod 与去掉符号位、hashCode 为 MIN_VALUE 的键→21同一个金额,HashSet 里是两个,TreeSet 里是一个==、equals/hashCode、compareTo 三套相等各管各的集合;Long 键用 int 查不到、改了字段的元素删不掉;asList 与 subList 是视图、List.of 不接受 null、toList 不可修改→22加一天,为什么只过了 23 小时时间点与本地时间、夏令时当天的 plusDays 与 plusHours、月末加月不可逆、YYYY 把 12 月 27 日写成下一年、默认解析把 2 月 30 日改成 28 日→23对账差了一分钱2.675 保留两位的五种写法两种结果;浮点转成分一万个错 573 个;各自舍入的拆分合计对不上,最大余数法守恒;分摊结果要存下来→24出了错,日志里却没有可用的信息finally 里的 return、关闭失败盖住原异常、换异常丢了原因、热点代码里没有调用栈的空指针、submit 与周期任务里没人看的异常→25输入只多了 4 个字符,正则的工作量变成 16 倍差一个字符就能匹配的输入让嵌套量词按指数回溯;JDK 8 与 JDK 25 的结果不同,加上反向引用后 JDK 25 仍是指数;改写、预算与长度上限→26浏览器能打开的 HTTPS 地址,Java 调用报 PKIX服务端漏发中间证书与客户端不信任根的报错一字不差;怎么区分与修复;JDK 25 上按 AIA 下载中间证书要两个开关,默认全部拒绝→27类加载与双亲委派同名类不能互相转换、初始化时机、SPI 与上下文类加载器、子优先加载与类加载器泄漏→28Maven Scope五种 scope 在四类路径上的可见性实测,以及打包后类找不到的原因→29两行依赖对调了顺序,运行时少了一个方法同一个库的两个版本谁进类路径:最近者胜出、同样近时先声明者胜出;依赖管理、BOM 导入顺序与显式版本的优先关系;用 Enforcer 在构建时拦住→30Java 字节码操作库选型的关键是能否跟上 JDK:旧版 CGLIB 与 Byte Buddy 在 JDK 21 上的实测结果→31对象映射方案对比MapStruct 与反射相差 100 倍的原因,以及「错误何时暴露」这个更重要的判据→32Guava EventBus异常隔离、DeadEvent、强引用泄漏与同步异步的实测边界→