<?xml version="1.0" encoding="utf-8"?>
<ul>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本附录汇总本书常用命令、参数和排查路径，便于日常快速查阅。不同 JDK 版本和发行版的行为可能不同，生产使用前应以当前运行时输出和官方文档为准。1. 常用命令进程与基本信息# 查看 Java 进程jps -l# 查看系统进程ps -ef | grep java# 查看 JVM 启动参数jcmd &lt;pid&gt; VM.command_line# 查看最终 flagsjcmd &lt;pid&gt; VM.flags# 查看所有 flagsjava -XX:+PrintFlagsFinal -version内存与 GC# 堆信息jcmd &lt;pid&gt; GC.heap_info# GC 概览jstat -gc &lt;pid&gt; 1000jstat -gcutil &lt;pid&gt; 1000# 类直方图，可能开销较大jcmd &lt;pid&gt; GC.class_histogram# 手动堆 dumpjcmd &lt;pid&gt; GC.heap_dump /data/dump/app.hprof# 触发 Full GC，慎用jcmd &lt;pid&gt; GC.run线程与诊断# 线程 dumpjcmd &lt;pid&gt; Thread.print &gt; thread-1.txtjstack &lt;pid&gt; &gt; thread-1.txt# 连续采集for i in 1 2 3 4 5; do  jcmd &lt;pid&gt; Thread.print &gt; "thread-$i.txt"  sleep 2done# JFRjcmd &lt;pid&gt; JFR.checkjcmd &lt;pid&gt; JFR.start name=live settings=profile filename=/log/live.jfr maxsize=512mjcmd &lt;pid&gt; JFR.dump name=live filename=/log/now.jfrjcmd &lt;pid&gt; JFR.stop name=liveLinux 排查# 进程 CPUtop -p &lt;pid&gt;# 线程 CPUtop -H -p &lt;pid&gt;ps -L -p &lt;pid&gt; -o pid,tid,%cpu,%mem,state,comm --sort=-%cpu# 十进制线程号转十六进制printf "%x\n" &lt;tid&gt;# 系统指标vmstat 1pidstat -p &lt;pid&gt; -t 1iostat -x 12. 常用 JVM 参数内存            参数      用途                  -Xms      初始堆              -Xmx      最大堆              -Xmn      新生代              -Xss      线程栈              -XX:NewRatio      Old 与 New 比例              -XX:SurvivorRatio      Eden 与 Survivor 比例              -XX:MaxMetaspaceSize      元空间上限              -XX:MaxDirectMemorySize      直接内存上限              -XX:SoftMaxHeapSize      ZGC 等软堆目标      收集器-XX:+UseSerialGC-XX:+UseParallelGC-XX:+UseG1GC-XX:+UseZGC-XX:+UseShenandoahGC   # 需运行时支持-XX:MaxGCPauseMillis=50CMS 相关：            JDK      状态                  JDK 8      可用              JDK 9      deprecated              JDK 14      移除      容器-XX:+UseContainerSupport-XX:InitialRAMPercentage=60.0-XX:MaxRAMPercentage=60.0-XX:MinRAMPercentage=50.0-XX:ActiveProcessorCount=4容器内存预算：容器 limit  &gt;= Heap   + Metaspace   + Thread stacks   + Direct memory   + CodeCache   + Native memory   + OS 余量3. 日志配置现代 JDK：java -Xlog:gc*,safepoint:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=50m \  -jar app.jarJDK 8：java -XX:+PrintGCDetails \  -XX:+PrintGCDateStamps \  -XX:+PrintTenuringDistribution \  -Xloggc:/logs/gc.log \  -XX:+UseGCLogFileRotation \  -XX:NumberOfGCLogFiles=10 \  -XX:GCLogFileSize=50M \  -jar app.jarOOM 证据：-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/data/dump-XX:+ExitOnOutOfMemoryError4. 工具速查            工具      用途                  javap      查看字节码              jstat      GC 统计              jmap      堆信息与 dump              jcmd      推荐 JVM 命令入口              jstack      线程 dump              jhsdb      调试服务              jfr      查看 JFR 事件              MAT      堆 dump 分析              JMC      JFR 可视化              Arthas      在线诊断              GCEasy / GCViewer      GC 日志可视化              async-profiler      CPU 与分配采样      5. 字节码命令# 编译javac -g Demo.java# 查看字节码javap -c Demo.class# 查看常量池和详细信息javap -v Demo.class# 查看私有成员javap -p Demo.class常见指令：            指令      含义                  aload_0      加载 this              iload_1      加载第 1 个 int 参数              istore_1      保存 int 到局部变量              invokevirtual      调用虚方法              invokespecial      构造、private、super              invokestatic      调用静态方法              invokeinterface      调用接口方法              invokedynamic      Lambda、字符串拼接等动态调用              new      创建对象              monitorenter      进入 monitor              monitorexit      退出 monitor      6. 线程状态            状态      含义                  NEW      尚未启动              RUNNABLE      可运行，可能等待 CPU 或 Native IO              BLOCKED      等待 monitor              WAITING      无限期等待              TIMED_WAITING      限时等待              TERMINATED      结束      死锁特征：thread A waiting to lock L1, held by Bthread B waiting to lock L2, held by A处理方向：统一加锁顺序、缩小锁范围、tryLock 超时、拆分锁粒度、避免锁内 IO。7. GC 选择速查            场景      起点                  小内存、低并发      Serial              批处理、吞吐优先      Parallel              常见在线服务      G1              大堆、低停顿、资源充足      ZGC              发行版支持且压测通过      Shenandoah      G1 关键点：Region 化堆Young / Mixed / Full GCRemembered Set可设置停顿目标ZGC 关键点：着色指针读屏障并发整理Allocation StallJDK 21 分代模式8. OOM 类型速查            类型      方向                  Java heap space      堆 dump、GC 日志、对象泄漏              GC overhead limit exceeded      存活对象高、堆余量不足              Metaspace      类加载器、动态类生成              Unable to create new native thread      线程数、栈大小、OS 限制              Direct buffer memory      NIO、Netty、直接内存              Requested array size exceeds VM limit      超大数组              OOMKilled / exit 137      容器总内存超限      应急流程：摘流量  -&gt; 保存日志和 dump  -&gt; 记录监控和事件  -&gt; 重启或替换  -&gt; 分析根因  -&gt; 灰度验证9. CPU 高排查1. top 判断 user/sys/iowait2. top -H 找高 CPU 线程3. printf 转十六进制4. 线程 dump 搜索 nid5. 判断业务 / GC / JIT 线程6. JFR 或 Arthas 定位方法7. 结合流量和数据特征8. 修复后压测验证Arthas：dashboardthread -n 5thread -btrace com.demo.Service method '#cost &gt; 100' -n 10watch com.demo.Service method params returnObj throwExp -x 2 -n 5stack com.demo.Service method -n 5stop10. 内存泄漏常见来源            来源      特征                  静态集合      Retained Heap 大且持续增长              ThreadLocal      线程池线程持有              监听器      未注销              无界缓存      key 持续增长              大查询      byte[]、ArrayList、String 巨大              ClassLoader      同名类大量重复加载              队列积压      生产快于消费      MAT 关键视图：HistogramDominator TreePath to GC RootsLeak SuspectsThread Overview重点指标：Shallow Heap：对象自身大小Retained Heap：回收该对象可释放总量11. JFR 模板启动时：java -XX:StartFlightRecording=filename=/logs/app.jfr,settings=profile,maxsize=1g,maxage=6h,dumponexit=true \  -jar app.jar告警后：jcmd &lt;pid&gt; JFR.dump name=&lt;recording&gt; filename=/logs/incident.jfr看什么：CPU LoadExecution SampleAllocation SampleJava Monitor BlockedThread ParkGarbage CollectionSafepointException Statistics12. 容器检查清单1. memory request 与 limit2. CPU request 与 limit3. -Xmx 或 MaxRAMPercentage4. MaxMetaspaceSize5. MaxDirectMemorySize6. 线程数和 -Xss7. ActiveProcessorCount8. GC 日志目录挂载9. dump 目录挂载10. Java 进程能收到 SIGTERM11. 优雅停机时间小于 terminationGracePeriodSeconds12. 健康检查和就绪检查推荐启动模板：exec java \  -XX:+UseContainerSupport \  -XX:InitialRAMPercentage=60.0 \  -XX:MaxRAMPercentage=60.0 \  -XX:ActiveProcessorCount=4 \  -XX:MaxMetaspaceSize=512m \  -XX:MaxDirectMemorySize=1g \  -XX:+HeapDumpOnOutOfMemoryError \  -XX:HeapDumpPath=/data/dump \  -XX:+ExitOnOutOfMemoryError \  -Xlog:gc*,safepoint:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=50m \  -XX:StartFlightRecording=filename=/logs/app.jfr,settings=profile,maxsize=512m,maxage=6h,dumponexit=true \  -jar /app/app.jar13. 监控指标必须监控：process_cpucontainer_cpu_usagecontainer_cpu_throttlingcontainer_memory_usagejvm_heap_usedjvm_heap_committedjvm_heap_maxjvm_gc_pausejvm_gc_countjvm_gc_timejvm_metaspacejvm_direct_memoryjvm_thread_countjvm_class_loadedsafepoint_timehttp_p50http_p99http_p999error_rate建议告警：            指标      方向                  Full GC      大于 0 需关注              GC P99      超过延迟目标              Allocation Stall      大于 0              回收后 Old      持续增长              memory usage / limit      大于 85%              CPU throttling      持续增长              线程数      突增或接近上限              重启次数      异常增长      14. 排障模板时间：实例：版本：现象：系统指标：  CPU =  Memory =  Disk =  Network =JVM：  Heap =  GC =  Threads =  Metaspace =  Direct memory =证据：  GC log =  Thread dump =  Heap dump =  JFR =  Business log =分析：  1.  2.  3.根因：修复：验证：预防：15. 版本提醒            主题      重要差异                  永久代      JDK 8 移除，使用 Metaspace              CMS      JDK 14 移除              偏向锁      JDK 15 起 deprecated              ZGC      JDK 15 转正，JDK 21 引入分代              虚拟线程      JDK 21 正式发布              JFR      JDK 11 后开源并广泛可用      升级或迁移时，应重新压测，而不是假设参数和默认值完全兼容。16. 快速判断表            现象      优先检查                  OOM heap      dump、GC 日志、大对象              OOMKilled      容器总内存、堆外              CPU user 高      线程栈、JFR 热点              CPU sys 高      上下文切换、锁、系统调用              GC 停顿高      收集器、堆、Live 数据              安全点等待高      长循环、大数组操作              线程 BLOCKED      锁持有者、临界区              线程池满      外部超时、队列、下游              启动慢      JIT、类加载、CPU limit              延迟毛刺      CPU throttling、GC、安全点</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。掌握 JVM 不是记住所有参数，而是建立从字节码到运行时、从单机到容器、从现象到根因的完整能力。本章给出继续深入的学习路径和工程实践建议。31.1 能力模型JVM 大师能力可以分成六层：            层级      能力                  入门      会启动、看异常、配置 JVM 参数              基础      理解内存区域、类加载、常用 GC              进阶      能读字节码、分析 JIT、锁和内存模型              熟练      能用 GC、JFR、dump、Arthas 排查线上问题              高阶      能设计容量、调优资源、治理 JVM 配置              精通      能阅读 HotSpot 源码并评估版本演进      不必每层都完美，但要清楚自己的短板。31.2 学习路径第一阶段：跑通基础目标：  写代码观察对象分配；  使用 javap -v 看字节码；  开启 GC 日志；  手动生成堆 dump；  生成线程 dump；  用 JFR 采集一次事件。练习：javac Demo.javajavap -v Demo.classjava -Xlog:gc -jar demo.jarjcmd &lt;pid&gt; Thread.printjcmd &lt;pid&gt; GC.heap_dump demo.hprof第二阶段：制造问题主动构造：  无界缓存导致堆增长；  嵌套循环导致 CPU 高；  synchronized 竞争导致 BLOCKED；  ThreadLocal 未清理；  大查询导致频繁 Young GC；  动态类生成导致 Metaspace 增长。能稳定复现问题，才算是真正理解问题。第三阶段：建立证据链每次练习都输出报告：现象时间窗口系统指标JVM 指标GC 日志线程 dumpJFR堆 dump假设修复验证久而久之，你会形成自己的排障直觉。31.3 深入 HotSpot进阶阅读方向：  HotSpot 源码目录结构；  oop 与对象模型；  类加载与 SystemDictionary；  解释器和模板解释器；  C1/C2 编译流程；  G1、ZGC、Shenandoah 实现；  safepoint 机制；  JVM 参数解析与 flag 系统；  JFR 事件体系。源码学习建议：先从一个问题进入  -&gt; 找到参数或日志关键字  -&gt; 搜索源码位置  -&gt; 画调用链  -&gt; 对比不同版本  -&gt; 用实验验证理解不要从第一页开始通读，那会很快迷路。31.4 关注版本演进JVM 每个版本都会变化，例如：            版本      代表变化                  JDK 8      Lambda、Metaspace、广泛使用的旧生态              JDK 11      模块化成熟、HTTP Client、JFR 开源              JDK 17      LTS、密封类、模式匹配增强              JDK 21      LTS、虚拟线程、分代 ZGC              JDK 25      新 LTS，具体特性以发布文档为准      学习新版本时问四个问题：  解决什么问题；  默认行为是否变化；  旧参数是否废弃；  对当前业务有什么收益和风险。31.5 工程治理大师级的 JVM 能力最终要落到系统治理：  统一 JVM 参数模板；  版本升级策略；  容量规划和成本评估；  GC 与安全点监控告警；  OOM 和 CPU 高应急预案；  dump 与 JFR 文件管理；  性能回归测试；  诊断平台化；  故障复盘机制。一个团队的问题往往不是缺少某个参数，而是缺少一致、可验证、可观测的运行时治理。31.6 性能工程清单发布前检查：1. 堆、元空间、直接内存、线程栈预算2. 容器 CPU 和 memory limit3. GC 日志滚动配置4. OOM 自动 dump5. JFR 采集策略6. 线程池命名和监控7. 外部调用超时8. 优雅停机9. 预热策略10. 性能基线运行中监控：QPS / P50 / P99 / P999CPU usage / throttlingMemory usageHeap used / committed / maxGC pause / count / timeMetaspaceDirect memoryThread countSafepointDownstream latency31.7 常见进阶主题            主题      入手点                  虚拟线程      调度、pinning、线程池模型变化              GraalVM Native Image      提前编译、反射配置、代理              Project Lilliput      对象头和内存密度优化              Panama      Foreign Function &amp; Memory API              Valhalla      值对象与对象布局演进              JFR 持续流      在线事件消费              CRaC      快照恢复启动优化      这些项目都围绕同一个主题：让 Java 在性能、内存密度和部署形态上继续进化。31.8 建立个人知识库建议维护三类笔记：第一，原理笔记：对象头结构G1 Region 生命周期volatile 语义第二，实验笔记：代码启动参数命令输出结论第三，故障库：时间现象证据根因修复后续预防知识库的价值不在收藏，而在能被下次故障快速复用。31.9 学习节奏一个可持续的 12 周计划：            阶段      内容                  1-2 周      内存区域、类加载、对象布局              3-4 周      字节码、解释执行、JIT              5-6 周      JMM、锁、线程池              7-8 周      GC 原理与日志              9-10 周      生产诊断工具              11 周      容器与调优实战              12 周      复盘、输出文章或内部分享      每周至少完成一个可复现实验。输出会强迫你把模糊的理解变清楚。31.10 最终建议  先建立稳定基线，再谈调优；  先看证据，再提假设；  先修业务内存问题，再扩堆；  先确认版本，再套参数；  先看分布，再看平均；  先做回滚方案，再灰度；  先理解默认值，再改高级参数；  先把诊断数据留住，再重启。JVM 的学习没有终点。真正的高手不是永远不遇到问题，而是遇到问题时有方法、有工具、有历史数据，并且能把自己学到的东西沉淀给团队。本章小结从 JVM 基础到大师的关键跨越，是把知识点变成可复现实验、可依赖证据和可治理流程。继续深入时，应从实际问题进入 HotSpot 源码和新特性，同时建立版本意识、性能基线、故障库和团队级运行时治理体系。思考题  你当前最薄弱的 JVM 能力是哪一层？  如何设计一个可持续的性能实验环境？  团队如何避免每个人复制一套不同 JVM 参数？  新 JDK 特性上线前应评估哪些风险？  你准备把哪次线上故障沉淀为可复用的故障库案例？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章把 JVM 面试中的高频问题整理成可讲清、可追问、可落地的答案。面试的重点不是背诵结论，而是展示边界意识和排查能力。30.1 JVM 内存区域问题：JVM 运行时数据区有哪些？回答要点：  堆：对象和数组，线程共享；  方法区：类元数据、运行时常量池，HotSpot 现为 Metaspace 实现；  虚拟机栈：栈帧、局部变量表、操作数栈；  本地方法栈：Native 方法；  程序计数器：线程私有，记录当前执行字节码位置。加分点：JDK 8 移除永久代，Metaspace 使用本地内存；虚拟机栈可能 StackOverflowError；堆和 Metaspace 可能 OOM；直接内存不属于 JVM 规范定义的堆，但会影响进程总内存。30.2 对象创建过程问题：new 一个对象时发生什么？简化流程：1. 类加载检查2. 分配内存3. 初始化零值4. 设置对象头5. 执行 &lt;init&gt; 构造逻辑追问点：  指针碰撞还是空闲列表取决于堆是否规整；  分配可能使用 TLAB；  对象头包含 Mark Word 和类型指针；  大对象可能进入老年代或特殊 Region。30.3 对象内存布局问题：对象在堆里如何存储？结构：Object Header  Mark Word  Klass PointerInstance Data  字段，可能重排Padding  对齐到 8 字节压缩指针：  开启压缩 OOP 时，Klass Pointer 可能为 4 字节；  堆超过一定范围后可能失效；  对齐方式可能调整；  具体行为与 JVM 版本和参数有关。30.4 类加载过程问题：类加载有几个阶段？回答：加载 -&gt; 验证 -&gt; 准备 -&gt; 解析 -&gt; 初始化            阶段      关键动作                  加载      读取字节流生成 Class 对象              验证      格式、元数据、字节码、符号引用              准备      静态变量分配并设零值              解析      符号引用转直接引用              初始化      执行 &lt;clinit&gt;      追问：静态变量在准备阶段是零值，static final int x = 1 这类编译期常量可能在准备阶段直接赋值。30.5 双亲委派问题：什么是双亲委派？流程：ClassLoader.loadClass  -&gt; 先委托 parent  -&gt; parent 找不到再由当前 loader findClass作用：  避免核心类被重复加载；  防止用户替换核心 API；  保证类的一致命名空间。破坏场景：  SPI 需要父加载器使用子加载器；  Web 容器隔离应用类；  OSGi 模块化；  热部署和动态代理。“破坏”不一定是贬义，而是为满足隔离和可见性需求做的设计取舍。30.6 判断对象存活问题：JVM 如何判断对象可以回收？回答：  主流 HotSpot 使用可达性分析；  从 GC Roots 出发搜索；  不可达对象可回收；  finalize 已废弃，不应作为资源释放方案；  引用类型影响回收时机。常见 Roots：线程栈局部变量静态字段JNI 引用系统类加载器加载的核心类活跃线程对象30.7 引用类型            类型      回收倾向      典型用途                  强引用      不回收      普通对象              软引用      内存不足前回收      缓存，现代缓存更常用显式容量              弱引用      下次 GC 回收      WeakHashMap、ThreadLocalMap              虚引用      不影响生命周期      跟踪回收、堆外内存清理      追问：ThreadLocalMap 的 key 是弱引用，value 是强引用，因此仍需要 remove()。30.8 GC 算法问题：常见的 GC 算法有哪些？回答：            算法      思路      问题                  标记-清除      标记后直接释放      碎片              标记-复制      存活对象复制到另一块      空间成本              标记-整理      存活对象向一侧移动      移动成本              分代      按生命周期分区      跨代引用和复杂度      结合收集器说明：Serial、Parallel、G1、ZGC、Shenandoah 是不同工程落地，不是孤立算法名词。30.9 G1 与 CMS问题：G1 相比 CMS 有什么优势？回答：  G1 是 Region 化布局；  可预测停顿目标；  标记整理降低碎片；  Mixed GC 逐步回收 Old；  CMS 在 JDK 14 被移除。CMS 的历史问题：并发标记清除产生碎片concurrent mode failure 退级 Serial OldRemark 阶段成本Remembered Set 维护成本面试中要说明自己使用的是哪个 JDK，避免把 JDK 8 经验泛化。30.10 ZGC 特点问题：ZGC 为什么停顿低？回答：  标记、搬迁大部分并发执行；  着色指针记录对象状态；  读屏障帮助修正引用；  停顿阶段工作量小；  JDK 21 引入分代模式。代价：  内存开销更高；  读屏障影响吞吐；  并发阶段消耗 CPU；  堆太满时可能 Allocation Stall。30.11 volatile问题：volatile 有什么作用？回答：  可见性：写入后其他线程能读到新值；  有序性：禁止特定指令重排；  不保证原子性。示例：private volatile boolean running = true;public void stop() {    running = false;}适合状态标志。volatile int count; count++ 仍不是线程安全的，应使用 AtomicInteger 或锁。30.12 synchronized 锁升级问题：synchronized 有哪些锁状态？简化路径：无锁 -&gt; 偏向锁 -&gt; 轻量级锁 -&gt; 重量级锁要点：  JDK 15 起默认废弃偏向锁；  锁状态存储在对象头 Mark Word；  竞争程度影响升级；  锁不一定是“只能升级不能降级”的简单过程；  JIT 可能锁消除或锁粗化。回答时补充版本差异，比背固定路径更专业。30.13 Java 内存模型问题：JMM 解决什么问题？回答：  定义共享变量的可见性；  定义指令重排的边界；  定义 happens-before 规则；  为 volatile、final、锁提供语义基础。示例规则：程序顺序规则monitor lock 规则volatile 规则线程 start/join 规则传递性JMM 不是堆栈分区图，面试中不要把内存区域和内存模型混为一谈。30.14 排查 OOM问题：线上 OOM 怎么排查？回答框架：1. 先确认 OOM 类型2. 读取完整异常栈3. 保存 GC 日志和 dump4. 分析回收后占用5. MAT 找 Retained Heap 和引用链6. 结合业务源码确认生命周期7. 修复后压测验证加分：  区分堆、Metaspace、线程、直接内存；  区分 Java OOM 和 OOMKilled；  说明 dump 会 STW；  说明自动 dump 参数；  给出真实案例结构。30.15 排查 CPU 高问题：Java 进程 CPU 100% 怎么办？回答：top -H -p &lt;pid&gt;printf "%x\n" &lt;tid&gt;jcmd &lt;pid&gt; Thread.print然后：  定位高 CPU 线程；  判断业务、GC、JIT 或其他 JVM 线程；  用 JFR 或 Arthas 定位方法；  结合流量和输入特征；  修复后验证。加分点：说明容器 CPU throttling 和 JVM 感知处理器数。30.16 线程池参数问题：线程池核心参数有哪些？回答：corePoolSizemaximumPoolSizekeepAliveTimeworkQueuethreadFactoryrejectedExecutionHandler提交流程：core 未满 -&gt; 创建核心线程core 满 -&gt; 入队队列满且 max 未满 -&gt; 创建非核心线程队列满且 max 满 -&gt; 拒绝注意无界队列会让 maximumPoolSize 失效。生产要显式命名线程、设置有界队列、监控队列和拒绝数。30.17 类冲突问题：ClassNotFoundException 和 NoClassDefFoundError 有什么区别？            类型      时机      常见原因                  ClassNotFoundException      显式加载时抛异常      类路径缺失              NoClassDefFoundError      编译时存在，运行时初始化失败      依赖缺失、版本冲突、静态块失败      排查：  看完整异常栈；  确认 jar 是否在 classpath；  检查同名类多个版本；  使用 javap 反汇编；  检查类加载器和模块可见性；6| 查看-parent 子类缺失父类的情况。30.18 高频反问面试官可能追问：  你实际用过哪个收集器；  你们服务的堆多大、延迟目标多少；  调优前后指标是什么；  dump 有多大，怎么分析；  为什么不用另一个收集器；  容器内存如何规划；  如何确认参数生效；  如何避免 GC 调优只优化了平均值。回答原则：先给结论再讲证据说明版本和边界承认不确定性展示验证过程本章小结JVM 面试要能从概念、实现、版本差异、生产排查四个层面回答。核心区域、类加载、GC、JMM、锁、线程池、OOM 和 CPU 排查是高频主线。回答时引用实际指标、日志、dump 和压测过程，比背诵名词更能体现能力。思考题  为什么内存区域和 Java 内存模型不能混为一谈？  如何向面试官描述一次完整的 GC 调优？  volatile 和 synchronized 分别适合什么场景？  你如何回答“G1 和 ZGC 怎么选”？  哪些 JVM 面试答案必须补充 JDK 版本？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。GC 调优不是背参数，而是在明确目标下持续度量、提出假设、验证效果。多数应用不需要激进调优，合理设置堆、CPU、日志和诊断，往往比复制复杂参数更有效。29.1 调优目标开始前必须明确目标，常见目标有三种：            目标      典型指标                  降低延迟      P99、P999、最大停顿              提高吞吐      QPS、批处理总耗时              降低资源      CPU、内存、成本      目标之间可能冲突：更小停顿 -&gt; 可能增加屏障和并发成本 -&gt; 吞吐下降更高吞吐 -&gt; 可能接受更长停顿更低内存 -&gt; GC 更频繁 -&gt; 延迟抖动不要说“把 GC 调到最优”，而要说“在 4C8G、P99 不超过 150ms 的前提下，将 GC 停顿 P99 控制在 20ms 内”。29.2 基线采集调优前先采集：  JDK 版本和发行版；  JVM 参数；  堆、CPU、容器 limit；  QPS 和请求分布；  P50/P99/P999；  GC 日志；  JFR；  堆 dump 或 Live 数据估算；  机器资源和宿主机干扰；  业务特征，如大促、批处理、夜间任务。基线表：            指标      当前值                  QPS      1200              P99      180ms              Young GC 频率      8 次/分钟              Young GC 平均停顿      35ms              Full GC      2 次/小时              回收后 Old      2.8g              堆      4g              CPU limit      4 核      没有基线，就无法证明调优是否有效。29.3 判断是否有问题            现象      是否需要调优                  Young GC 频繁但停顿 5ms      视业务目标              P99 正常但平均 GC 低      通常不需要              Full GC 偶发于发布后      先看预热和缓存              GC 停顿超过延迟目标      需要              Allocation Stall      需要              吞吐下降明显      需要              OOM      必须处理      优先级：1. 修复泄漏和异常 Full GC2. 降低异常分配3. 合理设置堆和 CPU4. 调整新生代或收集器5. 微调高级参数29.4 常见问题与处理情况一：Young GC 太频繁证据：Young GC 20 次/分钟每次回收 Eden 512M -&gt; 32M停顿 8msP99 正常若延迟目标满足，可以不调。若 CPU 成本高，可以：  增大堆或新生代；  减少临时对象；  复用缓冲；  流式处理数据；  降低序列化频率。情况二：Young GC 停顿太长证据：Young GC 120ms存活对象扫描过多方向：  减小新生代；  降低单次请求临时对象；  检查 Survivor 和晋升；  换 G1 或 ZGC；  增加并行 GC 线程，前提 CPU 充足；  减少线程数。情况三：对象过早晋升证据：每次 Young GC Old 增加Tenuring 分布显示大量对象 age 1 晋升方向：  增大 Survivor；  调整 TargetSurvivorRatio 前先验证；  避免突发大对象；  拆分大请求；  检查缓存是否存入短生命周期对象。情况四：Mixed GC 不干净证据：G1 Mixed GC 多轮后 Old 仍高回收集合收益低方向：  检查大对象和 Region 占用；  检查缓存生命周期；  增加 G1 停顿目标要谨慎；  增加 G1MixedGCCountTarget 需压测；  若 Live 数据接近堆上限，扩堆更直接。情况五：Full GC 频繁方向：  找触发原因；  检查是否 System.gc；  检查 promotion failed；  检查元空间；  堆 dump 看 Live；  修泄漏后扩堆；  更换收集器。29.5 收集器选择            场景      起点建议                  小内存、单核或低并发      Serial              批处理、吞吐优先      Parallel              常见服务、均衡      G1              大堆低延迟      ZGC              支持并验证过的低延迟方案      Shenandoah      选择流程：明确延迟目标  -&gt; 统计当前停顿分布  -&gt; 判断 G1 是否满足  -&gt; 若不满足且资源充足，测试 ZGC  -&gt; 对比吞吐、CPU、内存、P999  -&gt; 灰度上线收集器没有绝对最强，只有和负载、资源、版本是否匹配。29.6 调整堆大小估算思路：Live 数据：回收后仍存活的堆占用余量：应对流量峰值、并发标记、碎片和波动经验起点：堆大小约为 Live 数据的 1.5 到 3 倍这只是估算，不是规则。若 Live 数据 2g，堆 2.2g 通常过紧；4g 可能更稳；但堆外和容器 limit 也必须考虑。判断堆是否够：  Full GC 后 Old 是否仍高；  并发 GC 是否频繁触发；  Allocation Stall 是否出现；  回收后占用是否持续上升；  是否有足够余量应对峰值。29.7 新生代调整分代收集器中，新生代大小影响频率和单次停顿：新生代变大  Young GC 频率下降  单次停顿可能变长  Survivor 容量增加新生代变小  频率上升  单次停顿可能变短  更容易晋升G1 通常优先设置停顿目标，让 JVM 控制新生代大小。手动设置 -Xmn 或 NewRatio 可能削弱 G1 的自适应能力。Parallel 更常显式调整新生代，但仍要基于日志和压测。29.8 压测方法每次只改一个关键变量：            实验      变量                  A      当前基线              B      堆 4g -&gt; 6g              C      G1 PauseTarget 100ms -&gt; 50ms              D      ZGC 替代 G1      固定条件：  相同数据集；  相同流量模型；  相同预热时间；  相同机器资源；  相同 JDK 和应用版本；  足够长观察窗口；  记录多次结果。输出：            实验      QPS      P50      P99      P999      GC 时间占比      最大停顿      CPU      内存                  A      1200      40ms      180ms      900ms      4%      240ms      70%      4g              B      1250      39ms      120ms      500ms      2%      90ms      68%      6g              C      1180      41ms      110ms      420ms      3%      60ms      74%      6g      只有完整权衡才能决定上线方案。29.9 案例：接口 P999 抖动现象：日常 P99 80ms，P999 偶尔 800ms日志显示 G1 Young GC Pause 70ms安全点总停止 300ms分析：  GC 本身 70ms；  应用停止 300ms；  多出 230ms 是安全点等待；  线程 dump 中有大循环未及时进入安全点。处理：  将大循环拆小；  使用可计数循环结构；  压测验证安全点等待；  再评估是否需要调整 GC。如果只调小新生代，GC 停顿可能从 70ms 降到 40ms，但 P999 仍高。29.10 案例：批处理任务慢现象：任务读取大 CSV，解析后写入数据库总耗时 2h，吞吐低GC 60 次，停顿总 3s判断：GC 总停顿只有 3s，不是主要问题。继续 JFR 发现：60% 时间等待数据库写入20% CPU 在 JSON 转换优化：  JDBC 批量提交；  合理事务大小；  并行分片；  降低每行 JSON 转换；  数据库索引和约束检查前移。GC 调优不能替代系统瓶颈分析。29.11 上线与回滚上线步骤：1. 在预发完成同流量压测2. 评审参数含义和风险3. 灰度 1 个或少量实例4. 对比 GC、延迟、CPU、错误率5. 观察 24 小时以上6. 分批推广7. 保留回滚参数8. 固化到部署模板必须回滚的信号：  P99 或 P999 明显恶化；  OOM；3| Allocation Stall 增加；  CPU 成本超预算；  Full GC 新增；  吞吐下降无法接受。本章小结GC 调优要先明确目标、采集基线、确认问题类型，再决定调整堆、新生代、收集器或高级参数。大多数问题来自内存泄漏、异常分配、CPU 不足或配置余量不足，而不是缺少神奇参数。每次调优都应单变量实验，完整对比延迟、吞吐、CPU 和内存，并具备灰度和回滚能力。思考题  为什么吞吐优先和延迟优先的 GC 策略可能冲突？  如何判断堆大小是否充足？  G1 中手动设置 -Xmn 有什么风险？  GC 停顿 70ms 但应用停 300ms，应排查什么？  如何设计一份 GC 调优报告？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。容器中的 JVM 问题，常常不是 JVM 单独的问题，而是 JVM、操作系统 cgroup、Kubernetes 调度和应用资源模型共同作用的结果。本章重点讲 JVM 在容器中如何感知资源，以及如何配置内存、CPU、日志和诊断。28.1 与物理机部署的差异            维度      物理机/虚拟机      容器                  CPU      看到整机核数      受 CPU limit 约束              内存      看到系统内存      受 cgroup memory limit 约束              线程限制      系统 ulimit      还可能受 pids cgroup 限制              OOM      进程或系统行为      可能被 OOMKilled              存储      本地盘      依赖挂载卷              诊断      直接读文件      需考虑 Pod 生命周期      JVM 看到的资源与实际可用资源不一致，是最常见的问题来源。28.2 内存模型Java 进程内存不只是堆：容器 memory limit  Java Heap  Metaspace  Thread stacks  Direct memory  CodeCache  GC 开销  Symbol/Internal  Native library  OS 余量如果只设置：容器 limit = 4Gi-Xmx4g堆外内存稍一增加，就可能 OOMKilled。推荐使用比例并预留空间：java -XX:MaxRAMPercentage=60.0 -jar app.jar根据应用特点，常见区间在 50% 到 70%。堆外占用高的 Netty、RPC、缓存类应用应从更低比例开始。28.3 容器内存参数常用参数：            参数      含义                  -XX:+UseContainerSupport      启用容器感知              -XX:InitialRAMPercentage      初始堆占容器内存比例              -XX:MaxRAMPercentage      最大堆占容器内存比例              -XX:MinRAMPercentage      小内存容器场景              -XX:ActiveProcessorCount      显式指定处理器数      示例：java -XX:+UseContainerSupport \  -XX:InitialRAMPercentage=60.0 \  -XX:MaxRAMPercentage=60.0 \  -XX:ActiveProcessorCount=4 \  -jar app.jar如果同时设置 -Xmx 和 MaxRAMPercentage，最终结果要按 JVM 规则确认，不要靠猜。28.4 CPU 感知JVM 会根据感知到的 CPU 数决定 GC 线程、JIT 编译线程等默认并发度。常见问题：  宿主机 64 核，Pod CPU limit 2 核；  JVM 启动过多 GC 线程；  CPU throttling 导致延迟毛刺；  编译线程与业务线程争抢；  并行流或框架线程池使用错误 CPU 数。显式指定：-XX:ActiveProcessorCount=4对于 cpu limit = 2 的 Pod，将其设置为远大于 2 通常是不合适的。28.5 request 与 limitKubernetes 中：            字段      作用                  request      调度依据和共享资源保障              limit      cgroup 上限      常见配置：resources:  requests:    cpu: "2"    memory: "4Gi"  limits:    cpu: "4"    memory: "4Gi"内存 request 与 limit 通常需要一致，避免调度时认为资源够、运行时却触发 OOM。CPU 可以允许 burst，但要评估 throttling。28.6 诊断文件布局推荐挂载：/logs       GC 日志、JFR/data/dump  堆 dump/tmp        临时文件，可单独限制示例启动参数：java -XX:+HeapDumpOnOutOfMemoryError \  -XX:HeapDumpPath=/data/dump \  -Xlog:gc*,safepoint:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=50m \  -XX:StartFlightRecording=filename=/logs/app.jfr,maxsize=512m,maxage=6h,dumponexit=true \  -jar /app/app.jar如果日志写镜像可写层，Pod 重建后会丢失。使用 emptyDir 也会随 Pod 删除，重要诊断文件需要外发或挂持久卷。28.7 优雅停机Kubernetes 会先向容器进程发送 SIGTERM，等待 terminationGracePeriodSeconds 后再 SIGKILL。建议：  Java 进程作为 PID 1 或正确转发信号；  Spring Boot 等框架开启优雅停机；  停止接收新请求；  等待存量请求完成；  关闭线程池和连接池；  主动释放应用锁；  超时时间小于 terminationGracePeriodSeconds。常见错误是在 shell 脚本中启动 Java 后没有 exec：java -jar app.jar更稳妥：exec java -jar /app/app.jar否则 SIGTERM 可能发给 shell，Java 进程收不到。28.8 镜像与运行时选择基础镜像时关注：  JDK 还是 JRE；  是否包含诊断工具；  glibc 还是 musl；  字体、时区、语言包；  镜像安全修复策略；  JVM 是否匹配目标架构。Alpine 类镜像小，但 musl libc 对某些 Native 库、内存分配行为和诊断工具会有差异。追求稳定时，使用发行版标准运行时通常更省心。28.9 监控指标至少监控：            指标      目的                  Pod memory usage      是否接近 limit              Pod CPU usage/throttling      CPU 是否受限              JVM heap used/committed/max      堆趋势              GC pause      延迟影响              Metaspace      类元数据              Direct memory      堆外              Thread count      线程增长              Restart count      稳定性              OOMKilled event      容器级问题      只监控堆会漏掉 OOMKilled。必须同时看进程 RSS 和容器 limit。28.10 常见故障            现象      可能原因      排查                  exit 137      OOMKilled      memory limit、堆外              延迟毛刺      CPU throttling、GC      监控和 GC 日志              启动慢      CPU 低、JIT      就绪探针、CPU              信号不生效      Java 不是 PID 1      检查 ENTRYPOINT              dump 丢失      本地文件随 Pod 删除      挂卷或上传              GC 线程过多      CPU 感知错误      ActiveProcessorCount              频繁重启      健康检查过严或 OOM      事件和应用日志      本章小结容器中 JVM 的关键是资源感知和总内存预算。堆、元空间、线程栈、直接内存、CodeCache 和 Native 都必须放入容器 limit；CPU limit 会影响 GC、JIT 和业务线程调度。诊断文件要挂载并考虑 Pod 生命周期，优雅停机要确保 Java 进程能收到 SIGTERM。思考题  为什么 -Xmx 等于容器 memory limit 是危险的？  JVM 的 GC 线程数为什么会受容器 CPU limit 影响？  Kubernetes exit code 137 说明什么？  如何保证 Pod 重建后诊断文件仍可使用？  启动脚本为什么常用 exec java？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。OOM（OutOfMemoryError）表示 JVM 无法满足某个内存区域的分配或分配相关请求。它不只是“堆不够”，不同 OOM 类型的排查方向完全不同。27.1 常见类型            OOM      含义      方向                  Java heap space      Java 堆不足      对象过多、泄漏、堆配置              GC overhead limit exceeded      GC 占用时间过高且回收效果差      堆接近耗尽              Metaspace      类元数据空间不足      动态类生成、ClassLoader              Unable to create new native thread      无法创建 Native 线程      线程数、栈大小、OS 限制              Direct buffer memory      直接内存不足      NIO、Netty、堆外池              Requested array size exceeds VM limit      数组超过 JVM 限制      拆分数据              Out of swap space 或 OS 级 OOM      交换空间或系统内存不足      容器、进程、系统      必须先读完整异常信息，再决定排查方向。27.2 准备证据生产 OOM 后不要急着重启。应先保留：  OOM 完整异常栈；  GC 日志；  堆 dump；  JVM 参数；  容器 memory limit；  进程 RSS；  线程数；  元空间和直接内存指标；  当时流量和业务日志。推荐启动配置：java -XX:+HeapDumpOnOutOfMemoryError \  -XX:HeapDumpPath=/data/dump \  -XX:+ExitOnOutOfMemoryError \  -Xlog:gc*:file=/log/gc.log:time,uptime,level,tags:filecount=10,filesize=50m \  -jar app.jar27.3 Java heap space完整异常：java.lang.OutOfMemoryError: Java heap space排查流程：1. 看是否伴随突发流量或大请求2. 分析 GC 日志确认回收后占用3. 打开堆 dump4. 找 Retained Heap 最大对象5. Path to GC Roots 定位持有者6. 结合源码确认生命周期7. 修复后压测验证常见原因：            原因      特征                  无界缓存      HashMap/List 持续增长              大查询      一次加载百万行              大文件      全量读入内存              批量导出      累积所有结果              ThreadLocal 泄漏      线程池线程持有              队列积压      生产快于消费              堆配置不足      Live 数据确实需要更多      如果堆 dump 显示大量不可达对象，可能是 OOM 时分配过快，GC 来不及完成，而不是传统泄漏。27.4 GC overhead limit exceeded该 OOM 表示 JVM 花了大量时间回收，但释放空间很少。通常说明堆里大量对象仍可达，或堆余量太小。示例判断：观察期 GC 时间占比 &gt; 98%回收后堆占用 &gt; 2%不同版本行为和参数可能调整，不能只靠字面比例记忆。重点仍是看 GC 日志中回收后占用和停顿分布。处理：  堆 dump 分析存活对象；  修复杂质对象或泄漏；  增大堆只是临时缓解；  降低分配速率；  检查缓存上限。27.5 Metaspace异常：java.lang.OutOfMemoryError: Metaspace常见原因：  动态代理生成大量类；  Groovy、JSP、脚本引擎重复编译；  反射调用生成类；  热部署导致旧 ClassLoader 无法卸载；  类加载器泄漏。排查：jcmd &lt;pid&gt; VM.metaspacejcmd &lt;pid&gt; GC.class_histogram堆 dump 中查看：  类数量；  ClassLoader 数量；  同名类被多少 Loader 加载；  Class 对象 Retained Heap；  引用链指向谁。处理方向：  限制动态类生成；  复用脚本编译结果；  检查热部署释放；  清理全局注册回调；  合理设置 MaxMetaspaceSize。27.6 无法创建线程异常：java.lang.OutOfMemoryError: unable to create new native thread它不是堆内存不足，而是进程无法再创建 Native 线程。常见原因：  线程数达到进程或系统限制；  线程栈设置过大；  代码频繁 new Thread；  容器 PID namespace 或 pids cgroup 限制；  系统内存不足；  线程泄漏。查看：jcmd &lt;pid&gt; Thread.print &gt; thread.txtps -M -p &lt;pid&gt; | wc -lulimit -ucat /proc/&lt;pid&gt;/status | grep Threads处理：  改用线程池并设置上限；  降低 -Xss；  排查未关闭的线程来源；4| 提高容器 pids 限制，前提是有明确容量评估；  使用异步 IO 或虚拟线程时重新评估模型。27.7 直接内存异常：java.lang.OutOfMemoryError: Direct buffer memory常见来源：            组件      典型使用                  NIO      DirectByteBuffer              Netty      PooledDirectByteBuf              RPC 框架      堆外缓冲              缓存      堆外缓存              压缩/序列化      Native 缓冲      排查：  检查 -XX:MaxDirectMemorySize；  查看直接内存指标；  检查 Netty arena 指标；  确认释放路径；  是否泄漏 DirectByteBuffer；  NMT 是否启用。启用 Native Memory Tracking：java -XX:NativeMemoryTracking=summary -jar app.jarjcmd &lt;pid&gt; VM.native_memory summaryNMT 有轻微开销，生产启用前应评估。27.8 容器 OOM KillJVM 未抛 OOM 但进程消失，可能是被容器运行时或操作系统杀掉。证据：Kubernetes Event: OOMKilled, exit code 137dmesg: Out of memory: Killed process &lt;pid&gt;原因：  -Xmx 加堆外超过容器 limit；  线程栈过多；  直接内存泄漏；  Native 库泄漏；  sidecar 或同 Pod 其他进程消耗内存；  limit 设置过低。排查：1. 确认 OOMKilled2. 查看 Pod memory limit3. 查看 JVM 最大堆4. 统计线程数和栈大小5. 查看直接内存和 Native 内存6. 调整堆占比或修复泄漏常见公式：容器内存 &gt; 堆 + Metaspace + 线程栈 + 直接内存 + CodeCache + Native + 余量27.9 OOM 应急流程发现告警  -&gt; 确认是否仍可访问  -&gt; 摘除流量  -&gt; 保存 GC 日志和 dump  -&gt; 记录监控和事件  -&gt; 如果无法诊断，手动生成 dump  -&gt; 重启或替换实例  -&gt; 修复根因  -&gt; 灰度验证如果配置了 ExitOnOutOfMemoryError，进程会快速退出，由编排系统拉起。这样可以避免故障实例继续接收请求，但前提是 dump 已写入本地卷。本章小结OOM 排查第一步是识别类型：堆、元空间、线程、直接内存、数组限制还是系统级 OOM。不同类型对应不同工具和参数。堆 OOM 用 GC 日志和堆 dump；Metaspace 看类加载器；线程 OOM 看线程数和系统限制；直接内存看堆外使用；OOMKilled 则要看容器总内存。生产环境必须提前配置 dump 和日志，否则重启后证据消失。思考题  Java heap space 和 Direct buffer memory 的排查差异是什么？  为什么堆 dump 可能看到大量不可达对象？  Metaspace OOM 常见来源有哪些？  容器 exit code 137 一定代表 Java OOM 吗？  如何设计 OOM 应急预案？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。CPU 高不是根因，而是现象。JVM 场景下，CPU 高可能来自业务计算、GC、JIT 编译、序列化、正则回溯、锁自旋，也可能是容器 CPU 配额不足导致的一点小负载就打满。本章给出一套从系统到 Java 方法、再到根因的排查路径。26.1 先定义问题排查前先确认四个事实：  是 user CPU 高，还是 sys CPU 高；  是持续高，还是毛刺；  是单实例，还是所有实例；  是否伴随延迟、超时、GC 或错误率变化。            现象      方向                  user 高，业务慢      业务方法、序列化、GC              sys 高      线程切换、系统调用、IO、锁              iowait 高      磁盘 IO              单实例高      实例异常、请求倾斜、节点故障              全部实例高      流量、代码、数据特征、下游              启动期高      JIT、类加载、预热              GC 线程高      内存压力      26.2 Linux 基础命令查看进程：top -p &lt;pid&gt;查看线程：top -H -p &lt;pid&gt;查看线程使用率并输出线程号：ps -L -p &lt;pid&gt; -o pid,tid,%cpu,%mem,state,comm --sort=-%cpu | head -20将十进制线程号转十六进制：printf "%x\n" &lt;tid&gt;在线程 dump 中搜索：nid=0x&lt;hex-tid&gt;查看系统状态：vmstat 1pidstat -p &lt;pid&gt; -t 1iostat -x 126.3 判断 CPU 去向进程 CPU 高  |  +-- 业务线程？  |     +-- 计算密集  |     +-- 正则回溯  |     +-- JSON/序列化  |     +-- 大集合处理  |  +-- GC 线程？  |     +-- 分配过快  |     +-- 堆不足  |     +-- Live 数据增长  |  +-- JIT 编译线程？  |     +-- 启动预热  |     +-- 热点变化  |  +-- 其他 JVM 线程？        +-- 编译、压缩、类加载如果高 CPU 线程名形如："GC Thread#0""G1 Conc#0""C2 CompilerThread0"则优先分析 GC 和 JIT，而不是业务代码。26.4 业务线程高 CPU流程：top -H -p &lt;pid&gt;printf "%x\n" &lt;tid&gt;jcmd &lt;pid&gt; Thread.print &gt; thread.txt找到线程栈后，例如："http-nio-8080-exec-5" RUNNABLE   at java.util.regex.Pattern$Curly.match0(...)   at java.util.regex.Pattern.matcher(...)   at com.demo.SearchService.match(...)   at com.demo.SearchController.search(...)这说明热点在正则匹配。继续检查：  正则来源是否用户可控；  是否存在嵌套量词；  输入长度是否过大；  是否有超时保护；  是否可以换成安全匹配方式。26.5 使用 JFR 定位热点开启或 dump JFR 后，在 JMC 的 Code 页查看 Execution Sample。典型热点：com.demo.ReportService.aggregate  com.demo.ReportMapper.select  com.demo.ReportController.download继续用 Arthas trace：trace com.demo.ReportService aggregate '#cost &gt; 100' -n 10如果 aggregate 单次耗时高且 CPU 高，说明确实存在计算热点；如果耗时高但 CPU 低，则更可能是等待数据库或下游。26.6 GC 导致 CPU 高判断证据：  GC 线程 CPU 高；  GC 日志频繁；  Young GC 或并发 GC 周期密集；  堆使用接近上限；  Live 数据增长。排查：jcmd &lt;pid&gt; GC.heap_infojcmd &lt;pid&gt; VM.flagsjcmd &lt;pid&gt; JFR.start name=cpu settings=profile duration=120s filename=cpu.jfr常见处理：            原因      处理                  分配速率高      减少临时对象、流式处理              堆太小      扩容或降低 Live              Survivor 过小      调整新生代              缓存泄漏      堆 dump 定位              CPU 配额不足      增加 CPU limit              收集器不适合      对比 G1/ZGC      26.7 JIT 编译导致 CPU 高启动阶段常见 C1 CompilerThread、C2 CompilerThread 使用 CPU。这是 JVM 将热点方法编译成本地代码的正常行为。判断：  高 CPU 集中在启动后几分钟；  稳定后下降；  请求延迟逐步改善；  没有异常 GC。处理方式：  预热流量逐步放开；  使用应用启动后健康检查就绪延迟；  避免 CPU limit 设置过低；  不要盲目关闭 JIT。如果是运行中长期大量编译，则可能是方法过多、热点不稳定、缓存失效或类加载异常，需要继续抓 JFR。26.8 sys CPU 高与线程切换sys CPU 高通常与系统调用、上下文切换、锁竞争、大量短生命周期线程有关。观察：vmstat 1pidstat -wt -p &lt;pid&gt; 1关注：            指标      含义                  cs      上下文切换              in      中断              r      可运行队列              b      阻塞进程              voluntary/involuntary ctx switch      线程切换类型      处理方向：  降低线程数量；  使用虚拟线程时关注阻塞和调度行为；  减少锁竞争；  批量化和缓冲 IO；  检查是否频繁做系统调用；  保持 CPU limit 与实际负载匹配。26.9 容器 CPU 场景容器中要区分：  宿主机 CPU 使用率；  Pod CPU usage；  CPU limit；  CPU throttling；  JVM 感知的 CPU 数；  GC 线程数。查看 JVM 感知处理器数：jcmd &lt;pid&gt; VM.flags | findstr /i "ActiveProcessorCount"CPU limit 过低会造成：  GC 并发阶段争抢 CPU；  JIT 编译拖慢；  延迟毛刺；  应用线程排队。若 limit 为 2 核，却配置大量业务线程和 GC 线程，操作系统会限制实际执行时间，表现为“代码不慢但请求排队”。26.10 排查模板时间：2026-08-25 10:20-10:35现象：单实例 CPU 95%，P99 从 80ms 到 2s1. top -H 找到高 CPU 线程2. 连续采集 3 次线程 dump3. nid 对应：http-nio-8080-exec-84. 栈顶：com.demo.JsonUtils.toJson5. JFR：JsonUtils 占 65% execution sample6. 业务日志：该时间段一个导出请求返回 80MB 数据7. 结论：循环序列化大对象导致 CPU 高8. 修复：分页流式输出，限制单次导出9. 验证：压测 CPU 40%，P99 90ms排查报告要包含证据链，避免只写“代码有性能问题”。本章小结CPU 高排查要先判断 user/sys/iowait，再定位到具体线程。业务线程通过线程 dump、JFR 和 Arthas 定位方法；GC 线程回到 GC 日志和堆分析；JIT 线程多见于启动预热。容器环境还必须考虑 CPU limit、throttling 和 JVM 感知处理器数。最终结论要有系统指标、线程栈、事件数据和业务特征的共同支撑。思考题  user CPU 高和 sys CPU 高的排查方向有何不同？  为什么 socket read 时 Java 线程可能显示 RUNNABLE？  GC 线程 CPU 高时应该先看哪些证据？  CPU limit 过低为什么会造成延迟毛刺？  如何证明某个方法是 CPU 高的根因？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。线程 dump 是 JVM 中所有 Java 线程在某时刻的栈快照，通常称为 thread dump 或 javacore。它是分析 CPU 高、接口阻塞、死锁、线程池耗尽和外部依赖故障的基础证据。单次线程 dump 只是一个瞬间，生产问题通常需要连续采集多次。25.1 获取线程 dump使用 jstack：jstack &lt;pid&gt; &gt; thread-1.txt强制输出：jstack -F &lt;pid&gt; &gt; thread-forcely.txt使用 jcmd：jcmd &lt;pid&gt; Thread.print &gt; thread-1.txtLinux 可通过信号：kill -3 &lt;pid&gt;输出通常会写到 JVM 标准输出日志，而不是命令行终端。25.2 采集策略建议连续采集：for i in 1 2 3 4 5; do  jcmd &lt;pid&gt; Thread.print &gt; "thread-$i.txt"  sleep 2done同时记录：  时间戳；  CPU 使用率；  GC 日志；  业务日志；  请求 QPS 和错误率；  容器 CPU limit；  数据库和下游状态。连续 dump 的价值在于比较：一个线程短暂 WAITING 正常，长期卡在同一个调用则异常。25.3 线程状态常见状态：            状态      含义      常见原因                  RUNNABLE      可运行，可能正在执行或等待 CPU      计算密集、CPU 不足              BLOCKED      等待进入 monitor      synchronized 竞争              WAITING      等待被显式唤醒      join、wait、LockSupport.park              TIMED_WAITING      限时等待      sleep、带超时等待              NEW      尚未启动      刚创建              TERMINATED      已结束      正常结束      注意：Java 线程显示 RUNNABLE 时，不一定正在消耗 CPU。例如 socket read 可能映射为 RUNNABLE，但实际在等待内核网络事件。25.4 线程栈结构简化示例："http-nio-8080-exec-3" #42 daemon prio=5 os_prio=0 tid=... nid=... waiting on condition   java.lang.Thread.State: WAITING (parking)        at jdk.internal.misc.Unsafe.park(Native Method)        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:...)        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(...)        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:...)        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:...)        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:...)        at java.lang.Thread.run(Thread.java:...)阅读顺序：  线程名，判断角色；  状态；  栈顶，判断阻塞点；  业务包名，定位代码；  锁信息，判断持有者和等待者。25.5 死锁分析jvm 自动检测到的死锁会在 dump 末尾输出：Found one Java-level deadlock:============================="thread-A":  waiting to lock monitor ... object ...,  which is held by "thread-B""thread-B":  waiting to lock monitor ... object ...,  which is held by "thread-A"手动分析步骤：1. 找 waiting to lock 的线程2. 找锁被哪个线程持有3. 查看持有者在做什么4. 画出锁依赖环5. 检查加锁顺序修复方向：  统一加锁顺序；  使用 tryLock 加超时；  缩小锁范围；  拆分锁粒度；  避免在锁内调用外部 IO；  使用无锁结构或队列化写。25.6 线程池耗尽典型特征：  业务线程数量等于 maximumPoolSize；  大量线程阻塞在同一个外部调用；  队列持续增长；  出现 RejectedExecutionException；  CPU 反而很低。示例："order-worker-1" RUNNABLE   at java.net.SocketInputStream.socketRead0(Native Method)   at ...HttpClient.execute(...)   at com.demo.rpc.PayClient.call(...)   at com.demo.task.OrderWorker.run(...)如果 20 个 worker 全部卡在 PayClient.call，问题多半是支付服务慢或超时配置缺失，而不是线程池太小。处理顺序：1. 给外部调用设置超时2. 隔离不同下游的线程池3. 熔断和降级4. 排查下游故障5. 再评估线程数25.7 CPU 高分析线程 dump 与操作系统线程号关联：nid=0x1a2bLinux 排查：top -H -p &lt;pid&gt;printf "%x\n" &lt;tid&gt;然后在 dump 中搜索对应 nid=0x...。Windows 可用 Process Explorer 或 PowerShell 查看线程 CPU，再结合 dump 分析。高 CPU 常见栈：            栈顶方向      可能原因                  正则解析      回溯灾难              JSON/XML 解析      大报文、复杂结构              序列化      循环序列化大对象              集合操作      大 List 排序、过滤              GC 线程      GC 压力              JIT 编译线程      启动期或热点重编译      25.8 锁竞争分析特征：  多个线程 BLOCKED；  waiting to lock 同一对象；  持有者长期不释放；  响应时间抖动。分析模板：等待锁：0x000000076ae12345等待线程：order-1, order-2, order-3持有线程：report-worker-1持有线程栈：  com.demo.ReportService.buildLargeReport(...)  com.demo.ReportController.download(...)结论：下载大报表持有全局锁，订单请求排队。修复可以是将报表生成移到异步任务、去掉全局锁或按租户分锁。25.9 常见误判            误判      实际情况                  WAITING 就是故障      线程池空闲时正常等待任务              RUNNABLE 就是耗 CPU      可能阻塞在 Native IO              单次 dump 就下结论      偶发栈无法代表持续状态              线程多就是泄漏      池大小配置合理时线程数稳定              CPU 高一定是业务死循环      可能是 GC、JIT、压缩、序列化      线程 dump 只描述栈和状态，最终要结合操作系统线程 CPU、监控和业务日志。本章小结线程 dump 是分析阻塞、死锁、线程池耗尽和 CPU 异常的基础证据。生产采集应连续多次，并同时保存 CPU、GC 和业务指标。分析时先看线程名和状态，再看栈顶业务代码和锁关系，最后结合多快照比较确认线程是否持续停留在同一位置。思考题  为什么单次线程 dump 不足以定位偶发慢请求？  RUNNABLE 线程一定在消耗 CPU 吗？  如何从线程 dump 找到死锁依赖环？  线程池满且 CPU 低，优先排查什么？  如何把 Linux 线程 ID 和 Java 线程栈对应起来？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Arthas 是阿里开源的 Java 在线诊断工具。它允许 attach 到运行中的 JVM，查看线程、内存、类加载、方法调用、参数返回值和耗时，适合在测试或受控生产环境中定位疑难问题。使用在线诊断工具要遵守权限和变更流程：它可能读取敏感数据，增强类也会带来少量开销。24.1 安装与启动下载并启动：curl -O https://arthas.aliyun.com/arthas-boot.jarjava -jar arthas-boot.jar启动后会列出 Java 进程，输入编号 attach。也可以直接指定 pid：java -jar arthas-boot.jar &lt;pid&gt;退出并释放增强：stop如果只执行 exit，本地会话退出，但服务端可能仍在。生产用完应执行 stop。24.2 基础命令            命令      用途                  dashboard      总览              thread      线程列表和栈              jvm      JVM 信息              sysprop      系统属性              sysenv      环境变量              sc      查看已加载类              sm      查看方法              jad      反编译类              getstatic      读取静态字段              heapdump      导出堆              logger      查看和修改日志级别      示例：dashboardthread -n 5thread -bjvmsc -d com.demo.OrderServicethread -b 用于找出阻塞其他线程最多的锁持有线程。24.3 watch 方法watch 可以观察方法入参、返回值和异常。基础形式：watch com.demo.OrderService create params returnObj throwExp -x 2常见参数：            参数      含义                  params      方法参数              returnObj      返回值              throwExp      抛出的异常              target      当前对象              -x      展开层级              -n      匹配次数      条件过滤：watch com.demo.OrderService create params returnObj 'params[0] &gt; 1000' -x 2 -n 3这表示只观察第一个参数大于 1000 的调用，最多 3 次。注意：打印过大的对象可能拖慢请求，也可能刷爆终端。生产上要加条件和次数。24.4 trace 耗时trace 用于查看调用链中每层方法的耗时。trace com.demo.OrderService create -n 5 --skipJDKMethod false输出中可以看到每个子调用的耗时占比。适合回答：一个接口慢，到底慢在 SQL、RPC、序列化还是自身计算。常用技巧：# 只看超过 100ms 的调用trace com.demo.OrderService create '#cost &gt; 100' -n 5限制：  只能看到匹配类的方法链；  方法增强有开销；  异步和动态代理可能需要换切点；  线程切换会导致栈不连续；  极高频方法要严格限制次数。24.5 stack 查看调用来源stack 输出方法被调用时的调用栈：stack com.demo.OrderService create -n 5适合定位：  某个方法被谁调用；  意外路径从哪里进入；  缓存穿透来源；  定时任务触发链。如果调用非常频繁，同样要加过滤条件。24.6 tt 时间隧道tt 可以记录方法调用，之后回看参数、返回值和调用栈。tt -t com.demo.OrderService create -n 10tt -ltt -i 1000tt -i 1000 -p含义：            命令      用途                  -t      记录调用              -l      列出记录              -i      查看指定编号              -p      重放调用      重放请求可能再次写数据库或调用下游，生产环境必须确认幂等和安全。24.7 热更新能力Arthas 支持 retransform 加载修改后的 class，但热更新不适合作为常规发布方式。风险：  只替换方法体，不能随意改变类结构；  与下一次正式发布可能不一致；  造成“线上行为和代码仓库不一致”；  增强残留可能影响性能；  审计和回滚困难。更稳妥的用法是：热更新只作为极端应急手段，操作前保存证据，操作后尽快正式发布等价代码。24.8 排查 CPU 高步骤：1. dashboard 观察 CPU 和线程2. thread -n 5 找高 CPU 线程3. 查看线程栈4. trace 或 profiler 定位热点方法5. jad 查看线上类是否与预期一致6. 结合日志和监控确认示例：dashboardthread -n 5thread &lt;thread-id&gt;trace com.demo.TaskService execute '#cost &gt; 100' -n 5如果高 CPU 是 GC 线程，应转向 GC 日志和堆分析，而不是业务方法。24.9 排查接口偶发慢建议流程：1. 确认入口类和方法2. trace 加成本过滤3. watch 保存异常请求参数4. 检查 SQL、RPC、锁、线程池5. 对比正常和异常调用6. 用业务日志补充时间线示例：trace com.demo.OrderController detail '#cost &gt; 200' -n 10watch com.demo.OrderController detail params returnObj throwExp -x 2 -n 10thread --state BLOCKED偶发问题要先拿到样本，再缩小范围。24.10 安全与治理生产使用建议：  通过跳板机或运维平台执行；  记录操作审计；  敏感字段脱敏；  限制 -n 和条件；  用完执行 stop；  不长期保留方法增强；  灰度实例操作；  避免在无降级能力的核心链路随意 watch 大对象。本章小结Arthas 能在不重启 JVM 的情况下观察线程、方法、参数、返回值、调用链和类加载信息，是定位线上疑难问题的利器。watch、trace、stack、tt 是核心命令，但都应加条件、次数和时长限制。生产使用要重视权限、审计、敏感数据和释放增强。思考题  watch 和日志埋点各有什么优劣？  为什么 trace 需要加 #cost 条件？  thread -b 能解决什么问题？  tt -p 在生产中有什么风险？  使用 Arthas 后为什么要执行 stop？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。JFR（Java Flight Recorder）是 JVM 内置的事件记录系统，JMC（JDK Mission Control）是分析和可视化这些事件的客户端工具。相比采样 profiler 和业务日志，JFR 的开销低，适合在生产环境持续或按需采集。JDK 11 以后 JFR 开源并成为 JDK 的重要诊断能力；JDK 8 中的可用性和参数存在商业授权与版本差异。23.1 JFR 的价值JFR 可以记录：  CPU 热点方法；  内存分配位置；  GC 与安全点；  锁竞争和线程等待；  类加载和异常；  I/O 与socket相关事件；  JVM 运行信息。应用运行   |   +-- JFR Event Stream         |         +-- method sampling         +-- allocation sampling         +-- monitor blocked         +-- GC / safepoint         +-- JVM metadata它解决的是“当时发生了什么”，而不是事后猜测。23.2 启动方式启动时开启持续记录：java -XX:StartFlightRecording=filename=/log/app.jfr,maxsize=512m,maxage=1d \  -jar app.jar运行中开始：jcmd &lt;pid&gt; JFR.start name=live filename=/log/live.jfr maxsize=512m maxage=1d停止：jcmd &lt;pid&gt; JFR.stop name=livedump 当前内容：jcmd &lt;pid&gt; JFR.dump name=live filename=/log/now.jfr查看状态：jcmd &lt;pid&gt; JFR.check23.3 常用配置            配置      含义                  filename      输出文件              maxsize      文件最大尺寸              maxage      事件保留时长              settings      事件模板，如 profile              dumponexit=true      JVM 退出时输出              name      记录名称      低开销 profile 示例：java -XX:StartFlightRecording=filename=/log/app.jfr,settings=profile,maxsize=1g,maxage=6h \  -jar app.jarsettings=profile 通常包含更细的采样事件，但仍应根据实际压测确认开销。23.4 JMC 界面JDK Mission Control 打开 .jfr 文件后，常用页面包括：            页面      用途                  Overview      CPU、堆、线程、异常概览              Memory      堆、GC、分配热点              Code      方法热点和调用栈              Threads      线程状态和锁              Lock Instances      锁竞争              Exceptions      异常统计              I/O      文件和网络事件              Environment      JVM 和系统信息      分析顺序建议：1. 看记录时间窗口是否覆盖故障2. 看 CPU 和堆是否异常3. 找热点方法或热点分配4. 查看线程状态和锁5. 把事件时间与业务日志对齐23.5 CPU 高分析JMC 的 Code 页可以看到方法采样占比。典型结论：40% CPU -&gt; com.demo.OrderParser.parse          at OrderService.handle          at HttpWorker.run继续检查：  调用次数是否异常；  单次耗时是否变长；  是否存在正则回溯、大 JSON 解析、重复序列化；  是否 JIT 未热身；  是否锁竞争导致自旋；  是否容器 CPU limit 过低。JFR 的方法采样是采样数据，适合定位热点方向；精确到每个调用的耗时应结合业务埋点或 Arthas trace。23.6 内存分配热点JFR 可以记录对象分配事件的采样栈。典型现象：byte[] 分配 800MB/s  -&gt; JsonWriter.toByteArray  -&gt; OrderController.export排查方向：  是否一次性导出大文件；  是否循环中创建大集合；  是否每请求创建大缓冲；  是否反序列化对象过多；  是否日志拼接过度；  是否缓存未复用。分配速率高会推高 Young GC 频率，在 ZGC 等收集器下还可能引发 Allocation Stall。23.7 锁与线程分析JFR 常见线程状态：            状态      含义                  RUNNABLE      可运行或运行中              BLOCKED      等待进入 monitor              WAITING      无限期等待              TIMED_WAITING      限时等待              SLEEPING      睡眠      如果大量线程 BLOCKED：1. 查看 Lock Instances2. 找持有者线程3. 查看持有时间4. 定位方法调用栈5. 缩小临界区或改并发结构注意 synchronized 和 java.util.concurrent.locks 在事件呈现上可能不同，JDK 版本也会影响锁事件粒度。23.8 常用事件            事件      用途                  CPU Load      进程和机器 CPU              Java Monitor Blocked      synchronized 竞争              Java Monitor Wait      wait/notify 等待              Thread Park      LockSupport.park              Allocation Sample      分配热点              Execution Sample      CPU 热点              Garbage Collection      GC 概览              Safepoint Begin/End      安全点              Exception Statistics      异常              Class Loading      类加载      可用 jfr metadata 查看当前 JFR 支持的事件类型：jfr metadata23.9 生产实践建议策略：  常态低开销记录，保留最近数小时；  告警触发后立即 dump；  把 JFR 文件、GC 日志、业务日志时间对齐；  控制文件大小和保留周期；  限制下载权限，因为记录可能包含参数和对象信息；  变更后对比基线；  对高吞吐服务先压测 JFR 开销。Kubernetes 场景要注意：Pod 重建后本地文件会丢失。可以挂载日志卷，或由运维平台在接收到告警后立刻采集上传。本章小结JFR 是 JVM 内置的低开销事件记录系统，配合 JMC 可以分析 CPU 热点、分配热点、锁竞争、GC 和线程状态。生产上适合常态保留最近事件，并在异常时快速 dump。分析时应把事件流与 GC 日志、业务指标和源码对齐，避免把采样热点直接等同根因。思考题  JFR 和堆 dump 分别适合什么问题？  maxsize 和 maxage 为什么重要？  如何用 JFR 定位 Young GC 频繁的分配来源？  方法采样占比高是否一定代表根因？  如何在 Kubernetes 中设计 JFR 采集方案？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。堆 dump 是某个时刻 Java 堆的快照。它像一次完整的“合影”，能看到对象类型、数量、字段、引用链和类加载器。它不适合观察趋势，但适合回答：堆里到底是什么、谁引用了它、为什么不能回收。22.1 dump 类型            类型      内容      用途                  heap dump      对象和引用      内存泄漏、对象膨胀              core dump      进程完整崩溃快照      Native 崩溃              JFR      持续事件流      性能分析      堆 dump 通常使用 HPROF 格式，文件扩展名常为 .hprof。注意：堆 dump 本身会占用磁盘空间，大小通常接近活跃对象和堆结构元数据规模。22.2 获取方式OOM 自动生成：java -XX:+HeapDumpOnOutOfMemoryError \  -XX:HeapDumpPath=/data/dump \  -jar app.jar手动生成：jmap -dump:live,format=b,file=/data/dump/app.hprof &lt;pid&gt;推荐使用 jcmd：jcmd &lt;pid&gt; GC.heap_dump /data/dump/app.hprofjmap -dump:live 会先触发一次 Full GC，只保留可达对象。这样文件更小，但会丢失不可达对象和真实的瞬时内存状态。22.3 采集注意点手动 dump 会引发 Stop-The-World，堆越大停顿越久。生产采集前应确认：  已摘除流量或允许短时不可用；  磁盘剩余空间足够；  dump 路径挂载在足够大的卷；  文件不会写入容器可写层；  有权限读取进程；  记录采集时间、实例、版本和当时流量；  敏感数据需要脱敏和访问控制。更稳妥的策略是 OOM 自动 dump，并配合滚动保留策略避免磁盘被填满。22.4 MAT 基本用法Eclipse Memory Analyzer（MAT）是常用堆分析工具。打开 dump 后，常用视图：            视图      用途                  Overview      堆大小、对象总数、大类              Dominator Tree      谁独占最多内存              Histogram      类维度对象数量和浅堆              Path to GC Roots      为什么对象未回收              Leak Suspects      自动可疑点              Thread Overview      线程栈和局部变量      关键指标：            指标      含义                  Shallow Heap      对象自身大小              Retained Heap      对象被回收后可释放总量              Objects      实例数量              Class Loader      类加载来源      排查内存问题优先看 Retained Heap，而不是只看实例数。22.5 分析流程1. 确认 dump 采集时间和症状2. 看 Overview，确认堆规模3. Histogram 找数量异常的类4. Dominator Tree 找 Retained Heap 最大对象5. 查看对象字段内容6. Path to GC Roots 找引用者7. 映射到业务对象或缓存 key8. 结合源码确认生命周期9. 修复后压测验证不要只依赖 MAT 的 Leak Suspects 自动报告。自动报告只能提示可疑点，最终要回到引用链和源码。22.6 常见泄漏形态            形态      特征      方向                  静态集合增长      Map/List Retained 大      检查清理逻辑              ThreadLocal 未清理      线程池线程持有 Entry      finally 中 remove              监听器未注销      回调对象长期持有      生命周期绑定              缓存无界      业务 key 持续增加      容量、过期、淘汰              ClassLoader 泄漏      类和实例大量重复加载      检查动态类加载              大数组暂存      byte[] Retained 大      拆分或释放              连接对象未关闭      连接和缓冲持有      try-with-resources      22.7 ThreadLocal 泄漏示例错误代码：private static final ThreadLocal&lt;List&lt;byte[]&gt;&gt; CONTEXT =    ThreadLocal.withInitial(ArrayList::new);public void handle(int size) {    CONTEXT.get().add(new byte[size]);}在线程池中，线程会长期存活。如果业务对象放入 ThreadLocal 后不移除，对象可能一直挂在线程的 ThreadLocalMap 上。正确处理：try {    CONTEXT.get().add(new byte[size]);    process();} finally {    CONTEXT.remove();}更基本的问题是：ThreadLocal 是否应该存大对象？如果只是传参，优先使用方法参数或明确的请求上下文对象。22.8 ClassLoader 泄漏热部署、脚本引擎、动态代理、OSGi、反射生成类都可能创建新的 ClassLoader。如果老 ClassLoader 加载的类仍被全局对象引用，就无法卸载。MAT 中的特征：  同名类出现大量 ClassLoader；  Class 对象数量异常；  Metaspace 增长；  某个应用 ClassLoader Retained Heap 很大。排查方向：1. Path to GC Roots 查看谁引用 ClassLoader2. 检查静态字段、线程、JMX、日志器、缓存3. 检查第三方库注册回调4. 重新部署是否真正释放旧类5. 限制不必要的动态类生成22.9 大对象分析如果 byte[]、char[]、String 或集合占用量大，需要继续问三个问题：  它是什么业务数据；  谁引用它；  生命周期应该多长。常见来源：            类型      来源                  byte[]      文件、网络、序列化、加解密              String      大 SQL、大 JSON、日志拼接              ArrayList      一次性加载全量数据              HashMap      无界缓存              StringBuilder      循环拼接      优化方向：  分页或流式处理；  限制单次读取大小；  使用有界缓存；  复用缓冲区；  压缩冷数据；  生命周期结束后显式清空引用。22.10 修复与验证修复不是只改一行代码，还要验证：1. 压测复现原问题2. 对比修复前后堆增长曲线3. 观察 Full GC 后 Old 占用4. 检查实例数量是否稳定5. 确认延迟和吞吐没有劣化6. 保留监控告警一个合格结论应写成：现象：Old 区 6 小时从 1.2g 增长到 3.8g证据：MAT 显示 xxxCache Retained 1.9g，key 为请求参数根因：本地缓存没有容量和过期策略修复：改为 Caffeine，maximumSize=100000，expireAfterWrite=10m验证：8 小时压测后 Old 回收后稳定在 1.3g本章小结堆 dump 适合分析某个时刻堆内对象和引用关系，是定位内存泄漏和大对象问题的重要证据。生产中应优先配置 OOM 自动 dump，手动 dump 要评估停顿和磁盘空间。MAT 分析要抓住 Dominator Tree、Histogram、Retained Heap 和 Path to GC Roots，最终把引用链映射回业务生命周期。思考题  Shallow Heap 和 Retained Heap 有什么区别？  jmap -dump:live 为什么会改变堆状态？  ThreadLocal 泄漏为什么在线程池场景更明显？  Metaspace 持续增长时应检查什么？  如何写出可复现、可验证的内存泄漏分析报告？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。JVM 参数是运行时的控制面。理解参数体系，比背下一堆开关更重要：同一个目标在不同 JDK 版本、不同收集器、不同部署环境下，可能对应不同参数和默认值。21.1 参数分类常见 JVM 参数分为以下几类。            类型      示例      用途                  标准参数      -version、-jar      大多数 JVM 都支持              -X 参数      -Xms、-Xmx、-Xss      常用非标准参数              -XX 参数      -XX:+UseG1GC、-XX:MaxHeapFreeRatio      JVM 实现级参数              系统属性      -Dfile.encoding=UTF-8      传给 Java 应用              环境变量      JAVA_TOOL_OPTIONS      间接影响 JVM      布尔参数写法：-XX:+UseG1GC    开启-XX:-UseG1GC    关闭键值参数写法：-XX:MaxHeapSize=4g-XX:ActiveProcessorCount=421.2 内存参数常用参数：            参数      含义                  -Xms      初始堆大小              -Xmx      最大堆大小              -Xmn      新生代大小              -XX:NewRatio      老年代与新生代比例              -XX:SurvivorRatio      Eden 与 Survivor 比例              -Xss      单线程栈大小              -XX:MaxMetaspaceSize      元空间上限              -XX:MaxDirectMemorySize      直接内存上限              -XX:SoftMaxHeapSize      ZGC 等收集器的软堆目标      典型配置：java -Xms4g -Xmx4g \  -Xss1m \  -XX:MaxMetaspaceSize=512m \  -XX:MaxDirectMemorySize=1g \  -jar app.jar生产服务通常建议 -Xms 等于 -Xmx，避免堆伸缩引入噪声。21.3 收集器参数常用收集器：# Serialjava -XX:+UseSerialGC -jar app.jar# Paralleljava -XX:+UseParallelGC -jar app.jar# G1java -XX:+UseG1GC -jar app.jar# ZGCjava -XX:+UseZGC -jar app.jar# Shenandoah，需运行时支持java -XX:+UseShenandoahGC -jar app.jarCMS 在新 JDK 中已被移除：JDK 8：可用JDK 9：标记 deprecatedJDK 14：移除不要把旧博客中的 CMS 参数直接搬到 JDK 17 或 JDK 21。21.4 OOM 与诊断参数堆 dump 参数：-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/data/dumpOOM 时主动退出，避免故障实例继续服务：-XX:+ExitOnOutOfMemoryError统一日志：-Xlog:gc*,safepoint:file=/log/gc.log:time,uptime,level,tags:filecount=10,filesize=50mJFR：-XX:StartFlightRecording=filename=/log/start.jfr,maxsize=512m,maxage=1d这些参数应在部署模板中统一配置，而不是等事故发生后手动添加。21.5 容器相关参数在容器中，关键是 JVM 能正确识别 CPU 和内存限制。JDK 8u191+ 和 JDK 10+ 对容器支持明显增强。现代 LTS 通常默认会识别 cgroup 限制，但行为仍与 JVM 版本和容器运行时有关。可用参数：            参数      用途                  -XX:+UseContainerSupport      启用容器感知              -XX:ActiveProcessorCount=4      显式指定可用 CPU 数              -XX:InitialRAMPercentage=50.0      初始堆占容器内存比例              -XX:MaxRAMPercentage=50.0      最大堆占容器内存比例              -XX:MinRAMPercentage=50.0      小容器场景比例      示例：java -XX:+UseContainerSupport \  -XX:MaxRAMPercentage=60.0 \  -XX:ActiveProcessorCount=4 \  -jar app.jar堆不能等于容器内存 limit，还要预留元空间、线程栈、直接内存、JIT 代码缓存和 Native 库。21.6 查看参数查看最终生效值：java -XX:+PrintFlagsFinal -version过滤指定参数：java -XX:+PrintFlagsFinal -version | findstr /i "HeapSize GC"Linux 下：java -XX:+PrintFlagsFinal -version | grep -Ei 'HeapSize|Use.*GC'查看运行中进程命令行：jcmd &lt;pid&gt; VM.command_linejcmd &lt;pid&gt; VM.flags启动日志也是确认参数是否生效的可靠方式：java -Xlog:jvm+init:file=jvm-init.log -jar app.jar21.7 参数优先级常见来源：命令行JAVA_TOOL_OPTIONS_JAVA_OPTIONSJDK_JAVA_OPTIONS环境变量不同来源的优先级与 JVM 版本、启动方式有关。生产排障时应以实际输出为准：jcmd &lt;pid&gt; VM.command_line容易踩坑的场景：  启动脚本、K8s YAML、镜像 ENTRYPOINT 各写一份参数；  基础镜像设置了 JAVA_TOOL_OPTIONS；  平台注入了内存参数；  重复设置 -Xmx；  参数拼写错误但启动仍成功。21.8 参数治理生产环境建议：  启动模板统一管理 JVM 参数；  所有实例版本一致或明确版本矩阵；  dump 和日志目录提前创建并挂载；  把有效 JVM 参数作为实例元数据上报；  修改参数必须灰度并对比延迟与 GC；  移除无效参数，避免误导后续维护者；  避免把调优参数复制到不同负载的服务。参数清单模板：            参数      当前值      目的      风险      验证方式                  -Xmx      4g      控制堆上限      容器 OOM      jcmd VM.flags              -XX:+UseG1GC      true      低延迟目标      吞吐下降      GC 日志              -XX:MaxMetaspaceSize      512m      防元空间失控      加载失败      监控      21.9 常见错误            错误      后果                  只设置堆，不考虑堆外      容器被 OOM Kill              新生代设置过大      单次 Young GC 停顿变长              盲目调大线程栈      线程多时 Native 内存暴涨              使用已移除参数      启动失败或无效              未固定 dump 路径      OOM 后拿不到证据              只看平均值      忽略长尾              复制他人参数      与业务负载不匹配      本章小结JVM 参数体系要按内存、GC、诊断、容器和运行时行为分类理解。参数在不同 JDK 与部署环境中可能不同，生产上必须通过 PrintFlagsFinal、jcmd 和启动日志确认最终生效值。参数治理的重点是统一模板、可验证、可回滚，并把内存限制理解为整个进程而非只有堆。思考题  -Xmx 和容器 memory limit 有什么关系？  如何确认一个运行中 JVM 的实际参数？  为什么要显式配置 OOM dump？  -XX:+UseConcMarkSweepGC 在 JDK 17 上会发生什么？  如何设计团队级 JVM 参数变更流程？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。GC 日志是判断 JVM 内存健康度最重要的证据。它不能直接告诉业务代码哪里有问题，但能回答三个关键问题：垃圾什么时候产生、GC 做了什么、应用为 GC 付出了什么代价。JDK 9 以后统一使用 -Xlog 体系；JDK 8 及更早版本使用 -XX:+PrintGCDetails 等旧参数。本章以统一日志为主，同时说明旧日志的阅读方法。20.1 开启日志现代 JDK 通用配置：java -Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags:filecount=10,filesize=50m \  -jar app.jar参数拆解：            片段      含义                  gc*      所有 gc 标签日志              safepoint      安全点日志              file=gc.log      输出文件              time,uptime      时间格式              filecount=10,filesize=50m      最多 10 个文件，每个 50MB      JDK 8 常见配置：java -XX:+PrintGCDetails \  -XX:+PrintGCDateStamps \  -XX:+PrintTenuringDistribution \  -Xloggc:gc.log \  -XX:+UseGCLogFileRotation \  -XX:NumberOfGCLogFiles=10 \  -XX:GCLogFileSize=50M \  -jar app.jar这些旧参数在新 JDK 中可能无效或行为不同，迁移时要替换为 -Xlog。20.2 日志样本G1 日志简化示例：[2026-08-25T10:00:01.123+0800][12.345s] GC(12) Pause Young (Normal)JavaHeap: 2048M-&gt;512M(4096M)[12.400s] GC(12) PlayerThreadScan=1msVM Thread=3msParallel 日志简化示例：[2026-08-25T10:00:02.456+0800] GC(20) PSYoungGen: 1536M-&gt;192M(1792M)]ParOldGen: 512M-&gt;480M(2048M)]Heap after GC invocations=20:  PSYoungGen 192M(1792M) ParOldGen 480M(2048M)ZGC 日志简化示例：GC(3) Garbage Collection (Warmup)GC(3) Pause Mark Start 0.018msGC(3) Concurrent Mark 25msGC(3) Pause Mark End 0.021msGC(3) Concurrent Relocate 12ms日志格式会随 JDK 版本变化，分析时应关注字段含义，不要死记某一行样例。20.3 关键指标每次 GC 至少要看四类信息。            指标      含义                  GC 前堆使用      回收压力              GC 后堆使用      存活对象规模              堆容量      剩余空间              停顿时间      对应用延迟的直接影响      还应该统计：Young GC 次数Mixed GC 次数Full GC 次数平均停顿P99 停顿最大停顿GC 频率吞吐损失晋升速率回收效率吞吐损失可以简化计算：GC 时间占比 = 总 GC 停顿时间 / 观察窗口总时间 * 100%并发 GC 的并发阶段时间不计入停顿，但会消耗 CPU，也应单独观察。20.4 判断 Minor GC健康的 Minor GC 通常表现为：  Eden 快满时触发；  回收后新生代占用明显下降；  老年代没有快速增长；  停顿符合预期。异常形态：Young GC 前：Eden 1024M -&gt; Survivor 96M -&gt; Old 300MYoung GC 后：Eden   0M -&gt; Survivor 96M -&gt; Old 310M连续 10 次这说明每次都有对象晋升，老年代按每次 10M 增长。若增速稳定且最终会回收，可能只是缓存预热；若持续增长，就要怀疑大对象、长生命周期对象或 Survivor 过小。20.5 判断晋升问题常见晋升原因：            原因      日志特征      方向                  Survivor 太小      对象过早进入老年代      调整比例或大小              对象太大      Eden 直接分配老年代      拆分对象              Tenuring 阈值低      晋升年龄小      观察 age 分布              突发流量      短期分配速率高      限流或扩容              对象确实长期存活      Old 稳定后不再降      属于业务数据      JDK 8 可以开启对象年龄分布：-XX:+PrintTenuringDistributionG1 也可以通过 gc+age=trace 观察更细信息。年龄分布要结合多次 GC 看，单次数据没有结论。20.6 判断 Full GCFull GC 通常不是好事，常见原因包括：  老年代空间不足；  元空间不足；  System.gc() 显式触发；  CMS concurrent mode failure；  promotion failed；  JVM 兜底行为；  巨型对象分配失败。处理顺序：1. 找到触发原因2. 判断是否回收有效3. 看回收后老年代占用4. 分析晋升来源5. 检查元空间和直接内存6. 必要时抓堆 dump如果 Full GC 后堆占用从 3.5g 降到 1g，说明有大量临时对象进入老年代，应优化分配和晋升；如果从 3.5g 只降到 3.4g，说明存活对象确实很多，扩容或减少对象才是方向。20.7 安全点有些延迟不是 GC 停顿导致，而是安全点等待导致。发起安全点  -&gt; 等待所有 Java 线程到达安全点  -&gt; 执行 VM 操作  -&gt; 恢复线程典型现象：Total time for which application threads were stopped: 120msGarbage collection: 5msGC 只花了 5ms，但应用停了 120ms，多出的 115ms 可能花在等待线程进入安全点。常见原因是长循环计数没有安全点、大数组清零、反优化等。JDK 11+ 可用：-Xlog:safepoint排查时不要只看 GC 日志里的 Pause，还要看应用线程总停止时间。20.8 日志分析流程一个实用流程：1. 确认时间窗口和故障时间2. 统计 GC 次数、频率、停顿分布3. 找 Full GC、Degenerated GC、Allocation Stall4. 对比 GC 前后新生代、老年代、元空间5. 计算晋升速率6. 关联流量、定时任务、缓存预热7. 观察安全点等待8. 得出假设并验证表格化记录：            时间      事件      GC 前后      停顿      业务表现      结论                  10:00      Young GC      2g -&gt; 600m      20ms      P99 30ms      正常              10:05      Full GC      3.8g -&gt; 3.6g      2s      超时      存活对象高      把 GC 事件和业务指标放在同一时间轴上，很多问题会立刻显形。20.9 常用分析工具            工具      用途                  GCEasy      上传日志，生成统计图表              GCViewer      本地查看旧日志              IBM GC Policy Analyzer      分析特定策略              JFR      结合分配、锁、CPU 分析              Arthas      在线观察 JVM 和方法              Prometheus + Grafana      暴露 GC 指标并告警      工具只是汇总数据，判断仍要回到四个问题：  为什么触发；  回收了多少；  停了多久；  是否影响请求。20.10 建议告警            指标      告警方向                  5 分钟 Full GC 次数      大于 0 需关注              GC 停顿 P99      超过延迟目标              GC 时间占比      持续超过阈值              老年代回收后占用      持续增长              Allocation Stall      大于 0 需分析              安全点等待      显著高于 VM 操作耗时      不要把“GC 次数多”单独当成故障。高吞吐服务 Young GC 频繁可能是正常现象，关键看停顿和业务延迟。本章小结GC 日志分析的核心是把事件、耗时、空间变化和业务影响关联起来。现代 JDK 使用 -Xlog 统一日志体系，应重点观察停顿分布、回收效果、晋升速率、Full GC 原因和安全点等待。日志只能给出方向，最终结论要结合流量、线程、CPU 和堆 dump 验证。思考题  同样 100ms 的应用停顿，为什么可能是 GC，也可能是安全点等待？  如何通过 GC 前后空间变化判断缓存泄漏？  Full GC 后堆从 3.8g 降到 1.2g，说明什么？  Young GC 频繁但停顿很短，一定需要调优吗？  如何设计一套 GC 告警规则？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Shenandoah 是 OpenJDK 中另一个并发整理型低停顿垃圾收集器。它最早由 Red Hat 主导开发，目标是减少 GC 停顿对大堆服务的影响。与 ZGC 类似，它也在标记、整理、引用修正上大量使用并发执行；但实现细节不同，演进路径也不同。Shenandoah 在不同发行版和 JDK 版本中的可用性存在差异。某些商业发行版长期支持，某些版本可能默认关闭或不包含。使用前应以当前运行时和发行版文档为准。19.1 设计目标Shenandoah 的核心目标是：  大部分 GC 工作与应用并发执行；  支持大堆；  停顿时间不依赖堆大小；  尽量减少压缩带来的全局停顿。应用线程             GC 线程  |                    |  | &lt;-- 极短 STW --&gt;   |  | ------ 并发 ------&gt; | 标记  | ------ 并发 ------&gt; | 整理  | ------ 并发 ------&gt; | 修正引用  |                    |它同样不是零停顿。根扫描、部分同步点仍会短暂停止应用线程。19.2 转发指针早期 Shenandoah 使用 Brooks Forwarding Pointer，为每个对象额外维护一个转发指针。对象头+-----------------------+-------------------+| 原本对象头和字段      | forwarding pointer |+-----------------------+-------------------+                          |                          v                     新对象地址对象被搬迁后，旧对象的转发指针指向新对象。应用线程访问旧地址时，通过转发指针找到新地址。这种方式的优点是不依赖 64 位地址中的空闲位；缺点是对象布局变大，访问路径增加。19.3 加载引用屏障新版 Shenandoah 引入加载引用屏障，减少屏障次数，只在读取引用时执行必要处理。简化流程：读取一个对象引用  -&gt; 引用是否需要处理？     -&gt; 否：直接使用     -&gt; 是：        1. 判断对象是否已被搬迁        2. 需要时读取新地址        3. 更新引用        4. 返回正确对象这与 ZGC 的读屏障思想相近，但实现和屏障插入位置不同。两者都把“修正引用”的一部分工作分散到应用线程执行。19.4 GC 周期Shenandoah 的周期可以简化为：1. 初始标记       STW，扫描部分根2. 并发标记       与应用并发标记对象图3. 最终标记       STW，完成标记并准备整理4. 并发整理       选择 Region 并搬迁存活对象5. 并发清理       释放空 Region不同模式的并发程度不同。为了应对紧急内存压力，Shenandoah 也存在降级模式，允许牺牲停顿目标换取回收速度。            模式      特点                  正常并发模式      停顿低，吞吐有屏障成本              降级模式      回收更激进，停顿可能变长              Full GC      兜底，通常意味着压力过高或配置不当      19.5 启用方式如果当前 JVM 支持 Shenandoah：java -XX:+UseShenandoahGC -Xms16g -Xmx16g -jar app.jar查看是否支持：java -XX:+UseShenandoahGC -version如果输出 Unrecognized VM option，说明当前运行时没有启用或没有包含该收集器。19.6 与 ZGC 对比            维度      Shenandoah      ZGC                  引用处理      加载引用屏障，早期使用转发指针      着色指针 + 读屏障              出身      OpenJDK 社区，Red Hat 主导早期开发      Oracle 主导进入 HotSpot              目标      低停顿、并发整理      低停顿、大堆              分代      后续版本持续演进      JDK 21 引入分代              可用性      与发行版关系更大      HotSpot 常见发行版通常可用      两者不是简单的新旧关系，而是同一目标下的不同工程取舍。实际选择必须看 JDK 发行版、版本、业务负载和压测结果。19.7 生产观察启用日志：java -XX:+UseShenandoahGC \  -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=50m \  -jar app.jar关注：Pause Init MarkConcurrent MarkingPause Final MarkConcurrent EvacuationConcurrent CleanupAllocation StallDegenerated GCFull GC健康信号：  少量 Degenerated GC；  没有 Full GC；  停顿分布稳定；  Live 数据不持续增长；  CPU 有余量支撑并发阶段。19.8 常见问题            现象      可能原因      处理                  Degenerated GC 频繁      堆余量不足、分配过快      增大堆或降低分配              Full GC      回收速度跟不上、配置问题      检查堆、对象和日志              吞吐下降      屏障成本      对比 G1 或 ZGC              CPU 抖动      并发 GC 线程争抢      检查 CPU limit 和线程数              容器 OOM      堆外内存超限      统计 Native、线程、直接内存      19.9 使用建议  先确认 JDK 发行版明确支持 Shenandoah；  生产环境固定 -Xms 和 -Xmx；  给堆留出足够余量，避免长期高占用；  对比测试 G1、ZGC、Shenandoah 的延迟分布；  关注发行版升级日志，因为 Shenandoah 的屏障和调度策略变化较快；  不建议只因为“低停顿”就在内存紧张的容器中盲目启用。本章小结Shenandoah 通过转发指针、加载引用屏障和并发整理，把对象搬迁与引用修正大部分移出全局停顿。它与 ZGC 解决的是类似问题，但实现路径和版本可用性不同。生产使用前要先确认运行时支持，再通过日志确认是否出现 Degenerated GC、Full GC 和 Allocation Stall，并用同流量压测比较延迟与资源成本。思考题  转发指针如何帮助并发整理？  加载引用屏障和早期转发指针方案有什么取舍？  Degenerated GC 频繁出现说明什么？  Shenandoah 与 ZGC 的可用性判断有什么不同？  为什么不能只看官方停顿目标选择收集器？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ZGC 是 HotSpot 中面向大堆、低延迟场景的垃圾收集器。它的核心承诺是把大部分 GC 工作并发执行，让停顿时间主要取决于根扫描等少量阶段，而不是堆大小、对象数量或整理工作量。不同 JDK 版本的 ZGC 差异很大。本章以常见 LTS 为主线：JDK 11 引入实验版 ZGC，JDK 15 转正，JDK 17 持续增强，JDK 21 引入分代 ZGC。使用具体版本时，应以该版 JVM 的官方文档和 -XX:+PrintFlagsFinal 输出为准。18.1 设计目标ZGC 的目标可以概括为四点：  停顿时间不随堆大小线性增长；  支持 TB 级别的大堆；  吞吐量损失可接受；  支持并发整理，避免长时间 Stop-The-World 压缩。传统整理型 GC：  堆越大、存活对象越多 -&gt; 搬迁和修正引用越久 -&gt; STW 越长ZGC：  并发标记  并发搬迁  并发修正  极短 STW 阶段它不是“零停顿”。仍有短暂的全局同步点，只是这些阶段的工作量被刻意控制得很小。18.2 着色指针ZGC 使用对象地址中的若干位记录 GC 状态，这被称为着色指针。简化示意：64 位对象地址+------------------+-------+----------------------+| 可用作普通地址位 | GC 位 | 实际偏移信息         |+------------------+-------+----------------------+                        |                        +-- Marked0 / Marked1 / Remapped 等常见视图包括：            视图      含义                  Marked0      一轮标记周期使用的活跃视图              Marked1      交替标记周期使用的活跃视图              Remapped      引用已指向搬迁后的新地址      着色指针把元数据放在指针里，而不是额外维护一张巨大的引用表。应用线程读写引用时，JVM 可以根据指针颜色决定是否需要处理。18.3 读屏障ZGC 的关键机制是读屏障。它是 JVM 在应用线程读取对象引用时插入的一小段逻辑。简化流程：读取引用  -&gt; 指针颜色是否与当前 GC 阶段匹配？     -&gt; 匹配：直接访问     -&gt; 不匹配：        1. 检查对象是否已被搬迁        2. 需要时帮助搬迁        3. 修正引用        4. 继续业务逻辑这带来一个重要特性：应用线程在 GC 过程中可以“顺手”完成部分搬迁相关动作。代价是读屏障会增加额外指令，因此 ZGC 的吞吐量通常低于 Parallel，但能换取稳定停顿。18.4 堆布局ZGC 将堆划分为多个 Region，也常称为 ZPage。            类型      典型对象大小                  small      小对象              medium      中等对象              large      大对象      ZGC 堆+--------+--------+--------+--------+| small  | small  | medium | large  || Region | Region | Region | Region |+--------+--------+--------+--------+Region 大小由 JVM 根据堆大小推导，也可以通过参数控制。堆越大，Region 通常越大。大对象可以独占一个或多个连续 Region。18.5 GC 周期非分代 ZGC 的简化周期：1. 初始标记       极短 STW，扫描根2. 并发标记       与应用并发执行3. 再标记         短 STW，处理剩余根和引用4. 并发预备搬迁   选择待整理 Region5. 初始搬迁       短 STW，搬迁少量关键对象6. 并发搬迁       与应用并发搬迁对象7. 并发修正       应用线程和 GC 修正引用实际日志中的阶段名称会随 JDK 版本变化。看日志时应重点抓三类信息：  每个阶段的耗时；  GC 触发原因；  分配停顿或负载是否过高。18.6 分代 ZGCJDK 21 引入分代 ZGC，把对象划分为年轻代和老年代，并假设大多数对象朝生夕死。分代 ZGC+----------------------+| Young Generation     || 频繁回收，成本低     |+----------------------+| Old Generation       || 较少回收，避免长期对象反复扫描 |+----------------------+分代带来的收益：  年轻对象死亡快时，减少无效工作；  降低分配压力对老年代的影响；  改善常见 Web、RPC、短生命周期任务场景的吞吐。同时也引入了写屏障和跨代引用处理的成本。如果对象长期存活比例很高，分代收益会下降。常用开关：java -XX:+UseZGC -XX:+ZGenerational -jar app.jar在较新的 JDK 中，分代 ZGC 可能已经是默认形态；在更早版本中该参数不存在。不要把某个版本的默认值当成永久结论。18.7 启用与基础参数启用 ZGC：java -XX:+UseZGC -Xms16g -Xmx16g -jar app.jar建议生产环境将 -Xms 和 -Xmx 设置成相同值，减少堆伸缩带来的干扰。Soft max heap：java -XX:+UseZGC -Xmx24g -XX:SoftMaxHeapSize=16g -jar app.jar含义：            参数      含义                  -Xmx      硬上限，超过会触发分配失败或 OOM              -XX:SoftMaxHeapSize      软目标，ZGC 会尽量把堆控制在其以下      这个组合常用于希望平时用 16g、高峰期允许临时扩展到 24g 的服务。18.8 触发时机ZGC 会根据分配速率、堆占用、元数据分配、GC 周期耗时等因素主动触发。常见触发原因包括：            触发原因      方向                  Allocation Stall      分配速度超过回收速度，应用等待内存              GC Trigger: Allocation Rate      分配速率高              Warmup      JVM 启动初期预热              Proactive      主动回收，避免堆持续增长              Metadata      元空间相关分配压力      Allocation Stall 是重点告警信号。它表示应用线程因为内存暂时不可用而等待，会造成接口延迟抖动。18.9 观察日志启用统一日志：java -XX:+UseZGC \  -Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags:filecount=10,filesize=50m \  -jar app.jar关注字段：Garbage Collection  周期编号与原因Pause Mark StartPause Mark EndConcurrent MarkConcurrent RelocateAllocation StallUsed / Live / Allocated健康形态通常是：  GC 周期稳定推进；  并发阶段耗时长但停顿短；  Live 数据平稳；  没有 or 极少 Allocation Stall；  停顿时间分布符合目标。18.10 生产注意事项第一，不要只看平均停顿。低延迟系统要看 P99、P999、最大值和停顿次数。第二，ZGC 更耗内存。着色指针、多视图、Region 管理和并发搬迁都需要空间，不能按 Parallel 的经验精确套用。第三，堆不能压得太满。ZGC 需要留出并发回收的余量。Live 数据长期接近 -Xmx 时，应扩容或减少对象。第四，CPU 资源要充足。并发 GC 线程会和应用争抢 CPU。容器中要正确暴露 CPU 配额，避免 JVM 误判可用资源。第五，谨慎使用巨大对象。超大数组或超大缓冲区可能造成 Region 分配压力，甚至让优势消失。18.11 常见故障与排查            现象      可能原因      排查                  Allocation Stall 频繁      堆不足、分配速率过高、CPU 不足      看 GC 日志、CPU、分配速率              吞吐明显下降      读屏障成本、GC 线程过多      对比其他收集器，压测验证              堆占用持续上升      缓存泄漏、Live 数据增长      堆 dump 分析              启动时 CPU 高      预热、类加载、初始 GC      观察 JFR 和启动日志              容器中被 OOM Kill      JVM 堆外内存超限      检查总内存和容器 limit      排查模板：1. 收集 GC 日志、容器 CPU/内存 limit、JFR2. 确认 Allocation Stall 次数和耗时3. 查看 Live 数据是否持续增长4. 查看堆外内存、直接内存、元空间5. 用堆 dump 确认对象类型6. 调整堆、软上限、CPU 配额后压测对比18.12 ZGC 与 G1 的选择            维度      G1      ZGC                  目标      延迟与吞吐折中      更低停顿              停顿目标      毫秒级目标      亚毫秒到毫秒级常见              堆规模      中大堆      大堆、超大堆优势更明显              实现复杂度      Region + Remembered Set      着色指针 + 读屏障              吞吐      通常更好      屏障和并发成本更高      如果业务能接受 50ms 到 200ms 的 GC 停顿，G1 往往更省资源。如果 P999 对几十毫秒停顿都敏感，并且机器内存充足，ZGC 值得测试。最终选择应基于同流量压测，而不是只看收集器名字。18.13 一个压测观察示例启动应用：java -XX:+UseZGC \  -Xms12g -Xmx12g \  -Xlog:gc*:file=gc.log:time,uptime,level,tags \  -jar app.jar压测后观察：QPSP50 / P99 / P999CPU 使用率GC 次数GC 停顿分布Allocation Stall 次数Live 数据对比实验建议：            实验      目的                  G1 默认      基线              ZGC + 12g      验证停顿              ZGC + 16g      验证余量              ZGC + 更高 CPU limit      验证并发能力      只有延迟、吞吐、资源成本同时看，才能得到可上线的结论。本章小结ZGC 通过着色指针、读屏障、并发标记、并发搬迁和 Region 化布局，把大部分垃圾回收工作移出全局停顿。它适合大堆和低延迟场景，但会消耗更多内存和 CPU。JDK 21 的分代 ZGC 进一步改善了常见短生命周期对象场景的表现。生产使用时，要重点观察停顿分布、Allocation Stall、Live 数据和容器资源，而不是只看平均 GC 时间。思考题  为什么说 ZGC 的停顿时间通常不随堆大小线性增长？  着色指针和读屏障分别解决什么问题？  Allocation Stall 频繁出现时，你会按什么顺序排查？  分代 ZGC 为什么能改善常见业务场景？什么场景收益有限？  如何设计一个 G1 与 ZGC 的公平对比实验？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。G1（Garbage First）是现代 HotSpot 的主流收集器之一。它把堆划分为多个 Region，优先回收垃圾比例高的区域，通过停顿预测模型尽量满足目标停顿，适合大堆交互服务。17.1 核心思想Heap  |-- Region 0 (Eden)  |-- Region 1 (Survivor)  |-- Region 2 (Old)  |-- Region 3 (Free)  +-- Region 4 (Humongous)特点：            特点      说明                  Region 化      不要求物理连续分代              垃圾优先      优先回收收益高的区域              停顿预测      根据 pause target 建模型              整理为主      复制存活对象降低碎片              并发标记      识别老年代垃圾      17.2 启用与目标java -XX:+UseG1GC \     -XX:MaxGCPauseMillis=200 \     -Xms4g -Xmx4g \     -jar app.jar常见参数：            参数      说明                  MaxGCPauseMillis      软停顿目标              G1HeapRegionSize      Region 大小              InitiatingHeapOccupancyPercent      触发并发标记的全堆占比              G1ReservePercent      预留空间              ConcGCThreads      并发线程              ParallelGCThreads      STW 并行线程      停顿目标是软目标。设置得越小，可能通过减少回收工作量实现，但 GC 更频繁、吞吐更低。17.3 回收活动Young GC：Eden / Survivor Regions 满  -&gt; STW     -&gt; 拷贝存活对象到新 Survivor / Old        -&gt; 回收空 RegionsMixed GC：并发标记完成后  -&gt; 回收 Young Regions     -&gt; 加上部分高收益 Old RegionsFull GC：并发标记跟不上 / 晋升失败 / Metaspace 等资源不足  -&gt; 单线程或多线程 Full GC，具体随 JDK 版本演进目标是尽量避免 Full GC。17.4 并发标记周期Initial Mark  -&gt; Root Region Scan     -&gt; Concurrent Mark        -&gt; Remark           -&gt; Cleanup              -&gt; Mixed GC阶段：            阶段      STW      说明                  Initial Mark      是      搭载一次 Young GC              Root Region Scan      否      扫描存活 Region 引用              Concurrent Mark      否      遍历对象图              Remark      是      处理剩余引用              Cleanup      部分      统计和筛选 Region              Mixed GC      是      分批回收 Region      17.5 Region 与大对象Region 大小通常为 1MB 到 32MB 的 2 的幂，由堆大小自动决定，也可显式设置：-XX:G1HeapRegionSize=4m大对象：对象 &gt;= 一半 Region  -&gt; Humongous Region连续大对象会占用多个连续 Region。风险：  大数组；  大字符串；  大查询结果；  大报文；  批量集合。处理：  分页；  流式处理；  拆批；  限制响应大小；  观察 humongous 指标。17.6 转移失败常见概念：Evacuation Failure / to-space exhausted含义：回收时没有足够空间接收转移对象。常见原因：  堆太小；  存活对象过多；  大对象过多；  并发标记太晚；  碎片或 Region 分配失败；  突发流量。处理：  扩堆；  降低存活对象；  减少大对象；  提前并发标记；  调整停顿目标；  观察晋升。17.7 GC 日志启用：java -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=50m \     -XX:+UseG1GC \     -jar app.jar关键指标：            指标      含义                  Pause Young      Young GC 停顿              Pause Mixed      Mixed GC 停顿              Concurrent Mark Cycle      并发标记              Humongous allocation      大对象分配              Evacuation Failure      转移失败              Heap before / after      堆变化              Post-GWUnit      G1 内部单位，以日志说明为准      用 GCEasy、GCViewer、JDK Mission Control 或自建脚本分析趋势。17.8 调优流程1. 采集稳定流量下的 GC 日志2. 明确 P99 / 吞吐 / CPU 目标3. 先修复大对象和泄漏4. 观察分配速率、晋升、并发标记时机5. 小步调整一个参数6. 压测对比7. 固化配置8. 继续监控常见调参方向：            问题      方向                  Young 频繁      降低分配、调整目标或堆              Mixed 不够快      检查并发标记和 Old 水位              转移失败      扩堆、减少存活和大对象              停顿长      降低目标、减少 Region 工作量              CPU 高      并发线程过多或分配过高      17.9 G1 适用场景适合：  大堆；  多核；  交互式服务；  需要平衡吞吐和延迟；  希望默认方案稳妥。不一定适合：  极低停顿需求，可评估 ZGC；  纯吞吐批处理，可评估 Parallel；  堆很小且资源紧张；  分配模式极端大对象；  无法承受额外内存结构开销。17.10 生产案例案例一：Mixed GC 后 Old 仍增长观察：jstat -gcutil &lt;pid&gt; 1000GC 日志显示并发标记完成，但每次 Mixed 回收 Region 数量少。方向：  MaxGCPauseMillis 过小；  停顿目标限制回收量；  大对象占用；  存活对象高；  Old 水位已接近危险。处理：调整目标、减少大对象、扩堆、观察。案例二：导出接口导致 Full GC观察：Humongous allocationEvacuation Failure处理：  SQL 分页；  流式写出 CSV / Excel；  限制导出行数；  异步任务；  独立实例承接。本章小结G1 用 Region 化布局、并发标记和停顿预测模型实现吞吐与延迟的平衡。它适合大堆多核交互服务，但需要关注分配速率、晋升、大对象和转移失败。调优应先修代码和对象分配，再小步调整参数并用日志验证。思考题  G1 的 Region 有什么作用？  Mixed GC 与 Young GC 的差异是什么？  什么是 Humongous Region？  转移失败的常见原因有哪些？  MaxGCPauseMillis 设得越小越好吗？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。CMS（Concurrent Mark Sweep）曾是主流低停顿老年代收集器，目标是让大部分标记和清理与应用并发执行。但它有内存碎片、并发失败、浮动垃圾和参数复杂等问题，HotSpot 在 Java 14 中移除了 CMS。16.1 历史定位Serial / Parallel  -&gt; 停顿随老年代增大变长     -&gt; CMS 引入并发标记清除        -&gt; G1 / ZGC / Shenandoah 继续演进Java 版本变化：            版本      变化                  Java 8      CMS 可用              Java 9      CMS 被标记 deprecated              Java 14      CMS 被移除      仍在使用 CMS 的系统通常是老 JDK 或存量平台，应规划升级。16.2 工作阶段Initial Mark  -&gt; Concurrent Mark     -&gt; Concurrent Preclean        -&gt; Final Remark           -&gt; Concurrent Sweep              -&gt; Concurrent Reset            阶段      STW      说明                  Initial Mark      是      标记根直接可达对象              Concurrent Mark      否      遍历对象图              Preclean      否      预处理并发期间变化              Final Remark      是      处理剩余引用              Concurrent Sweep      否      清理死亡对象              Concurrent Reset      否      重置状态      16.3 优缺点优点：  老年代标记清除大部分并发；  低停顿优于 Parallel Full GC；  适合当时交互服务。缺点：  mark-sweep 产生碎片；  并发阶段占用 CPU；  浮动垃圾；  需要预留空间；  concurrent mode failure 会退化 Serial Old；  参数复杂；  维护成本高。16.4 内存碎片清除后空闲块不连续  -&gt; 总空闲足够     -&gt; 无法分配大对象        -&gt; 触发 Full GC / 压缩碎片常见诱因：  大对象；  长期运行；  对象大小差异大；  晋升频繁；  碎片整理不及时。CMS 有增量整理策略，但难以彻底消除碎片问题。16.5 并发失败常见日志概念：concurrent mode failurepromotion failed原因：  老年代预留不足；  晋升过快；  碎片导致空间不可用；  并发回收跟不上分配；  CPU 不足。后果通常是退化到更重停顿的 Full GC。16.6 常见参数-XX:+UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction=70-XX:+UseCMSInitiatingOccupancyOnly-XX:CMSMaxAbortablePrecleanTime=5000-XX:CMSFullGCsBeforeCompaction=0这些参数只适用于仍支持 CMS 的 JDK。设置 CMSInitiatingOccupancyFraction 太高会增加并发失败风险，太低会频繁并发周期。16.7 与 G1 对比            项      CMS      G1                  堆布局      连续 Old 分代      Region 化              算法      标记清除      复制 / 整理              碎片      明显      通常更低              停顿可控      弱      PausePredictionModel              大对象      直接 Old      Humongous Region              维护      已移除      现代 JDK 持续演进      从 CMS 迁移到 G1：  升级 JDK；  保留 GC 日志和指标基线；  压测对比；  关注大对象和晋升；  检查内存占用变化；  分批灰度；  保留回滚。16.8 排障思路查看 GC 日志：1. CMS 周期频率2. Initial / Final Remark 耗时3. Old 回收量4. 晋升量5. concurrent mode failure6. Full GC 原因处理：            现象      方向                  并发周期频繁      分配高或阈值低              final remark 长      引用处理多              promotion failed      Survivor / Old 不足              concurrent failure      预留不足或 CPU 不足              碎片化      大对象和长期运行      16.9 面试要点CMS 常考问题：  初始标记和最终标记为什么 STW；  并发标记如何处理引用变化；  为什么会有浮动垃圾；  为什么 mark-sweep 会碎片化；  为什么并发失败停顿长；  为什么 CMS 被移除。回答时应说明它是特定历史阶段的折中设计，而不是把 CMS 当作现代默认选择。本章小结CMS 用并发标记清除降低老年代回收停顿，但代价是碎片、浮动垃圾、CPU 竞争和并发失败风险。它已在 Java 14 被移除，现代 Java 服务应优先评估 G1、ZGC 或 Shenandoah。理解 CMS 有助于理解低停顿收集器的演进。思考题  CMS 哪些阶段需要 STW？  为什么 CMS 会产生浮动垃圾？  concurrent mode failure 的常见原因是什么？  CMS 为什么会产生内存碎片？  从 CMS 迁移到 G1 要验证哪些指标？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Serial 和 Parallel 是经典分代收集器。Serial 适合小内存和单核场景，Parallel 追求吞吐量，适合批处理和对停顿不敏感的任务。理解它们有助于建立 GC 的基本模型。15.1 SerialYoung: Serial CopyingOld:   Serial Mark-Sweep-Compact启用：java -XX:+UseSerialGC -jar app.jar特点：            优点      缺点                  实现简单      单线程              内存开销小      停顿随堆增大变长              小内存表现稳定      不适合大堆交互服务              客户端场景经典      多核利用不足      适用：  小容器；  低内存客户端；  单核环境；  简单任务；  极低内存开销优先。15.2 ParallelYoung: Parallel ScavengeOld:   Parallel Old启用：java -XX:+UseParallelGC -jar app.jar特点：            优点      缺点                  多线程标记复制      停顿可能较长              吞吐量高      停顿目标不如 G1 / ZGC              适合批处理      并发能力有限              资源集中执行 GC      交互服务长尾风险      吞吐目标参数：-XX:GCTimeRatio=19-XX:MaxGCPauseMillis=200-XX:ParallelGCThreads=4GCTimeRatio=19 表示目标 GC 时间约 1/(1+19)=5%。MaxGCPauseMillis 是软目标，不是硬保证。15.3 分代布局Heap  |-- New Generation  |   |-- Eden  |   |-- Survivor 0  |   +-- Survivor 1  +-- Old Generation参数：-Xmn512m-XX:NewRatio=2-XX:SurvivorRatio=8-XX:MaxTenuringThreshold=15示例：NewRatio=2  New : Old = 1 : 2SurvivorRatio=8  Eden : one survivor : other survivor = 8 : 1 : 115.4 Minor GC 触发Eden 分配失败  -&gt; Minor GC     -&gt; 存活对象复制到 Survivor        -&gt; 年龄增加           -&gt; 达阈值或放不下晋升 OldFull GC 常见触发原因：  老年代空间不足；  元空间不足；  晋升失败；  GC 实现特定策略；  诊断命令触发。不建议在业务代码中显式请求 Full GC。15.5 停顿模型停顿与以下因素相关：  堆大小；  存活对象数量；  引用密度；  卡表和根扫描；  线程数；  CPU 资源；  内存带宽；  压缩范围。大堆问题：10G 堆，Old 中 6G 存活  -&gt; Full GC 需要标记和压缩大量对象     -&gt; 停顿可能达到秒级15.6 GC 日志Java 9 及以上统一日志：java -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20m \     -XX:+UseParallelGC \     -jar app.jarJava 8：java -XX:+PrintGCDetails \     -XX:+PrintGCDateStamps \     -Xloggc:gc.log \     -jar app.jar关键信息：            信息      说明                  GC 前后堆      回收量              年轻代变化      Minor GC              老年代变化      晋升              user/sys/real      CPU 与墙钟时间              worker 数      并行线程              停顿原因      触发条件      15.7 调优思路吞吐优先调优：1. 控制分配速率2. 增大新生代，降低 Minor GC 频率3. 避免过早晋升4. 保证 Old 有余量5. 合理设置并行线程6. 用真实数据压测风险：            操作      可能问题                  过大新生代      单次 Minor GC 变长              过小新生代      频繁 GC 和晋升              Old 太小      Full GC              Old 太大      Full GC 停顿长              线程过多      CPU 竞争      15.8 与 G1 选择            场景      推荐                  批处理、吞吐优先      Parallel              大堆交互服务      G1 / ZGC              小内存简单任务      Serial              需要可预测停顿      G1 / ZGC              现代微服务常见选择      G1 起步      选择流程：明确 SLA  -&gt; 测量当前停顿和吞吐     -&gt; 先优化分配和缓存        -&gt; 小范围调参           -&gt; 对比 GC 日志              -&gt; 再评估切换 GC15.9 生产案例案例一：夜间批任务 Full GC现象：批处理期间吞吐下降，Full GC 停顿 3 秒。jstat -gcutil &lt;pid&gt; 1000发现：大量对象直接晋升。处理：  拆小批次；  流式读取；  复用缓冲；  增大新生代；  评估 Parallel 目标；  压测验证。案例二：小容器停顿抖动现象：256Mi 容器，交互服务频繁 GC。方向：  降低内存分配；  评估 G1 是否更适合；  限制并发；  扩容；  优化缓存；  控制堆外内存。本章小结Serial 简单低开销，适合小内存；Parallel 多线程吞吐优先，适合批处理。两者都是经典分代模型，停顿会受堆大小、存活对象和 CPU 影响。调优先控制分配和晋升，再调堆比例，最后才考虑切换 GC。思考题  Parallel 的目标是什么？  NewRatio 和 SurvivorRatio 如何计算？  为什么大堆 Full GC 停顿更长？  什么场景仍适合 Serial？  如何比较 Parallel 和 G1 的压测结果？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。GC 需要判断对象是否可达。主流 HotSpot 使用可达性分析，而不是引用计数。并发标记阶段还会遇到三色标注、浮动垃圾和漏标风险。14.1 引用计数object.references++object.references--references == 0 -&gt; recycle优点是实现直观、判定快，但存在循环引用问题：class Node {    Node next;}Node a = new Node();Node b = new Node();a.next = b;b.next = a;a = null;b = null;即使 a 和 b 已不可达，两者计数仍不为 0。HotSpot 主流实现不采用引用计数判定对象存活。14.2 可达性分析从 GC Roots 出发：GC Roots  |-- thread stacks  |-- static fields  |-- JNI references  |-- class loaders  +-- monitor locks遍历对象图  reachable  -&gt; survive  unreachable -&gt; candidate常见根：            根      说明                  线程栈      局部变量和操作数栈              静态字段      类持有的引用              常量引用      常量池中的引用              JNI      native 全局和局部引用              系统类      核心类加载器对象              同步监视器      被锁对象      14.3 安全点GC 根扫描要求线程处于可识别引用位置的状态，即安全点。常见安全点：  方法返回；  循环回边；  异常抛出；  特定调用位置。问题：GC 想停顿  -&gt; 某线程长时间运行到安全点     -&gt; 其他线程等待        -&gt; 停顿时间变长排查诊断参数：-XX:+PrintSafepointStatistics-XX:+PrintGCApplicationStoppedTime参数可用性和输出格式与 JDK 版本相关。14.4 三色标记三色抽象：            颜色      含义                  白      未访问              灰      自身已访问，引用字段未处理完              黑      自身和引用字段已处理      流程：roots -&gt; gray setwhile gray not empty  object = pop gray  scan fields  referenced white -&gt; gray  object -&gt; black并发标记时应用线程同时在修改引用，因此必须保证不漏标存活对象。14.5 多标与少标多标：对象标记时可达  -&gt; 标记后立刻不可达     -&gt; 成为浮动垃圾浮动垃圾通常下一轮标记回收。少标更严重：存活对象未标记  -&gt; 被错误回收     -&gt; 程序数据损坏并发收集器通过写屏障、快照和读屏障等机制保证不漏标。14.6 并发标记阶段G1 典型并发标记周期：Initial Mark  -&gt; Root Scan     -&gt; Concurrent Mark        -&gt; Remark / Final Mark           -&gt; Cleanup            阶段      是否 STW                  Initial Mark      是，通常较短              Concurrent Mark      否              Remark      是              Cleanup      部分停顿      ZGC、Shenandoah 的阶段和屏障设计不同，不能直接套用 G1 的阶段图。14.7 引用对象SoftReference&lt;Cache&gt; soft;WeakReference&lt;Meta&gt; weak;PhantomReference&lt;Resource&gt; phantom;可达性级别：            类型      回收行为                  strong      可达即存活              soft      内存不足时倾向回收              weak      下次 GC 发现仅弱可达即回收              phantom      对象 finalize 后进入引用队列      注意：  soft 行为与策略和内存压力相关；  weak 不等于缓存容量上限；  phantom 不应替代显式 close；  ReferenceQueue 需要处理线程；  引用对象本身也要可达。14.8 方法区与类回收类卸载条件较苛刻：  类所有实例已回收；  加载它的 ClassLoader 已回收；  Class 对象没有其他引用。观察：jcmd &lt;pid&gt; GC.class_histogramjcmd &lt;pid&gt; VM.classloader_stats类泄漏来源：  动态代理；  脚本引擎；  反射生成；  热部署；  容器隔离；  Agent。14.9 MAT 判定泄漏步骤：打开 hprof  -&gt; Leak Suspects     -&gt; Dominator Tree        -&gt; Top Consumers           -&gt; Path to GC Roots              -&gt; 找业务引用链            视图      用途                  Histogram      对象数量              Dominator Tree      谁持有内存              Path to GC Roots      为什么不可回收              Duplicate Classes      类加载器冲突              Thread Overview      线程引用      过滤 weak / soft references 可以突出强引用路径。14.10 常见误判            误判      事实                  引用为 null 立即回收      只是不可达候选              GC 后旧对象一定回收      可能是浮动垃圾              weak cache 有容量上限      没有              heap dump 无影响      通常会触发 GC 和停顿              静态字段一定泄漏      只要有意管理就不是      生产 dump 包含敏感数据，必须有审批、脱敏、加密和保存期限。本章小结HotSpot 用可达性分析判断对象存活，从 GC Roots 遍历对象图。并发标记需要处理三色标记、浮动垃圾和漏标风险。引用强度影响回收倾向，但 soft / weak 不是缓存容量治理。类回收依赖实例、ClassLoader 和 Class 对象都释放。思考题  为什么引用计数不适合 HotSpot？  常见 GC Roots 有哪些？  什么是浮动垃圾？  三色标记中黑色和灰色分别表示什么？  类卸载为什么依赖 ClassLoader 回收？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。对象从分配、使用、不可达、回收到堆空间复用，贯穿 Young GC、晋升、并发标记和整理过程。理解生命周期，才能解释分配速率高、Survivor 溢出、老年代增长和缓存泄漏。13.1 生命周期Allocate  -&gt; Initialize     -&gt; Use        -&gt; Unreachable           -&gt; Collected              -&gt; Heap reused各阶段职责：            阶段      责任方                  分配      JVM，TLAB / Eden / 大 region              初始化      构造代码              使用      业务              不可达      GC 判定              回收      GC              释放      GC 与堆布局策略      13.2 分配速率观察：jstat -gc &lt;pid&gt; 1000关注：            指标      含义                  Eden delta      两次采样间 Eden 使用变化              YGC delta      Young GC 次数              EU / OU      Eden / Old 使用      粗略计算：allocation rate ≈ Eden delta / sample interval高分配速率会推高 Young GC 频率、CPU 消耗、缓存和内存带宽压力，并带来晋升压力。常见来源：  日志字符串；  DTO 重复转换；  大对象；  高频临时集合；  JSON 序列化；  装箱。13.3 Young GC简化流程：Eden 满  -&gt; 根扫描     -&gt; 复制存活对象到 Survivor / Old        -&gt; 清空 Eden 和空 Survivor复制收集特点：            优点      代价                  存活少时快      存活多时成本高              天然整理      复制对象              空间换时间      一块 Survivor 空闲      对象年龄参数：-XX:MaxTenuringThreshold=15-XX:TargetSurvivorRatio=50HotSpot 可能根据 Survivor 压力动态调整晋升年龄，不一定等到 MaxTenuringThreshold。13.4 晋升常见晋升条件：  对象年龄达到阈值；  Survivor 空间不足；  达到动态阈值；  大对象；  GC 和堆布局策略。观察：jstat -gcutil &lt;pid&gt; 1000风险：            现象      可能原因                  Old 持续增长      缓存或泄漏              每轮 YGC 后 Old 增长      过早晋升              Survivor 高      存活对象多              Full GC 后 Old 仍高      长期存活对象      处理：  降低临时对象；  调整新生代；  优化缓存；  拆批；  选择更合适 GC；  用 GC 日志确认。13.5 对象存活时间朝生夕死对象  -&gt; Young 回收，成本低中等生命周期对象  -&gt; Survivor 压力大，可能晋升长期对象  -&gt; Old / concurrent marking缓存对象  -&gt; 应有容量、TTL 和指标Java 应用常存在弱分代假说：多数对象生命周期很短。但 Web 服务的 session、连接、缓存和框架对象会长期存活，因此需要分代和并发标记。13.6 引用链根对象包括：  线程栈局部变量；  静态字段；  JNI 引用；  系统类加载器；  监视器对象；  JVM 内部根。GC Roots  -&gt; field     -&gt; object        -&gt; object           -&gt; cached object 不可回收典型泄漏模式：            模式      示例                  静态集合      static Map 无上限              监听器未注销      事件中心持有对象              ThreadLocal      线程池复用未清理              缓存      无 TTL / 容量              ClassLoader      热部署引用泄漏              Native      堆外未释放      13.7 ThreadLocalprivate static final ThreadLocal&lt;UserContext&gt; CONTEXT =        new ThreadLocal&lt;&gt;();CONTEXT.set(new UserContext(userId));try {    process();} finally {    CONTEXT.remove();}线程池复用线程时，未 remove 会导致：  数据串用；  ClassLoader 泄漏；  对象生命周期被拉长；  安全风险。优先使用显式参数传递；确需上下文时必须成对清理。13.8 Native 与堆外生命周期堆内对象由 GC 回收，堆外资源必须显式释放或通过框架的引用计数处理。try (ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024 * 1024)) {    process(buffer);}Direct ByteBuffer 的释放时机不应依赖 GC；Netty 等框架有自己的引用计数：ByteBuf buf = allocator.directBuffer();try {    process(buf);} finally {    buf.release();}堆外泄漏表现：  heap 正常；  RSS 增长；  Direct buffer OOM；  native memory 统计增长。13.9 对象终止finalize 已被废弃，不应使用。原因：  执行时机不确定；  可能 resurrection；  延迟对象回收；  异常被吞；  性能差。替代：try (Resource resource = open()) {    process(resource);}需要响应堆回收时，优先重新设计生命周期，而不是依赖 finalize 或 Cleaner。13.10 生产案例案例一：Young GC 频繁jstat -gcutil &lt;pid&gt; 1000现象：YGC 每秒多次，Eden 快速填满。处理：  用 allocation profile 找热点；  减少日志拼接；  复用对象；  修复深层查询；  评估新生代大小；  压测验证。案例二：Old 缓慢增长jstat -gcutil &lt;pid&gt; 10000jcmd &lt;pid&gt; GC.heap_dump /tmp/app.hprofMAT 分析：  Dominator Tree；  Top consumers；  Path to GC Roots；  重复对象；  ClassLoader。本章小结对象生命周期决定 GC 成本：朝生夕死对象适合 Young 复制，长期对象进入老年代，无上限缓存和未清理监听器会把生命周期意外拉长。堆内看可达性，堆外看显式释放。分析老年代问题要结合 GC 日志和堆 dump 引用链。思考题  如何估算对象分配速率？  对象为什么会提前晋升？  ThreadLocal 在线程池中有什么风险？  为什么不推荐 finalize？  如何分析 Old 区持续增长？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。AQS 是 J.U.C 中锁和同步器的基础框架，线程池是 Java 应用并发执行的常用形态。理解它们，可以诊断 BLOCKED、任务堆积、拒绝执行、线程泄漏和 CPU 饱和。12.1 AQS 结构AbstractQueuedSynchronizer 核心思想：state  + FIFO wait queue  + CAS  + park / unparkThread A  CAS state 成功    -&gt; 获取资源Thread B  CAS state 失败    -&gt; 入队       -&gt; park          -&gt; A 释放             -&gt; unpark B常见实现：            类      用途                  ReentrantLock      可重入互斥锁              Semaphore      许可证              CountDownLatch      一次倒计数              CyclicBarrier      循环屏障              ReentrantReadWriteLock      读写锁              ThreadPoolExecutor.Worker      工作线程      12.2 ReentrantLockReentrantLock lock = new ReentrantLock();try {    lock.lock();    // 临界区} finally {    lock.unlock();}tryLock：if (!lock.tryLock(100, TimeUnit.MILLISECONDS)) {    throw new TimeoutException("lock timeout");}公平锁：ReentrantLock fair = new ReentrantLock(true);公平锁降低饥饿概率但可能降低吞吐。多数业务使用非公平锁。12.3 读写锁ReentrantReadWriteLock lock = new ReentrantReadWriteLock();lock.readLock().lock();try {    // 读} finally {    lock.readLock().unlock();}lock.writeLock().lock();try {    // 写} finally {    lock.writeLock().unlock();}适用：读多写少、读操作耗时、可接受一定协调开销。StampedLock：StampedLock lock = new StampedLock();long stamp = lock.tryOptimisticRead();int value = data;if (!lock.validate(stamp)) {    stamp = lock.readLock();    try {        value = data;    } finally {        lock.unlockRead(stamp);    }}StampedLock 不可重入，写法复杂，必须有明确压测收益。12.4 线程池参数ThreadPoolExecutor executor = new ThreadPoolExecutor(        8,        16,        60L,        TimeUnit.SECONDS,        new ArrayBlockingQueue&lt;&gt;(1000),        new ThreadFactoryBuilder().setNameFormat("order-%d").build(),        new ThreadPoolExecutor.CallerRunsPolicy());参数：            参数      含义                  corePoolSize      核心线程              maximumPoolSize      最大线程              keepAliveTime      非核心线程空闲时间              workQueue      任务队列              threadFactory      线程创建              rejectedExecutionHandler      拒绝策略      12.5 提交流程提交任务  -&gt; 当前线程数 &lt; core     -&gt; 创建核心线程  -&gt; 否则入队     -&gt; 队列满且线程数 &lt; max        -&gt; 创建非核心线程     -&gt; 队列满且线程数 = max        -&gt; 拒绝这意味着使用无界队列时，maximumPoolSize 不会发挥作用。线程数估算：CPU 密集：约等于 CPU 核数IO 密集：核数 * (1 + 等待时间 / 计算时间)公式只是起点，最终以压测和延迟目标为准。12.6 拒绝策略            策略      行为                  AbortPolicy      抛 RejectedExecutionException              CallerRunsPolicy      提交线程执行              DiscardPolicy      静默丢弃              DiscardOldestPolicy      丢最老任务      生产建议：  显式设置有界队列；  拒绝策略记录日志和指标；  关键业务可降级或落盘；  CallerRuns 可能拖慢上游；  静默丢弃不可接受。自定义拒绝：RejectedExecutionHandler handler = (r, executor1) -&gt; {    log.error("task rejected, queue={}, pool={}",              executor1.getQueue().size(), executor1.getPoolSize());    throw new RejectedExecutionException("executor saturated");};12.7 监控线程池executor.getActiveCount();executor.getPoolSize();executor.getQueue().size();executor.getCompletedTaskCount();executor.getTaskCount();建议暴露指标：active_countpool_sizelargest_pool_sizequeue_sizecompleted_task_countrejected_counttask_latency异常场景：            现象      方向                  queue 持续增长      消费不足              rejected 增长      容量不足或突发流量              active == max 且 CPU 低      任务阻塞在外部 IO              线程数持续增长      线程池泄漏              CPU 高      任务计算密集或线程过多      12.8 优雅关闭executor.shutdown();if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {    List&lt;Runnable&gt; pending = executor.shutdownNow();    log.warn("forced shutdown, pending={}", pending.size());}区别：            方法      行为                  shutdown      不接新任务，存量继续              shutdownNow      尝试中断并返回等待任务              awaitTermination      等待结束      任务要正确响应中断：try {    while (!Thread.currentThread().isInterrupted()) {        process();    }} catch (InterruptedException e) {    Thread.currentThread().interrupt();}12.9 常见错误错误一：Executors 隐藏容量：Executors.newFixedThreadPool(10);Executors.newCachedThreadPool();固定线程池通常是无界 LinkedBlockingQueue，缓存线程池最大线程数近似无界。正确做法是显式 new ThreadPoolExecutor 并指定容量。错误二：没有命名：Executors.defaultThreadFactory()排查线程 dump 时很难定位。应使用自定义 ThreadFactory。错误三：提交任务无超时：Future&lt;String&gt; future = executor.submit(task);future.get();应设置：future.get(3, TimeUnit.SECONDS);错误四：重复创建线程池：每个请求创建一个线程池会导致线程和内存泄漏。线程池应生命周期明确、单例复用并统一治理。12.10 CompletableFutureCompletableFuture&lt;Order&gt; orderFuture =        CompletableFuture.supplyAsync(this::loadOrder, executor);CompletableFuture&lt;User&gt; userFuture =        CompletableFuture.supplyAsync(this::loadUser, executor);OrderView view = orderFuture        .thenCombine(userFuture, this::merge)        .orTimeout(2, TimeUnit.SECONDS)        .join();注意：  显式传入业务线程池；  设置超时；  处理异常；  避免默认 ForkJoinPool 承担远程 IO；  线程上下文要传递；  记录 trace。12.11 线程 dump 中的线程池jstack &lt;pid&gt; &gt; thread.txtjcmd &lt;pid&gt; Thread.print -l &gt; thread.txt观察："order-3" #125 daemon prio=5 ...   java.lang.Thread.State: WAITING (parking)   at jdk.internal.misc.Unsafe.park   at java.util.concurrent.locks.LockSupport.park判断：  线程数量；  线程名称；  等待位置；  锁持有者；  是否重复创建；  是否阻塞外部调用。本章小结AQS 通过 state、队列、CAS 和 park 实现同步器。线程池要显式设置核心数、最大线程、有界队列、命名工厂、拒绝策略和监控。优雅停机依赖 shutdown、awaitTermination、任务可中断和上下游摘流。思考题  AQS 的核心组成是什么？  线程池提交任务的顺序是什么？  为什么不推荐无界队列？  四种拒绝策略分别适合什么场景？  如何为一个 IO 密集服务设计线程池？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。volatile 提供可见性和有序性，CAS 提供无锁原子更新。两者是并发工具和高效计数器的基础，但也都有限制：volatile 不解决复合操作原子性，CAS 可能自旋、ABA 和缓存竞争。11.1 volatile 语义写：volatile write  前面的读写不会重排到写之后  对其他线程可见读：volatile read  后面的读写不会重排到读之前  能看到最后一次 volatile 写适用：  状态标志；  单次发布；  双重检查锁；  独立观察值；  配合 CAS 的热点字段。不适用：private volatile int count;count++; // 仍然不是原子操作11.2 状态标志示例public class Worker {    private volatile boolean running = true;    public void run() {        while (running) {            process();        }    }    public void shutdown() {        running = false;    }}如果 running 不是 volatile，工作线程可能长期看不到修改。更完整的停止方式：private volatile boolean running = true;public void run() {    while (running &amp;&amp; !Thread.currentThread().isInterrupted()) {        try {            process();        } catch (InterruptedException e) {            Thread.currentThread().interrupt();            break;        }    }}11.3 CAS 原理CAS（Compare And Swap）包含三个操作数：            操作数      含义                  location      内存位置              expected      预期旧值              new      新值      流程：if current == expected  -&gt; write new  -&gt; return successelse  -&gt; return failureJava 通过 Unsafe / VarHandle 提供底层能力，业务通常使用 java.util.concurrent.atomic。11.4 Atomic 使用计数：AtomicLong counter = new AtomicLong();counter.incrementAndGet();counter.updateAndGet(v -&gt; v + 10);counter.compareAndSet(0, 1);引用：AtomicReference&lt;State&gt; state = new AtomicReference&lt;&gt;(State.NEW);state.compareAndSet(State.NEW, State.RUNNING);字段：AtomicIntegerFieldUpdater&lt;Config&gt; updater =        AtomicIntegerFieldUpdater.newUpdater(Config.class, "version");updater.incrementAndGet(config);字段 updater 要求字段是 volatile int，且访问权限正确。11.5 LongAdder 与 LongAccumulator高并发计数时，AtomicLong 会在同一 value 上 CAS 竞争。LongAdder counter = new LongAdder();counter.increment();counter.add(10);long total = counter.sum();LongAdder 将更新分散到多个 Cell，读取时汇总。适合写多读少的统计。选择：            场景      工具                  竞争低      AtomicLong              高并发计数、偶尔汇总      LongAdder              自定义聚合      LongAccumulator              需要精确单值 CAS      AtomicLong      11.6 ABA 问题ABA：线程 1 看到 A线程 2 把 A 改成 B，再改回 A线程 1 CAS(A -&gt; C) 成功但中间状态已经被忽略数值统计通常不关心 ABA；链表、无锁栈、资源版本等场景可能关心。解决：AtomicStampedReference&lt;Integer&gt; ref =        new AtomicStampedReference&lt;&gt;(10, 1);int[] stampHolder = new int[1];Integer current = ref.get(stampHolder);ref.compareAndSet(current, 20, stampHolder[0], stampHolder[0] + 1);更常见做法是使用带版本号的对象或不可变状态。11.7 CAS 自旋与开销失败时通常重试：int current;int next;do {    current = value.get();    next = current + 1;} while (!value.compareAndSet(current, next));风险：  高竞争下自旋耗 CPU；  缓存行竞争；  长时间失败导致延迟；  复杂无锁算法难以验证。处理：  使用 LongAdder；  拆分热点；  使用锁；  批量更新；  用压测选择方案。11.8 VarHandleJava 9 后推荐使用 VarHandle 替代部分 Unsafe 用法。public class Buffer {    private volatile int size;    private static final VarHandle SIZE;    static {        try {            SIZE = MethodHandles.lookup()                    .findVarHandle(Buffer.class, "size", int.class);        } catch (ReflectiveOperationException e) {            throw new ExceptionInInitializerError(e);        }    }    public void setRelease(int value) {        SIZE.setRelease(this, value);    }    public int getAcquire() {        return (int) SIZE.getAcquire(this);    }}普通业务不需要手写 VarHandle；了解它有助于阅读 JDK 并发实现。11.9 无锁队列与并发容器常见容器：            容器      特点                  ConcurrentLinkedQueue      非阻塞无界队列              ArrayBlockingQueue      有界阻塞              LinkedBlockingQueue      可选容量链表队列              SynchronousQueue      直接交接              DelayQueue      延迟元素              ConcurrentLinkedQueue      高并发吞吐      无界队列会掩盖背压问题：生产速率 &gt; 消费速率  -&gt; 队列无限增长     -&gt; heap OOM生产系统应使用有界队列和明确拒绝策略。11.10 volatile 与 CAS 的组合public class CasState {    private final AtomicReference&lt;State&gt; state =            new AtomicReference&lt;&gt;(State.NEW);    private volatile Config config;    public void start(Config newConfig) {        if (!state.compareAndSet(State.NEW, State.STARTING)) {            throw new IllegalStateException("state=" + state.get());        }        config = newConfig;        state.set(State.RUNNING);    }}组合原则：  状态转换用 CAS；  配置发布用 volatile 或不可变对象；  复合状态封装在一个不可变对象；  失败路径要恢复状态；  对外暴露清晰生命周期。本章小结volatile 解决可见性和有序性，CAS 解决单个变量的原子更新。它们不是锁的完全替代品；复杂状态和临界区仍需要锁、不可变对象或并发容器。高并发计数优先评估 LongAdder，生产队列必须有界。思考题  volatile 能否让 count++ 线程安全？  CAS 的三个操作数是什么？  什么是 ABA，何时需要关注？  AtomicLong 和 LongAdder 有什么差异？  为什么生产系统不推荐无界队列？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。synchronized 是 Java 内建的监视器锁，既有互斥功能，也有内存语义。HotSpot 对它做了偏向锁、轻量级锁、重量级锁等优化，不同 JDK 版本的实现和废弃策略也在变化。10.1 基本用法实例方法：public synchronized void transfer() {    // 同一个实例互斥}静态方法：public static synchronized void reload() {    // 类对象互斥}代码块：private final Object lock = new Object();public void update() {    synchronized (lock) {        // 指定对象互斥    }}三种锁对象不同：            写法      锁对象                  实例方法      当前实例              静态方法      类 Class 对象              代码块      括号中的对象      不要用字符串常量、Integer 缓存对象等不可控对象做锁。10.2 内存语义解锁 happens-before 后续锁定：Thread A  写共享变量  unlock      |      v happens-beforeThread B  lock  读共享变量因此临界区内修改对后续进入同一锁的线程可见。10.3 锁升级传统 HotSpot 锁状态：无锁  -&gt; 偏向锁     -&gt; 轻量级锁        -&gt; 重量级锁            状态      适用                  偏向锁      只有一个线程访问，JDK 15 后逐步废弃              轻量级锁      短暂交替竞争，CAS 膨胀              重量级锁      竞争强，依赖操作系统 mutex，可能阻塞      JDK 15 引入默认禁用偏向锁，后续版本逐步移除。不要把锁升级细节写成永久不变的结论，应以当前 HotSpot 文档为准。10.4 对象头与 Mark Word对象头 Mark Word 会存储锁状态、identity hash、GC 年龄等信息。不同状态下位含义不同。查看：System.out.println(ClassLayout.parseInstance(object).toPrintable());注意：  identity hash 与部分锁状态存在交互；  锁状态是 JVM 实现细节；  业务代码不应依赖 Mark Word 布局；  性能分析可借助 JOL 和日志。10.5 锁优化JIT 可能执行：            优化      场景                  锁消除      对象不可能逃逸被其他线程访问              锁粗化      相邻重复锁同一对象              自旋      短暂等待，避免立即阻塞      逃逸分析示例：public String build() {    StringBuffer buffer = new StringBuffer();    buffer.append("a");    return buffer.toString();}如果 buffer 不逃逸，相关同步可能被消除。这不代表业务应滥用同步，而是说明 JVM 能优化明显局部对象。10.6 等待通知private final Object lock = new Object();private boolean ready = false;public void consume() throws InterruptedException {    synchronized (lock) {        while (!ready) {            lock.wait();        }        // 消费    }}public void produce() {    synchronized (lock) {        ready = true;        lock.notifyAll();    }}规则：  必须在持有锁时调用 wait / notify；  wait 要放在 while 循环中；  wait 会释放锁；  sleep 不释放锁；  优先使用 Condition 或高层队列。10.7 死锁public class Deadlock {    private final Object a = new Object();    private final Object b = new Object();    public void left() {        synchronized (a) {            synchronized (b) {            }        }    }    public void right() {        synchronized (b) {            synchronized (a) {            }        }    }}产生条件：  互斥；  持有并等待；  不可剥夺；  循环等待。避免：  统一加锁顺序；  缩小临界区；  使用 tryLock 超时；  一次事务只拿必要资源；  拆分锁粒度；  用超时和熔断控制外部调用。10.8 锁粒度过粗：synchronized (globalLock) {    processAllTenants();}问题：串行化、吞吐低、长尾高。过细：synchronized (map.get(tenantId)) {}风险：  锁对象可能变化；  复合操作不原子；  锁对象可能为 null；  引入隐藏竞态。更清晰的方式：private final ConcurrentHashMap&lt;String, Object&gt; locks = new ConcurrentHashMap&lt;&gt;();Object lock(String tenantId) {    return locks.computeIfAbsent(tenantId, k -&gt; new Object());}如果锁对象会累积，还需要清理策略。10.9 synchronized 与 Lock            特性      synchronized      ReentrantLock                  语法      语言内建      API              释放      自动      finally 显式 unlock              tryLock      不支持      支持              超时      不支持      支持              公平锁      不支持      可选              Condition      单一 wait 队列      多 Condition              可中断      不支持      lockInterruptibly      优先 synchronized；只有需要超时、可中断、公平或多条件队列时再考虑 Lock。10.10 生产排查线程 dump：jstack &lt;pid&gt;jcmd &lt;pid&gt; Thread.print -larthas thread -b死锁输出：Found one Java-level deadlock常见阻塞状态：            状态      含义                  BLOCKED      等待监视器锁              WAITING      wait / join / park              TIMED_WAITING      有超时等待              RUNNABLE      运行或等待 IO      锁监控：jstat -printsynchro &lt;pid&gt;jcmd &lt;pid&gt; Thread.print10.11 性能建议  临界区只做内存操作和局部计算；  不要在锁内调用 RPC、数据库、文件 IO；  避免在锁内创建大量对象；  复合状态使用不可变快照；  优先并发容器；  读多写少可评估 ReadWriteLock 或 StampedLock；  用压测验证锁竞争；  保证正确性后再优化。本章小结synchronized 提供互斥和内存可见性，锁状态和优化由 JVM 根据竞争情况处理。现代 JDK 中应优先使用清晰简单的 synchronized，必要时再用 Lock。生产排障靠线程 dump、锁竞争指标和调用链定位。思考题  实例方法和静态方法的锁对象分别是什么？  为什么 wait 必须写在 while 中？  死锁的四个条件是什么？  偏向锁的版本变化说明了什么？  如何定位 BLOCKED 线程的阻塞源头？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Java 内存模型（JMM）定义多线程读写共享变量时的可见性、有序性和原子性。很多并发 bug 只在特定调度、缓存和编译器优化下出现，理解 JMM 才能判断代码是否正确。9.1 问题来源Thread A  -&gt; write shared variable     -&gt; store buffer / cache        -&gt; memoryThread B  -&gt; read shared variable     -&gt; cache        -&gt; 可能读到旧值编译器、CPU 和内存系统都可能重排指令。单线程语义不变时，重排对单线程不可见，但多线程之间可能产生不同结果。三类问题：            问题      示例                  可见性      一个线程修改，另一个线程看不到              原子性      count++ 非原子              有序性      语句编写顺序与观察顺序不同      9.2 happens-beforeJMM 用 happens-before 表达可见性规则。若 A happens-before B，则 A 的结果对 B 可见。常见规则：  单线程程序顺序规则；  监视器锁解锁 happens-before 后续锁定；  volatile 写 happens-before 后续读；  线程 start happens-before 该线程内操作；  线程内操作 happens-before join 返回；  传递性；  线程中断调用 happens-before 检测中断；  对象默认初始化 happens-before 其他线程看到引用。错误理解：happens-before 不是时间先后，而是内存可见性约束。9.3 数据竞争public class Counter {    private int count;    public void increment() {        count++;    }    public int get() {        return count;    }}问题：  多线程写共享字段；  没有同步；  count++ 分为读、加、写；  存在数据竞争。正确方案：private final AtomicInteger count = new AtomicInteger();public void increment() {    count.incrementAndGet();}或：public synchronized void increment() {    count++;}9.4 重排示例public class ReorderDemo {    int a = 0;    boolean ready = false;    public void writer() {        a = 1;        ready = true;    }    public void reader() {        if (ready) {            System.out.println(a);        }    }}多线程下 reader 可能观察到 ready == true 但 a == 0。修复：volatile boolean ready = false;volatile 写之前的写，对后续 volatile 读可见，并限制相关重排。9.5 final 语义正确构造对象后，final 字段具有安全发布语义。public class Config {    private final int version;    private final List&lt;String&gt; nodes;    public Config(int version, List&lt;String&gt; nodes) {        this.version = version;        this.nodes = List.copyOf(nodes);    }}注意：  构造函数中不要泄漏 this；  final 引用不可变不代表对象内部可变状态线程安全；  集合应使用不可变集合；  静态工厂返回不可变对象更清晰。9.6 安全发布不安全：public mutable Object instance;其他线程可能看到部分构造对象。安全方式：            方式      机制                  volatile 字段      可见性和禁止重排              final 字段      正确构造后的语义              锁      互斥和内存语义              并发容器      内部同步              Atomic 类      CAS 和 volatile              线程 start / join      happens-before      9.7 原子性            操作      是否原子                  引用赋值      通常原子，但可见性需另行保证              long / double 写      Java 规范允许特殊处理，volatile 可保证              i++      否              AtomicLong.incrementAndGet      是              ConcurrentHashMap.put      单个映射操作原子      ConcurrentHashMap 保证单个操作的原子性，复合操作仍需 compute 等原子方法：map.compute(key, (k, old) -&gt; old == null ? 1 : old + 1);9.8 内存屏障常见屏障：            屏障      作用                  LoadLoad      前面读与后面读不重排              StoreStore      前面写与后面写不重排              LoadStore      前面读与后面写不重排              StoreLoad      前面写与后面读不重排      volatile 在不同 CPU 架构上可能生成不同指令。JMM 屏蔽硬件差异，编写代码时关注 Java 语义即可。9.9 常见误区            误区      事实                  volatile 让 ++ 原子      不会              synchronized 只做互斥      还有内存语义              sleep 会释放锁      不释放              双重检查不需要 volatile      需要正确写法              HashMap 加锁读就一定安全      也要看读写路径              ConcurrentHashMap 复合操作天然原子      不一定      双重检查锁：public class Singleton {    private static volatile Singleton instance;    public static Singleton getInstance() {        Singleton result = instance;        if (result == null) {            synchronized (Singleton.class) {                result = instance;                if (result == null) {                    result = new Singleton();                    instance = result;                }            }        }        return result;    }}更简单的方式是使用静态 holder 或 enum。9.10 JMM 与 GCJMM 主要讨论线程可见性，GC 主要讨论对象生命周期，但它们在引用发布上存在交互：  安全发布让 GC 能一致地看到引用；  weak / soft / phantom 引用读取需要同步；  ReferenceQueue 清理与业务线程并发；  finalizer 语义复杂，不推荐资源清理；  Cleaner 也有时序限制。堆外资源应使用 try-with-resources 明确释放。本章小结JMM 用 happens-before 定义多线程可见性。解决数据竞争要选择锁、volatile、Atomic、并发容器或不可变对象。volatile 提供可见性和有序性，不提供复合操作原子性。并发正确性先于性能优化。思考题  happens-before 和时间先后有什么区别？  为什么 count++ 不是原子操作？  volatile 能解决哪些问题，不能解决哪些问题？  final 的安全发布语义是什么？  如何证明一段代码存在数据竞争？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。方法调用是 Java 多态和模块化的基础，也是 JIT 优化的关键边界。调用是否可以内联、虚方法有几个实现、lambda 如何编译、异常是否在热路径，都会影响峰值性能。8.1 调用指令            指令      场景                  invokestatic      静态方法              invokespecial      构造器、private、super              invokevirtual      实例方法              invokeinterface      接口方法              invokedynamic      动态调用点      示例：public class CallDemo {    public void run() {        staticCall();        virtualCall(this);        interfaceCall(new Runnable() {            public void run() {                System.out.println("task");            }        });    }    private static void staticCall() {}    private void virtualCall(Object object) {}    private static void interfaceCall(Runnable task) {        task.run();    }}查看：javap -c CallDemo8.2 静态分派与动态分派静态分派：void print(String value) {}void print(Integer value) {}print("java");编译期根据静态类型选择重载方法。动态分派：Service service = new OrderService();service.execute();运行期根据实际对象类型选择实现。            类型      时机                  重载      编译期静态匹配              覆写      运行期动态分派      8.3 内联内联把方法体复制到调用点：caller()  -&gt; callee()变为：caller()  -&gt; callee 逻辑收益：  减少调用开销；  跨方法优化；  消除无法逃逸的对象；  支持锁消除；  提高缓存局部性。限制：  方法过大；  异常处理复杂；  虚方法实现过多；  字节码超过阈值；  类型信息不确定。8.4 内联阈值常见诊断参数：-XX:MaxInlineSize-XX:FreqInlineSize-XX:+PrintInlining查看：java -XX:+PrintFlagsFinal -version | grep Inline不建议先调阈值。优先：  用 profile 找热点；  检查方法是否过大；  检查接口实现数量；  拆小热点方法；  用 JMH 验证。8.5 多态与去虚拟化interface Payment {    void pay();}如果运行时只有一个实现，JIT 可能激进内联。之后出现第二个实现，会触发去优化。Mono / Bimorphic  -&gt; 内联机会较好Megamorphic  -&gt; 通常走常规虚调用生产意义：  压测输入要覆盖真实类型分布；  抽象层次不一定必然导致慢；  框架动态代理可能增加调用层；  先测再优化；  保持小方法比手工写大方法更有利于优化。8.6 Lambda 与方法引用int total = orders.stream()        .mapToInt(Order::amount)        .sum();Lambda 常通过 invokedynamic 创建调用点，不需要为每个 lambda 显式生成匿名类，具体机制与 JDK 版本有关。方法引用：Function&lt;String, Integer&gt; parser = Integer::parseInt;性能建议：  避免在超热循环中重复创建捕获 lambda；  复用无状态函数对象；  不要为了性能牺牲可读性；  用 JMH 验证；  关注自动装箱。8.7 反射与 MethodHandle反射：Method method = Order.class.getMethod("id");Object value = method.invoke(order);优化：method.setAccessible(true);MethodHandles.Lookup lookup = MethodHandles.lookup();MethodHandle handle = lookup.unreflected(method);现代 JDK 对反射有优化和缓存，但高频调用仍可能带来额外开销。框架常见做法：  缓存 Method；  字节码生成代理；  MethodHandle；  LambdaMetafactory；  编译期生成代码。8.8 动态代理JDK Proxy：Service proxy = (Service) Proxy.newProxyInstance(        Service.class.getClassLoader(),        new Class&lt;?&gt;[]{Service.class},        (proxyObj, method, args) -&gt; {            long start = System.nanoTime();            try {                return method.invoke(new OrderService(), args);            } finally {                long cost = System.nanoTime() - start;                System.out.println(method.getName() + " cost=" + cost);            }        });成本：  多层调用；  装箱；  反射；  栈深增加；  参数数组分配。生产排查要能关闭或采样增强逻辑。8.9 异常与调用成本问题示例：try {    int value = Integer.parseInt(input);} catch (NumberFormatException e) {    return 0;}异常作为控制流的成本来自：  填充栈；  构造对象；  捕获处理；  影响 JIT 优化；  日志放大。高频路径应先校验，或返回结果对象。8.10 内联诊断PrintInlining：java -XX:+UnlockDiagnosticVMOptions \     -XX:+PrintInlining \     -jar app.jar输出示例：inline (hot)  com.example.OrderService.calculate(int)常见标记：            标记      含义                  inline      已内联              too large      方法过大              hot method too big      热点方法过大              not inlinable      无法内联              megamorphic      多实现调用      生产建议先用 async-profiler 火焰图确认热点，再在测试环境打开诊断参数。本章小结方法调用指令决定分派方式，JIT 内联是跨方法优化的基础。小方法、稳定的类型分布和低异常频率通常更利于优化。接口、动态代理和反射不一定必然慢，但需要用 profile 和基准测试验证。思考题  invokevirtual 和 invokeinterface 的差异是什么？  内联有哪些收益和限制？  什么是 megamorphic call？  Lambda 与匿名类在实现上有什么不同？  为什么异常不适合作为高频控制流？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Java 既有解释执行的启动优势，也有 JIT 编译到本地代码后的峰值性能。分层编译、热点探测、去优化和代码缓存共同决定了 Java 服务的预热、抖动和压测结果。7.1 执行方式Bytecode  -&gt; Interpreter  -&gt; C1 编译代码  -&gt; C2 编译代码            方式      特点                  Interpreter      启动快，无需等待编译，峰值慢              C1      编译快，优化相对保守              C2      编译慢，优化激进，峰值高              Graal      可选编译器，架构不同      HotSpot 常见默认是分层编译：java -XX:+PrintFlagsFinal -version | grep TieredCompilation7.2 热点探测方法触发编译的常见依据：  方法调用计数；  回边计数；  分层阈值。相关参数：-XX:CompileThreshold-XX:-TieredCompilation-XX:+PrintCompilation查看：jstat -printcompilation &lt;pid&gt; 1000jcmd &lt;pid&gt; Compiler.codecache这就是 Java 服务需要预热的原因：启动初期  解释执行 + C1    -&gt; 热点累积       -&gt; C2 编译          -&gt; 峰值性能稳定7.3 JIT 优化            优化      说明                  方法内联      把被调方法复制进调用点，减少调用开销              逃逸分析      判断对象是否逃逸              锁消除      去掉不可能竞争的锁              锁粗化      合并相邻锁操作              公共子表达式消除      避免重复计算              循环展开      减少循环控制开销              去虚拟化      根据类型 profile 内联虚方法      优化基于运行时 profile，因此同一段代码在不同流量和输入分布下性能可能不同。7.4 去优化JIT 根据 profile 做乐观优化。假设被打破时会去优化，回退到较低层执行。常见触发：  新类型出现导致去虚拟化失败；  类加载改变继承关系；  异常路径进入；  优化假设失效；  监控点失效。观察：java -XX:+PrintCompilation -jar app.jar输出中出现 made not entrant 或 deoptimized 常见于去优化。诊断参数不要在高峰生产随意开启。7.5 Code Cache查看：jcmd &lt;pid&gt; Compiler.codecache分段：            段      用途                  non-method      生命周期长的 JVM stub              profiled      带 profile 的 C1 代码              non-profiled      C2 或优化后代码      满了可能：  停止 JIT；  退回解释执行；  延迟升高；  CPU 上升。参数：-XX:ReservedCodeCacheSize=256m7.6 JIT 与容器容器中要关注：  CPU limit 与编译线程数；  启动阶段 CPU 不足导致预热慢；  readiness 过早放流量；  弹性扩容后新实例冷启动；  CPU throttling 与长尾延迟。JVM 会根据可用 CPU 调整编译线程和 GC 线程。容器感知能力随 JDK 版本改进，建议使用现代 LTS。7.7 预热策略发布后：1. readiness 通过后再接流量2. 少量灰度流量3. 观察延迟和错误率4. 逐步增加5. 达到稳定后接全量压测：低流量  -&gt; 中流量     -&gt; 目标流量        -&gt; 峰值流量如果启动后立即全量压测，测到的往往是预热成本和资源竞争，不是稳定容量。7.8 基准测试JMH 示例：@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)@State(Scope.Thread)public class StringBenchmark {    @Benchmark    public String concat() {        return "a" + "b" + "c";    }}JMH 要点：  防止死代码消除；  fork 隔离进程；  预热；  多轮测量；  报告置信区间；  记录 JDK 和参数。7.9 火焰图async-profiler：./asprof -d 30 -f cpu.html &lt;pid&gt;Arthas：profiler startprofiler stop --format html解读：            特征      可能含义                  解释器栈占比高      预热或去优化              JIT 编译线程高      启动或编译压力大              某业务方法很宽      CPU 热点              GC 栈宽      GC 占用高              lock / park 高      阻塞等待      7.10 生产案例案例一：发布后 P99 高现象：新实例启动后前几分钟延迟高。排查：jstat -printcompilation &lt;pid&gt; 1000cat /sys/fs/cgroup/&lt;path&gt;/cpu.stat结论：实例仍预热且触发 CPU throttling。处理：  更新 readiness；  灰度流量；  提高 CPU limit；  调整扩容阈值；  评估 AppCDS 或 AOT。案例二：CodeCache 满jcmd &lt;pid&gt; Compiler.codecache处理：  增大 Code Cache；  减少动态类生成；  升级 JDK；  修复反射生成类问题；  观察去优化。本章小结解释器负责快速启动，JIT 负责峰值性能，分层编译结合两者。JIT 依赖 profile，因此服务需要预热，异常输入和类型分布可能导致去优化。生产性能评估必须区分冷启动、预热完成和稳定阶段。思考题  为什么 Java 服务启动初期性能较低？  C1 和 C2 有什么差异？  什么是去优化？  Code Cache 满会造成什么影响？  如何设计发布后的预热策略？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。字符串是业务系统里最常见的对象之一。理解 String 不可变性、字符串常量池、intern、编码和拼接成本，可以避免一类典型内存和性能问题。6.1 String 不可变String a = "java";String b = a.concat("-17");System.out.println(a);System.out.println(b);不可变的收益：  字符串常量池可复用；  hashCode 可缓存；  并发读写安全；  作为 Map key 稳定；  安全边界清晰。代价：  修改会创建新对象；  高频拼接需要中间对象；  大字符串转换成本高。6.2 字符串常量池String literal = "java";String heap = new String("java");String interned = heap.intern();System.out.println(literal == heap);System.out.println(literal == interned);典型输出：falsetrue常量池位置随 JDK 版本变化。Java 7 起 HotSpot 将字符串常量池放在堆中，但这是实现细节，不应作为业务逻辑基础。常量池复用：String a = "order";String b = "order";System.out.println(a == b);运行时拼接：String prefix = "or";String value = prefix + "der";System.out.println(value == "order");典型输出为 false。编译期常量拼接可能被优化，具体以字节码和常量池为准。6.3 intern 的使用与风险String normalized = value.intern();可能收益：  大量重复字符串减少堆占用；  相同字面量复用；  某些比较场景减少比较成本。风险：  常量池占用堆；  高频 intern 增加竞争；  生命周期难以控制；  动态字符串大量进入池；  泄漏排查复杂。生产建议：优先使用 equals、枚举、字典编码或外部缓存。不要把 intern 当作万能去重器。6.4 String、StringBuilder、StringJoinerStringBuilder builder = new StringBuilder();for (int i = 0; i &lt; 10; i++) {    builder.append(i).append(',');}String result = builder.toString();StringJoiner：String result = new StringJoiner(",", "[", "]")        .add("a")        .add("b")        .toString();选择：            场景      工具                  单次拼接      +              循环拼接      StringBuilder              指定分隔符      StringJoiner              多行文本      Text Block              高频模板      预编译模板      6.5 字符编码Java String 内部以 UTF-16 表示，常见 API 会处理编码转换。byte[] utf8 = value.getBytes(StandardCharsets.UTF_8);String decoded = new String(utf8, StandardCharsets.UTF_8);长度：String value = "a\u00e9b";System.out.println(value.length());System.out.println(value.codePointCount(0, value.length()));常见问题：            问题      原因                  中文乱码      请求、响应、文件、数据库编码不一致              字节数超限      UTF-8 中文常占 3 字节，字符数不等于字节数              emoji 截断      UTF-16 代理对              排序异常      未使用 Collator      接口应统一 UTF-8，并显式指定 charset。6.6 字符串与 JSON常见内存放大：HTTP response  -&gt; byte[]     -&gt; String        -&gt; JSON tree           -&gt; DTO优化：  流式解析大 JSON；  避免 SELECT *；  分页返回；  复用解析器；  不要把大响应完整转字符串；  控制日志字符串长度。Jackson 流式：JsonFactory factory = new JsonFactory();try (JsonParser parser = factory.createParser(new File("events.json"))) {    while (parser.nextToken() != null) {        // 处理 token    }}6.7 字符串去重G1 字符串去重：-XX:+UseStringDeduplication适用条件：  使用 G1；  大量重复字符数组；  存活时间长；  用 CPU 换内存可接受。业务层去重：Map&lt;String, String&gt; canonical = new ConcurrentHashMap&lt;&gt;();String shared = canonical.computeIfAbsent(value, k -&gt; k);必须设置上限和过期策略，否则会变成新的泄漏点。6.8 equals 与 hashCodeString a = new String("order");String b = new String("order");System.out.println(a == b);System.out.println(a.equals(b));业务 key 应使用明确类型或 record：record CacheKey(long tenantId, String scene) {}6.9 内存分析直方图：jcmd &lt;pid&gt; GC.class_histogram | grep -E 'String|byte\[\]|char\[\]'堆 dump：MAT Histogram  -&gt; java.lang.String     -&gt; Group by value        -&gt; 查看重复字符串和大字符串关注：  String 数量；  value 数组大小；  重复值；  retained heap；  引用来源；  是否来自日志、缓存、反序列化。6.10 生产案例案例一：日志拼接导致 CPU 高问题代码：log.debug("order=" + hugeObject);即使日志级别关闭，拼接也已执行。处理：log.debug("order={}", () -&gt; expensiveToString(hugeObject));或前置判断：if (log.isDebugEnabled()) {    log.debug("order={}", expensiveToString(hugeObject));}案例二：重复字符串占用高发现：jcmd &lt;pid&gt; GC.class_histogram | head -20处理：  修复 DTO 重复转换；  增加缓存上限；  使用字典编码；  评估 G1 String Deduplication；  控制响应字段。本章小结String 不可变带来安全和复用收益，也意味着高频修改会产生大量中间对象。常量池和 intern 应谨慎使用，业务比较始终以 equals 为准。大 JSON、日志拼接和重复字符串是生产内存问题高发点。思考题  字面量字符串和 new String 有什么区别？  为什么循环拼接推荐 StringBuilder？  String.length() 返回什么长度？  intern 有哪些风险？  如何分析 String 占用过高？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。对象在堆里不只是字段数据，还包括对象头、可能的对齐填充和引用关系。理解对象大小和字段布局，有助于估算缓存容量、解释 false sharing、分析指针压缩和定位大对象。5.1 对象创建流程类加载检查  -&gt; 分配空间     -&gt; 初始化零值        -&gt; 设置对象头           -&gt; 执行构造函数 &lt;init&gt;分配路径：            路径      条件                  栈上分配      逃逸分析判断未逃逸，可能被优化              TLAB      线程本地分配缓冲，常见              Eden      TLAB 分配失败或不使用 TLAB              Old / 大 region      大对象，GC 相关      TLAB 示例参数：-XX:+UseTLAB-XX:+PrintTLABPrintTLAB 属于诊断参数，输出与 JDK 版本有关，不建议生产长期开启。5.2 对象结构Object  |-- Object Header  |   |-- Mark Word  |   |-- Klass Pointer  |   +-- Array Length（数组对象）  |-- Instance Data  +-- Padding64 位 HotSpot 常见情况：            状态      Mark Word      Klass Pointer                  开启压缩指针      8 字节      4 字节              关闭压缩指针      8 字节      8 字节      压缩指针默认行为与堆大小、JDK 和启动参数有关：java -XX:+PrintFlagsFinal -version | grep UseCompressedOops5.3 空对象大小public class Empty {}开启压缩指针时，常见估算是对象头 12 字节，再按 HotSpot 对齐策略处理。实际布局由 JVM 实现、字段类型和对齐策略决定，生产应以 JOL 输出为准。JOL 依赖：&lt;dependency&gt;  &lt;groupId&gt;org.openjdk.jol&lt;/groupId&gt;  &lt;artifactId&gt;jol-core&lt;/artifactId&gt;  &lt;version&gt;0.17&lt;/version&gt;&lt;/dependency&gt;查看：System.out.println(ClassLayout.parseInstance(new Empty()).toPrintable());5.4 字段布局public class FieldOrder {    private boolean flag;    private long value;    private int id;    private byte data;}JVM 会按策略重排字段，通常为了对齐和节省空间。字段声明顺序不保证内存顺序。类型大小：            类型      大小                  byte      1              boolean      通常 1              short / char      2              int / float      4              long / double      8              reference      开启压缩指针常见 4      缓存影响示例：缓存 100 万个对象  每个对象多 16 字节    -&gt; 额外约 16MB5.5 数组对象数组对象头多一个数组长度字段。byte[] bytes = new byte[16];int[] ints = new int[16];Object[] refs = new Object[16];估算：byte[16] = header + 16int[16] = header + 64大数组可能直接进入老年代或大 region，具体阈值由 GC 和对象大小决定。G1 可调整 region：-XX:G1HeapRegionSize=4m大对象过多会导致：  Humongous 区域占用高；  复制成本高；  碎片或停顿增加；  堆有效容量下降。5.6 引用类型            引用      回收时机      用途                  strong      可达不回收      普通对象              soft      内存不足前后      缓存，行为依赖实现              weak      下次 GC      弱引用缓存、规范化映射              phantom      对象 finalize 后      堆外资源清理      示例：Map&lt;Object, byte[]&gt; cache = new WeakHashMap&lt;&gt;();Object key = new Object();cache.put(key, new byte[1024]);key = null;弱引用键被回收后，entry 才可能被清理。缓存语义要明确，不能把 WeakHashMap 当作容量上限。5.7 逃逸分析public long noEscape() {    Point point = new Point(1, 2);    return point.x() + point.y();}如果 point 不逃逸方法，JIT 可能进行标量替换、消除对象分配，甚至消除关联锁。查看诊断参数：java -XX:+DoEscapeAnalysis -XX:+PrintEscapeAnalysis -version诊断参数在不同 JDK 中可用性不同。不要为“优化”故意写不逃逸代码而牺牲可读性，应让 JIT 自动优化。5.8 对象大小分析JOL：System.out.println(GraphLayout.parseInstance(object).toFootprint());堆直方图：jcmd &lt;pid&gt; GC.class_histogramjmap -histo:live &lt;pid&gt;MAT：Histogram -&gt; 按对象类型和 shallow / retained heap 排序Dominator Tree -&gt; 看谁持有大量内存Path to GC Roots -&gt; 查看引用链            指标      含义                  shallow heap      对象自身大小              retained heap      回收它可释放的总大小      5.9 false sharingCPU 缓存行通常为 64 字节。两个高频修改变量落在同一缓存行时，多核之间会反复失效缓存行。public class Counters {    private volatile long a;    private volatile long b;}Java 有 @Contended 相关能力，但通常属于 JDK 内部注解，普通业务使用需要 JVM 支持和参数授权。优先思路：  减少共享写入；  使用 LongAdder；  线程本地累加；  降低写频率；  用基准测试证明问题。5.10 压缩指针参数：-XX:+UseCompressedOops-XX:-UseCompressedOops开启时：  引用通常 4 字节；  可减少对象引用占用；  堆地址需在可压缩范围；  超大堆可能自动关闭。查看：java -Xmx32g -XX:+PrintFlagsFinal -version | grep UseCompressedOops不要硬记阈值，以当前 JDK 实际输出为准。5.11 生产案例案例一：缓存对象过多现象：Old 区持续增长，Full GC 后仍高。jcmd &lt;pid&gt; GC.class_histogram | head -30发现大量 CacheEntry。处理：  设置缓存上限；  使用 TTL；  拆分热点缓存；  迁移集中缓存；  按容量和成本评估。案例二：大对象导致停顿jstat -gcutil &lt;pid&gt; 1000jcmd &lt;pid&gt; GC.heap_info方向：  大 SQL 结果集；  一次性文件读取；  大 JSON 序列化；  批处理包过大；  DTO 字段冗余。处理：分页、流式处理、拆批和限制响应大小。本章小结对象由对象头、实例数据和对齐填充组成。字段大小、引用大小、数组长度和压缩指针会影响对象占用。高频对象要关注分配速率、缓存成本和大对象；生产分析用 JOL、class histogram、MAT 和 retained heap 结合判断。思考题  对象头包含哪些信息？  shallow heap 和 retained heap 有什么区别？  开启压缩指针对对象大小有什么影响？  逃逸分析可能带来哪些优化？  如何定位缓存导致的老年代增长？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。运行时数据区描述 JVM 内存的逻辑划分。堆只是其中一部分，进程 RSS 还包括 Metaspace、线程栈、Code Cache、GC 开销、Direct Memory 和 Native 库。理解这些区域，才能解释堆正常但容器 OOM、线程过多、类元数据溢出和 JIT 代码缓存满。4.1 内存全景JVM Process  |-- Heap  |   |-- Young  |   +-- Old / Regions  |-- Metaspace  |-- Thread Stacks  |-- Code Cache  |-- GC Structures  |-- Direct Memory  |-- Native Libraries+-- OS overhead粗略关系：容器或 systemd 内存上限  &gt;= 堆 + Metaspace + 线程栈 + Direct Memory + Code Cache + 其他 native不同 GC 和 JDK 版本的 native 开销不同，必须通过实测和指标确认。4.2 堆查看：jps -ljstat -gc &lt;pid&gt; 1000jstat -gcutil &lt;pid&gt; 1000jcmd &lt;pid&gt; VM.flags示例参数：java -Xms2g -Xmx2g -XX:+UseG1GC -jar app.jar分代视角：Heap  |-- Eden  |-- Survivor 0  |-- Survivor 1  +-- OldG1、ZGC、Shenandoah 将堆划分为 Region 或支持不同布局，逻辑分代视角不能完全等价于物理布局。常见 OOM：java.lang.OutOfMemoryError: Java heap space原因：  堆过小；  内存泄漏；  大查询；  批量加载；  缓存无上限；  流量突增；  GC 配置不匹配。4.3 MetaspaceMetaspace 存储类元数据，位于堆外，默认可伸缩。参数：-XX:MetaspaceSize=256m-XX:MaxMetaspaceSize=512m查看：jstat -gcmetacapacity &lt;pid&gt;jcmd &lt;pid&gt; VM.metaspace增长来源：  加载类过多；  动态代理；  脚本引擎；  反射生成类；  热部署 ClassLoader 泄漏；  Agent 或 AOP 增强。OOM：java.lang.OutOfMemoryError: Metaspace分析：jcmd &lt;pid&gt; GC.class_histogramjcmd &lt;pid&gt; VM.classloader_statsjcmd &lt;pid&gt; VM.classloaders4.4 虚拟机栈每个线程创建时分配栈空间。参数：-Xss512k栈帧：Stack Frame  |-- Local Variable Table  |-- Operand Stack  +-- Frame DataStackOverflowError：public class StackOverflowDemo {    public static void main(String[] args) {        recurse(0);    }    private static void recurse(int depth) {        recurse(depth + 1);    }}常见原因：  无终止递归；  递归深度过大；  框架调用链深；  JSON 深层嵌套；  栈太小。线程数过多：线程数 * -Xss 大约等于线程栈总量它不是唯一因素，但线程失控会同时消耗 CPU、内存和调度资源。4.5 程序计数器每个线程都有独立的 PC Register，记录当前字节码执行位置。线程切换后恢复执行依赖它。特点：  线程私有；  占用很小；  执行 native 方法时值可能为 undefined；  通常不是排障重点。4.6 本地方法栈本地方法栈服务 native 方法。HotSpot 中通常与虚拟机栈实现合并，具体由虚拟机实现决定。相关风险：  JNI 泄漏；  native 库崩溃；  堆外内存不足；  UnsatisfiedLinkError；  压缩或加密库版本不匹配。排查 native 问题时需要 core dump、native 栈和对应库符号。4.7 直接内存Direct Memory 常用于 NIO Buffer、网络框架、压缩和数据库驱动。参数：-XX:MaxDirectMemorySize=1g示例：ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024 * 1024);System.out.println(buffer.capacity());查看：jcmd &lt;pid&gt; VM.flagsjcmd &lt;pid&gt; VM.native_memory summaryVM.native_memory 需要启用 NativeMemoryTracking：java -XX:NativeMemoryTracking=summary -jar app.jarOOM：java.lang.OutOfMemoryError: Direct buffer memory方向：  Netty 池化配置；  直接内存上限；  Buffer 释放；  容器总内存；  泄漏检测。4.8 Code CacheCode Cache 存储 JIT 编译后的本地代码。参数：-XX:ReservedCodeCacheSize=256m查看：jcmd &lt;pid&gt; Compiler.codecachejstat -printcompilation &lt;pid&gt;满了之后可能：  退回解释执行；  性能下降；  出现 CodeCache is full 相关信息。常见来源：  加载类过多；  动态代理；  大量生成代码；  Code Cache 配置过小。4.9 运行时内存查看JCMD：jcmd &lt;pid&gt; VM.uptimejcmd &lt;pid&gt; GC.heap_infojcmd &lt;pid&gt; VM.flagsjcmd &lt;pid&gt; VM.metaspacejcmd &lt;pid&gt; VM.native_memory summaryJSTAT：jstat -gc &lt;pid&gt; 1000jstat -gcutil &lt;pid&gt; 1000jstat -gccapacity &lt;pid&gt;jstat -gcmetacapacity &lt;pid&gt;操作系统视角：ps -o pid,rss,vsz,cmd -p &lt;pid&gt;cat /proc/&lt;pid&gt;/smaps_rolluppmap -x &lt;pid&gt;4.10 内存分配预算示例：容器 limit 3Gi。Heap              1.8GMetaspace         256MThread stacks     300 * 1M = 300MDirect memory     256MCode cache        128MGC/native 余量     200M 左右建议：  Java 17 / 21 使用 MaxRAMPercentage；  预留堆外和系统内存；  限制线程数；  监控 RSS；  观察容器 OOMKilled；  不要把 -Xmx 设为容器 limit。示例：java -XX:MaxRAMPercentage=65.0 -jar app.jar4.11 生产排障堆正常但容器被杀kubectl describe pod &lt;pod&gt;cat /sys/fs/cgroup/&lt;path&gt;/memory.eventsjcmd &lt;pid&gt; VM.native_memory summary方向：Direct Memory、线程栈、Metaspace、native 库、RSS 与 limit。线程数持续增长jstack &lt;pid&gt; | grep '^"' | wc -ljcmd &lt;pid&gt; Thread.print | grep '^"' | wc -lps -T -p &lt;pid&gt; | wc -l检查线程池无界队列、重复创建执行器和资源未关闭。Metaspace 持续增长jcmd &lt;pid&gt; GC.class_histogram | head -50jcmd &lt;pid&gt; VM.classloader_stats关注动态生成类和旧 ClassLoader。本章小结运行时数据区包括堆、Metaspace、虚拟机栈、本地方法栈、程序计数器，以及 Code Cache、Direct Memory 等 native 区域。-Xmx 只是堆上限，进程内存必须按完整预算设计。生产排障要同时看 JVM 内部指标和操作系统 RSS / cgroup。思考题  JVM 进程内存为什么大于 -Xmx？  Metaspace 增长常见原因有哪些？  StackOverflowError 和 OutOfMemoryError 有什么区别？  Direct Memory 常用于哪些组件？  容器 limit 4Gi 时，如何设置堆和堆外预算？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。类加载决定类从哪里来、何时初始化、由谁隔离、初始化执行什么。生产中常见的 NoClassDefFoundError、ClassNotFoundException、LinkageError、同名类冲突、热部署失败和 Agent 干扰，都要回到类加载机制分析。3.1 类生命周期Loading 加载  -&gt; Verification 验证     -&gt; Preparation 准备        -&gt; Resolution 解析           -&gt; Initialization 初始化              -&gt; Using 使用                 -&gt; Unloading 卸载各阶段职责：            阶段      职责                  Loading      读取字节流，生成运行时表示              Verification      校验格式、语义和安全              Preparation      静态字段分配内存并设零值              Resolution      常量池符号引用转直接引用              Initialization      执行 &lt;clinit&gt;      验证、准备、解析常合称为 Linking。3.2 类初始化public class Config {    public static final int VERSION = 1;    public static int counter = initCounter();    static {        System.out.println("Config static block");    }    private static int initCounter() {        return 10;    }}准备阶段：counter = 0初始化阶段：counter = initCounter()执行静态代码块触发初始化的常见场景：  new 对象；  访问非 final 静态字段；  调用静态方法；  反射调用；  初始化子类时先初始化父类；  main 所在类。不触发初始化的示例：System.out.println(Config.VERSION);编译期常量会在使用处内联，具体行为以编译结果和规范为准。3.3 类加载器层次Java 9 之后：Bootstrap ClassLoader  -&gt; Platform ClassLoader     -&gt; Application ClassLoader        -&gt; Custom ClassLoader查看：System.out.println(String.class.getClassLoader());System.out.println(java.sql.Connection.class.getClassLoader());System.out.println(App.class.getClassLoader());            加载器      加载内容                  Bootstrap      核心类库              Platform      平台模块              Application      classpath / modulepath 应用类              Custom      业务隔离、热部署、插件      Bootstrap 在 Java 9 后可能显示为 null 或由内部实现表示，不应依赖输出字符串判断。3.4 双亲委派流程：Custom Loader  -&gt; Application     -&gt; Platform        -&gt; Bootstrap           -&gt; 找不到再逐层向下价值：  核心类库不被随意替换；  避免重复加载；  保证类型一致性；  提升安全性。破坏场景：  Web 容器隔离应用；  SPI 需要父加载器访问子实现；  OSGi 模块化；  热部署；  测试 Mock；  Java Agent 增强。破坏双亲委派不是坏设计，但必须明确隔离边界和生命周期。3.5 SPI 与线程上下文类加载器JDBC 示例：DriverManager.getConnection(    "jdbc:postgresql://db:5432/app",    "app",    "password");服务加载：ServiceLoader&lt;Driver&gt; drivers = ServiceLoader.load(Driver.class);for (Driver driver : drivers) {    System.out.println(driver.getClass().getName());}问题：核心模块中的 DriverManager 需要加载应用 classpath 的驱动。解决思路：使用线程上下文类加载器。查看：Thread.currentThread().getContextClassLoader()线程池会复用线程，任务中修改上下文类加载器必须谨慎。3.6 常见异常            异常      含义                  ClassNotFoundException      显式加载时找不到类              NoClassDefFoundError      编译时存在，运行时链接失败              LinkageError      链接一致性失败              ClassCastException      类型转换失败              UnsatisfiedLinkError      native 库找不到或不匹配      排查：jar tf app.jar | grep 'com/example/Missing.class'find ~/.m2/repository -name '*.jar' | xargs grep -l 'Missing.class'排查方向：  jar 是否打包；  classpath 顺序；  版本冲突；  类加载器隔离；  初始化失败后的二次访问；  模块系统 exports；  动态代理生成失败。3.7 类冲突诊断查看类来源：Class&lt;?&gt; clazz = Class.forName("com.example.Foo");System.out.println(clazz.getProtectionDomain().getCodeSource().getLocation());Arthas：sc -d com.example.FooMaven 依赖树：mvn dependency:tree -Dincludes=com.fasterxml.jackson.coreGradle：gradle dependencies --configuration runtimeClasspath常见冲突：            库      风险                  Jackson      版本不匹配导致序列化失败              Guava      内部 API 变化              Log4j / Logback      绑定冲突              Netty      多版本共存              ASM      字节码版本不支持新 class      3.8 自定义类加载器public class DirectoryClassLoader extends ClassLoader {    private final Path base;    public DirectoryClassLoader(Path base, ClassLoader parent) {        super(parent);        this.base = base;    }    @Override    protected Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {        try {            byte[] bytes = Files.readAllBytes(base.resolve(name.replace('.', '/') + ".class"));            return defineClass(name, bytes, 0, bytes.length);        } catch (IOException e) {            throw new ClassNotFoundException(name, e);        }    }}使用：ClassLoader loader = new DirectoryClassLoader(        Path.of("/tmp/classes"),        Thread.currentThread().getContextClassLoader());Class&lt;?&gt; clazz = Class.forName("com.example.Plugin", true, loader);Object plugin = clazz.getConstructor().newInstance();注意：  类名和字节码一致；  defineClass 只能调用一次；  包权限和签名；  parent 选择；  资源加载；  卸载条件。3.9 热加载与卸载类卸载条件较苛刻：1. 该 Class 的所有实例已回收2. 加载它的 ClassLoader 已回收3. Class 对象没有其他引用热部署常见问题：  旧类实例仍存在；  静态状态不重置；  类加载器泄漏；  Metaspace 增长；  框架缓存旧类；  线程持有旧 ClassLoader。开发环境热替换与生产全量发布是不同问题，不要混用。3.10 Java 模块系统Java 9 引入 JPMS。module-info.java：module com.example.order {    requires java.sql;    requires com.fasterxml.jackson.databind;    exports com.example.order.api;    opens com.example.order.model to com.fasterxml.jackson.databind;}常见错误：            错误      原因                  package is not visible      未 exports              module does not read package      未 requires              reflection denied      未 opens      Spring Boot 应用通常以非模块化路径运行，但依赖库可能带有 module-info，具体由启动方式和模块系统决定。本章小结类加载分为加载、链接和初始化，双亲委派保证核心类库一致，SPI 和容器隔离需要打破默认委派。排查类问题要确认类来自哪个 jar、由哪个加载器加载、是否版本冲突、是否初始化失败。自定义加载器和热部署要特别关注 Metaspace 和 ClassLoader 泄漏。思考题  准备阶段和初始化阶段分别做什么？  双亲委派解决什么问题？  ClassNotFoundException 和 NoClassDefFoundError 有什么区别？  如何定位同名类来自哪个 jar？  为什么热部署可能造成 Metaspace 增长？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。字节码是 Java 源码和 JVM 之间的中间表示。看懂字节码，能解释 i++ 与 ++i 的执行顺序、字符串拼接的真正实现、try-finally 的复制、switch 的跳转表、泛型擦除和异常表。2.1 编译与查看示例源码：public class Calculator {    public static int add(int a, int b) {        int sum = a + b;        return sum;    }}编译：javac Calculator.java查看字节码：javap -c Calculator输出示例：public static int add(int, int);  Code:     0: iload_0     1: iload_1     2: iadd     3: istore_2     4: iload_2     5: ireturn解释：            指令      含义                  iload_0      加载第 1 个 int 参数              iload_1      加载第 2 个 int 参数              iadd      int 相加              istore_2      存入第 3 个局部变量槽              iload_2      读取局部变量              ireturn      返回 int      2.2 class 文件结构简化结构：Magic Number  -&gt; Version     -&gt; Constant Pool        -&gt; Access Flags           -&gt; This Class / Super Class              -&gt; Interfaces                 -&gt; Fields                    -&gt; Methods                       -&gt; Attributes查看常量池：javap -v Calculator版本对应示例：            major version      Java 版本                  61      Java 17              65      Java 21      常见异常：UnsupportedClassVersionError表示运行时 JDK 版本低于 class 编译版本。生产要统一编译、构建和运行版本。2.3 操作数栈JVM 指令大多基于操作数栈执行。iload_0  -&gt; stack: aiload_1  -&gt; stack: a, biadd  -&gt; stack: a + b栈帧结构：Stack Frame  |-- Local Variable Table  |-- Operand Stack  +-- Dynamic Linking / Return Address示例：int result = (a + b) * 2;字节码流程：iload_0iload_1iaddiconst_2imulistore_32.4 i++ 与 ++ipublic class Increment {    public static int post(int i) {        return i++;    }    public static int pre(int i) {        return ++i;    }}查看：javap -c Incrementpost 核心逻辑：iload_0      读取 iiinc 0, 1    局部变量 i 加 1ireturn      返回旧值pre 核心逻辑：iinc 0, 1    局部变量 i 加 1iload_0      读取新值ireturn这解释了为什么两者作为独立语句时效果相同，作为表达式时结果不同。2.5 字符串拼接Java 9 之后，javac 通常使用 StringConcatFactory 生成拼接调用，而不是直接生成 StringBuilder.append 链。Java 8 常见形式：String result = a + b;等价逻辑：String result = new StringBuilder()        .append(a)        .append(b)        .toString();现代 JDK：javap -c StringConcat可能看到：invokedynamic #x  // InvokeDynamic #0:makeConcatWithConstants结论：  循环中显式使用 StringBuilder 仍更清晰；  不必假设编译器永远生成同一种拼接方式；  高频路径应做基准测试；  日志参数建议惰性求值。2.6 条件与循环public static String grade(int score) {    if (score &gt;= 90) {        return "A";    } else if (score &gt;= 60) {        return "B";    }    return "C";}控制指令：            指令      含义                  if_icmpge      int 比较              ifge      与 0 比较              goto      无条件跳转              tableswitch      连续 case 跳转表              lookupswitch      稀疏 case 查找表      循环：for (int i = 0; i &lt; 10; i++) {    sum += i;}字节码结构：初始化  -&gt; 条件检查     -&gt; 循环体        -&gt; 更新变量           -&gt; goto 条件检查2.7 方法调用指令            指令      场景                  invokestatic      静态方法              invokespecial      构造器、private、super 调用              invokevirtual      实例虚方法              invokeinterface      接口方法              invokedynamic      动态调用点，lambda、字符串拼接等      示例：public class CallSite {    public static void main(String[] args) {        Runnable task = CallSite::hello;        task.run();    }    private static void hello() {        System.out.println("hello");    }}Lambda 通常通过 invokedynamic 延迟生成调用站点，具体实现与 JDK 版本有关。2.8 异常表public static int parse(String value) {    try {        return Integer.parseInt(value);    } catch (NumberFormatException e) {        return 0;    } finally {        System.out.println("done");    }}查看：javap -c -v ExceptionDemo异常表字段：            字段      含义                  from      try 起点              to      try 终点              target      handler 位置              type      异常类型      finally 通常会复制到多个退出路径。这也是复杂 finally 和 return 交互容易混淆的原因。2.9 泛型与装箱类型擦除：List&lt;String&gt; names = new ArrayList&lt;&gt;();names.add("Java");String value = names.get(0);字节码里常见的是：List -&gt; 原始类型checkcast String -&gt; 读取时转换装箱：Integer boxed = 10;int primitive = boxed;对应：            操作      方法                  int -&gt; Integer      Integer.valueOf              Integer -&gt; int      Integer.intValue      高频循环中装箱拆箱会带来额外对象和 CPU 开销。2.10 Record 与模式匹配Java Record：public record Point(int x, int y) {}查看生成方法：javap -p Point常见生成内容：  全参构造器；  每个组件的访问器；  equals；  hashCode；  toString。模式匹配编译后仍是类型检查和转换，具体优化随 JDK 版本变化。2.11 ASM 与字节码增强常见场景：            场景      工具                  AOP      ASM、ByteBuddy              Agent      java agent              Mock      Mockito              ORM 懒加载      动态代理              微服务链路      字节码增强      ByteBuddy 示例：Class&lt;?&gt; type = new ByteBuddy()        .subclass(Object.class)        .method(ElementMatchers.isToString())        .intercept(FixedValue.value("dynamic"))        .make()        .load(getClass().getClassLoader(),              ClassLoadingStrategy.Default.WRAPPER)        .getLoaded();字节码增强要关注：  类加载器隔离；  性能开销；  安全风险；  与 JIT 优化交互；  出错时能否关闭。本章小结字节码把 Java 源码翻译成 JVM 可执行的栈式指令。通过 javap 可以看到局部变量表、操作数栈、常量池、方法调用和异常表。很多“语法糖”的行为只有在字节码层才清楚。生产诊断中，字节码用于验证编译结果、分析增强框架和理解底层执行成本。思考题  i++ 和 ++i 的字节码有什么差异？  为什么循环中不建议反复创建拼接对象？  invokevirtual 和 invokeinterface 有什么区别？  finally 在字节码中如何实现？  如何用 javap 确认一段代码是否发生装箱？</li>
  <li>这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。JVM（Java Virtual Machine）是 Java 程序运行的虚拟机。它屏蔽底层操作系统差异，负责加载字节码、管理内存、执行指令、优化热点代码、调度线程和执行垃圾回收。对生产系统来说，JVM 不是黑盒。理解 JVM 后，你才能回答：内存去了哪里、GC 为什么停顿、CPU 为什么高、线程为什么阻塞、容器为什么 OOM、应用为什么启动慢。1.1 Java 程序如何运行Hello.java  -&gt; javac     -&gt; Hello.class        -&gt; ClassLoader           -&gt; Runtime Data Areas              -&gt; Interpreter / JIT                 -&gt; Native Execution示例：public class Hello {    public static void main(String[] args) {        int total = 0;        for (int i = 0; i &lt; 100; i++) {            total += i;        }        System.out.println(total);    }}编译和运行：javac Hello.javajava Hello查看字节码：javap -c HelloJVM 的核心职责：  加载 class 文件；  校验字节码；  分配对象内存；  执行字节码；  JIT 优化热点代码；  回收不再使用的对象；  管理线程和锁；  提供诊断工具接口。1.2 HotSpot JVMJava 虚拟机规范定义了行为，不同厂商有不同实现。常见 JVM：            JVM      特点                  HotSpot      OpenJDK / Oracle JDK 主流实现              OpenJ9      IBM 贡献，注重启动和内存              GraalVM      支持多语言和 AOT              Zulu / Corretto / Temurin      OpenJDK 发行版      本书以 HotSpot 为主线。OpenJDK 发行版选择建议：  优先选择当前团队已验证的 LTS；  Java 17 / 21 是现代微服务常见选择；  新版本 GC、容器支持和语言特性更有优势；  升级前必须压测；  统一 JDK 供应商和版本。1.3 运行时数据区JVM 内存可以粗略分为线程共享区和线程私有区。JVM Runtime  |-- Heap                     线程共享  |   |-- Young Generation  |   |   |-- Eden  |   |   |-- Survivor 0|   |   +-- Survivor 1  |   +-- Old Generation  |-- Method Area / Metaspace 线程共享  |-- Thread Stacks           线程私有  |-- PC Registers            线程私有  +-- Native Method Stacks    线程私有            区域      作用      常见错误                  Heap      对象实例      OutOfMemoryError: Java heap space              Metaspace      类元数据      Metaspace OOM              Stack      栈帧、局部变量      StackOverflowError              Direct Memory      堆外内存      Direct buffer memory OOM              Code Cache      JIT 编译代码      CodeCache is full      注意：堆不是 JVM 进程的全部内存。进程 RSS 还包括 Metaspace、线程栈、Code Cache、GC 结构、Direct Memory、Native 库和操作系统开销。1.4 类加载初步Java 类加载过程：Loading  -&gt; Linking     |-- Verification     |-- Preparation     +-- Resolution  -&gt; Initialization类加载器层次：Bootstrap ClassLoader  -&gt; Platform ClassLoader     -&gt; Application ClassLoader        -&gt; Custom ClassLoader双亲委派的核心思想：  先委托父加载器；  父加载器无法加载再自己加载；  保证核心类库一致和安全；  避免重复加载；  某些场景需要打破，例如热部署和隔离。1.5 对象创建普通对象创建大致流程：类加载检查  -&gt; 分配内存  -&gt; 初始化零值  -&gt; 设置对象头  -&gt; 执行 &lt;init&gt;对象内存布局：            区域      内容                  Object Header      Mark Word、类型指针、数组长度              Instance Data      字段值              Padding      对齐填充      分配方式：  栈上分配，逃逸分析通过时可能优化；  TLAB 分配，线程本地分配缓冲；  Eden 分配；  大对象直接进入老年代或特殊区域。1.6 执行引擎JVM 有解释器和 JIT 编译器。            方式      特点                  解释执行      启动快，单次执行慢              C1 Client 编译      编译快，优化中等              C2 Server 编译      编译慢，优化强              分层编译      常见默认，结合两者优点      JIT 优化包括：  方法内联；  逃逸分析；  锁消除；  循环优化；  去虚拟化；  公共子表达式消除。这解释了为什么 Java 服务需要预热、为什么压测要从低流量逐步加压、为什么微小 benchmark 容易失真。1.7 GC 初步垃圾回收的核心问题：  哪些对象可以回收；  什么时候回收；  如何回收；  回收时应用是否停止；  停顿多长；  吞吐和延迟如何权衡。常见收集器：            收集器      特点                  Serial      单线程，适合小内存              Parallel      吞吐优先              CMS      老年代并发，Java 14 移除              G1      区域化、可预测停顿，常见默认              ZGC      低延迟大堆              Shenandoah      低延迟并发收集      选择建议：  默认 G1 是稳妥起点；  大堆低延迟可评估 ZGC；  吞吐型批处理可用 Parallel；  新版本能力要结合实际 JDK 支持；  不要凭感觉频繁切换 GC。1.8 线程与 Java 内存模型JVM 线程映射到操作系统线程。查看 Java 线程：jstack PIDjcmd PID Thread.printJava 内存模型（JMM）定义共享变量的可见性、有序性和原子性规则。核心概念：            概念      说明                  happens-before      操作可见性规则              volatile      可见性和禁止重排              synchronized      互斥和内存语义              CAS      原子比较交换              final      安全发布语义      并发 bug 往往不是必现，而是依赖时序、缓存和编译器优化。1.9 生产 JVM 诊断工具常用命令：java -versionjps -ljstat -gcutil PID 1000jmap -histo PIDjcmd PID VM.flagsjcmd PID GC.class_histogramjstack PIDjcmd PID JFR.start duration=120s filename=app.jfr外部工具：            工具      用途                  Arthas      在线诊断、watch、trace、火焰图              JFR + JMC      低开销飞行记录              MAT      堆 dump 分析              VisualVM      图形化观察              async-profiler      CPU / alloc 火焰图      本章小结JVM 负责加载字节码、管理内存、执行和优化代码、调度线程并回收对象。理解运行时数据区、类加载、执行引擎、GC 和线程模型，是诊断生产问题的基础。JVM 内存不只等于堆，容器 OOM 也可能来自堆外内存或进程限制。思考题  Java 源码到执行需要经历哪些阶段？  JVM 进程内存为什么大于 -Xmx？  分层编译为什么能兼顾启动性能和峰值性能？  G1、ZGC、Parallel 各适合什么场景？  如果线上服务 CPU 100%，你会按什么顺序排查？</li>
</ul>
