JVM 学习二月 19, 2026类加载与字节码# - 类加载与字节码 - 类加载机制 - 类的生命周期 - 流程 - Loading(加载) - 把 .class 的字节流变成 JVM 内部的 Class 对象 - 验证 - 确保字节码符合 JVM 规范、类型安全、不会破坏虚拟机 - 准备 - 为类变量 static 字段分配内存并设置默认零值 - 解析 - 把常量池中的符号引用(字符串/描述符)转换成直接引用(指针/句柄) - 初始化 - 执行类初始化方法 <clinit> - 准备阶段:有 static 变量的空间+默认值,初始化阶段执行你的 static 赋值逻辑 - 准备阶段 vs 初始化阶段 - 默认值发生在准备阶段 -  - 显式赋值发生在初始化阶段 -  - 常量折叠 - <clinit> 类初始化方法 - <clinit> 是编译器合成的类初始化方法 - 来自于所有 static 字段的显式赋值语句和所有 static{} 块 - 按源码出现顺序合并执行 - JVM 保证同一个类的 <clinit> 在多线程下只会被一个线程执行,其他线程阻塞等待 - 初始化触发时机 - 初始化只在主动使用时触发 - 常用触发初始化场景 - new A() - 读取写入 A.staticField - 调用 A.staticMethod() - Class.forName("A") - 初始化子类前,先初始化父类 - JVM 启动时执行 main 类 - Class.forName 和 ClassLoader.loadClass - Class.forName("A"):默认加载+连接+初始化 - ClassLoader.loadClass("A"),默认只做加载和必要时连接,不初始化 - 类加载体系与双亲委派 - 三类内置类加载器 - Bootstrap:加载核心库(rt/基础模块),通常由 JVM 实现,不是 Java 对象 - Platform(JDK9+)/Ext(JDK8):平台扩展库 - App:应用 classpath 下的类(写的业务代码) - 双亲委派模型 - 流程 - loadClass(name) 先检查本加载器是否已经加载 - 没加载则委派给父加载器 - 父加载器加载不到,再由当前加载器尝试 findClass 加载 - 最终返回 Class<?> - 动机 - 安全:防止核心类被伪造替换 - 一致性:保证同名类在 JVM 中尽量只加载一份,避免类型混乱 - 破坏双亲委派 - 破坏方式 - 自定义 ClassLoader,重写 loadClass,不再先委派 - 使用线程上下文类加载器绕过父类限制 - 典型场景 - SPI/ServierLoader:核心库需要加载应用层实现(反向委派) - JDBC Driver:早期通过 Class.forName 强制注册类驱动 - 应用容器(Tomcat):每个 WebApp 一个 ClassLoader,实现隔离与热部署 - 破坏带来的风险 - 类重复加载 - 内存泄露:容器热部署时,类加载器无法回收->元空间泄露 - 类相等判断 - JVM 中,一个类的唯一性由(类的全限定名)+(加载它的 ClassLoader)共同决定 JVM 内存结构与对象模型# - JVM 内存结构与对象模型 - 运行时数据区 - 整体结构 - 线程私有 - 程序计数器 - Java 虚拟机栈 - 本地方法栈 - 线程共享 - 堆 - 方法区(Hotspot 对应元空间 + 运行时常量池) - 程序计数器 - 存放线程正在执行的字节码指令地址 - 线程私有:线程切换后能恢复执行位置 - 几乎不会 OOM - 如果执行 Native 方法,PC 值不一定有意义 - Java 虚拟机栈 - 栈帧结构:一次方法调用对应一个栈帧 - 局部变量表:基本类型值、对象引用、returnAddress - 操作数栈:字节码执行的临时计算区 - 动态链接:指向运行时常量池的方法/字段引用信息 - 方法返回地址 - 异常表 - 栈中存的对象 - 栈里一般不存对象本体,存的是对象引用 - 对象本体在堆上 - 栈相关错误 - StackOverflowError:递归太深/栈帧太大 - OutOfMemoryError:unable to create new native thread:系统资源耗尽 - 常见参数 - -Xss:每个线程的栈大小(越大单线程可递归更深,但是可创建线程数更少) - 本地方法栈 - 执行 JNI/Native 方法用 - 堆 - 存放几乎所有对象实例 + 数组 - 常见结构 - 新生代:Eden + Survivor(S0/S1) - 老年代 - G1 下是 Region 化,不强行分固定代区 - 常用参数 - -Xms 初始堆 - -Xmx 最大堆 - -XX:NewRatio/-XX:SurvivorRatio (传统分代) - -XX:+UseG1GC 选择收集器 - 堆相关 OOM - OutOfMemoryError: Java heap space 最常见 - OutOfMemoryError: GC overhead limit exceed - 方法区/元空间 - 存放类元数据:类结构信息、字段/方法信息、运行时常量池、方法字节码元信息等 - JDK 8 以后 HotSpot 用 Metaspace (本地内存)替代永久代 PermGen - 常见参数 - -XX:MaxMetaspaceSize 限制元空间上限 - -XX:MetaspaceSize 触发元空间 GC 的阈值 - 典型 OOM - 动态生成大量类 CGLIB、ByteBuddy、JSP 编译、反射代理 - 热部署/自定义类加载器泄露导致旧类加载器无法回收 - 对象创建与内存分配 - 对象创建的典型步骤 - 类检查:该类是否加载、解析、初始化 - 分配内存:在堆上为对象划分出一块连续空间 - 零值初始化:对象实例字段设置为默认值 - 设置对象头:写入 Mark Word,类指针等元信息 - 执行 <init>:构造方法把字段设置为期望的初始值 - 内存分配方式:指针碰撞 vs 空闲列表 - 指针碰撞 - 堆内存规整,已用区和闲置区是连续的,只要把指针往后移 - 优点:速度快 - 空闲列表 - 堆存在碎片,需要维护空闲列表,从中找合适块 - 优点:有碎片(标记-清除类收集器) - 是否能用指针碰撞,取决于 GC 是否会整理/复制使空间规整;不规整举要用空闲列表 - 并发分配:TLAB(Thread Local Allocation Buffer) - TLAB:为每个线程在 Eden 预留一小块私有分配区域,线程在 TLAB 内分配对象时基本无需加锁。 - 原因 - 多线程并发 new 对象频繁,每次都竞争全局堆指针,会有锁/原子操作开销 - TLAB 把大多数分配变成线程本地,提升吞吐 - TLAB 不够时会申请新的 TLAB 或者退到全局分配路径 - 对象内存布局与引用类型 - 对象内存布局 - 对象头 - MarkWord:哈希码、GC 分代年龄、锁信息 - Klass Pointer:指向类元数据(对象属于哪个类) - 实例数据:定义的字段 - 对齐填充:按 8 字节对齐 - 引用类型 - 强引用 - 默认引用:只要强引用存在,对象不回收 - OOM 大户:强引用过长导致对象一直存活 - 软引用 - 内存不足时才回收 - 弱引用 - 下一次 GC 旧可能回收(只要没有强引用) - 典型:WeakHashMap、ThreadLocal 的 key - 虚引用 - 不能通过它获取对象 - 用于对象回收后的通知/资源清理配合 ReferenceQueue - ThreadLocal 为什么会内存泄露 - 关键结构 - 每个 Thread 内部有一个 ThreadLocalMap - ThreadLocalMap 的 Key 是 ThreadLocal 的弱引用 - value 是强引用 - 泄露发生条件 - ThreadLocal 实例没有强引用 - 线程还活着 - ThreadLocalMap 里面出现 key=null,value=大对象的 entry - value 可能长期挂挂着 - 正确姿势 - 用完必须 remove() - 线程池环境更好要 remove GC 体系# - GC 体系 - GC 基础原理 - GC 要解决的问题 - Java 对象生命周期动态、数量大、手动 free 易错 - GC 的核心目标:在可接受停顿和吞吐下,回收不可达对象占用的内存 - 判定垃圾 - 引用计数 - 优点:实现直接 - 缺点:循环引用难处理 - Hotspot 主流不靠它做 GC 判定 - 可达性分析 - 从 GC Roots 出发做图遍历,遍历不到的对象是垃圾 - GC Roots 常见来源 - 虚拟机栈(栈帧局部变量表)引用的对象 - 方法区/元空间中静态字段引用的对象 - 方法区中常量引用的对象 - JNI 引用的对象 - 活跃线程对象,同步锁持有的对象 - 三大经典回收算法 - 标记-清除(Mark-Sweep) - 过程:标记可达 -> 清掉不可达 - 优点:简单 - 缺点:内存碎片 - 标记-整理(Mark-Compact) - 过程:标记可达->把存活对象向一侧移动->整理出连续空闲区 - 优点:无碎片 - 缺点:移动对象成本高,停顿更长 - 复制 - 过程:把存活对象复制到另一块区域,清空原区域 - 优点:分配快、碎片少 - 缺点:需要额外空间 - 分代为什么有效 - 核心假设:大多数对象朝生夕死,少数对象存活很久 - 堆分为 - 新生代(Young):频繁回收,复制算法为主 - 老年代(Old):存活对象多,回收频率低,整理/并发标记策略为主 - STW 从哪里来 - STW 的根源 - 根枚举/安全点:要一致地拿到 GC Roots,需要让线程在安全点停下 - 对象移动/引用更新:复制/整理需要更新指针,必须在一致视图下完成 - 并发标记时代的写屏障/记忆集 - 当 GC 和应用线程并发时,会出现对象引用关系变化的一致性问题。需要 - 写屏障:在写入引用字段时执行一小段额外逻辑 - Card Table/Remembered Set (RSet):记录跨代引用,避免全堆扫描 - 为避免扫描整个老年代/全堆,新生代回收时需要知道老年代指向新生代的引用;并发标记需要处理引用更新,因此用写屏障把变更记录到卡表/RSet,保证正确性与效率 - 收集器体系 - G1 (Garbage First) - 设计目标 - 在吞吐与低停顿之间折中,提供可预测停顿 - 关键机制 - Region 化:堆切成很多等大小 Region - 按收益优先回收:优先回收“垃圾最多、收益最高“的 Region - RSet:每个 Region 维护“谁引用我“的集合,避免全堆扫描 - 并发标记:找出老年代可回收对象,随后 Mixed GC 回收部分 old Regions - G1 的堆结构 - Region 是物理切片;在不同时间扮演不同角色 - Eden Region - Survivor Region - Old Region - Humongous Region (大对象) - G1 不是固定的新生代/老年代,而是逻辑分代 - G1 的回收类型与流程 - Young GC(仅回收 Young) - 触发常见原因:Eden 满 - 步骤 - STW 暂停 - 处理根与 RSet,找出可达对象 - 复制存活对象:Eden/Survivor -> 新的 Survivor/Old (晋升) - 更新引用,释放旧 Eden/Survivor Region - 恢复执行 - Concurrent Mark (并发标记周期) - 找出 Old 区的存活情况,为 Mixed GC 提供候选 Old Regions - 阶段 - Initial Mark:STW,很短,标记 GC Roots 直接可达(通常搭车在一次 Young GC 上) - Concurrent Mark(并发标记):与应用并发,从根开始遍历可达视图 - Remark(再标记):STW,修正并发期间引用变化 - Cleanup(清理/回收候选统计):统计每个 Region 的存活率,为 Mixed 选择回收集合 - Mixed GC (回收 Young + 部分 Old) - 触发条件:完成并发标记后,G1 会在若干 GC 中选择一批 Old Regions 与 Young 一起回收 - 特点 - Mixed 仍然是 STW(复制/整理 + 引用更新) - 部分 Old 由 G1 按收益挑选 - G1 的 Old 回收不是一次性全回收,而是多次 Mixed 逐步回收 - Full GC(G1 不想发生,但会发生) - Full GC 是兜底 - 并发标记跟不上分配速度 - 晋升失败/分配失败 - to-space exhausted(复制目标空间不足) - Humongous 压力导致碎片/可用 Region 不够 - Full GC 特点 - STW 时间长 - 会做全堆标记整理 - Humongous(大对象) - 在 G1 中,大对象通常定义为:大小 > Region 大小的 50% - 表现 - 大对象会直接分配到 Humongous Regions - Humongous 回收不如普通对象灵活,容易造成 - Old 快速膨胀 - Mixed 效果变差 - 频繁并发标记或 Full GC 风险上升 - RSet - 作用:支持只回收一部分 Region 仍然能正确找到 Roots - 代价:维护 RSet 需要写屏障与额外内存/CPU - G1 调优 - 4 个参数 - -XX:+UseG1GC - -XX:MaxGCPauseMillis=<N>: 停顿目标 - -Xms/-Xmx: 堆大小 - -XX:InitiatingHeapOccupancyPercent=<N>:老年代占用多少开始并发标记 - CMS - CMS 是什么 - 是 HotSpot 里老年代并发收集器 - 核心目标是降低老年代回收的停顿时间-让标记尽可能与应用线程并发执行 - Young 通常配 ParNew - CMS 总体结构 - 经典周期 - Initial Mark(初始标记)-STW - Concurrent Mark(并发标记)- 并发 - Remark(重新标记)-STW - Concurrent Sweep (并发清除) - Initial Mark(初始标记,STW) - 从 GC Roots 触发,标记直接可达的对象 - STW:一致性地枚举根,必须在 safepoint 停住线程 - Concurrent Mark(并发标记) - 从初始标记得到的对象开始,遍历对象图,标记所有可达对象 - 与应用线程并发执行 - 问题:应用线程在改引用->可能影响标记正确性 - Remark(重新标记,STW) - 修正并发标记期间因为引用变化导致的“漏标/标记不完整“ - 处理并发期间记录到变更(dirty card 等),把可达闭包补齐) - STW 原因:并发期间引用一直在变,如果不暂停,就无法在一个确定点收束标记结果 - Concurrent Sweep(并发清除) - 回收未标记对象的空间,把它们加入空闲链表 - 与应用线程并发执行 - 设计缺陷 - 内存碎片 - CMS 是 Mark-Sweep,不做压缩整理 - 老年代空闲空间可能变成很多不连续小块 - 后果:触发 Full GC(Serial Old/Parallel Old)来做一次标记-整理压缩 - 浮动垃圾 - CMS 标记是并发进行的 - 并发标记开始后,应用线程还在不断创建新对象,产生新垃圾 - 这些对象可能在本轮标记后才产生,不会被本轮回收 - CMS 回收触发的典型机制 - 不会等老年代完全满才开始 - 老年代使用率达到阈值开始一次 CMS 周期 -XX:CMSInitiatingOccupancyFraction 配合 -XX:+UseCMSInitiatingOccupancyOnly - 如果阈值太高,容易并发失败;阈值太低,吞吐下降 - 常用参数 - -XX:+UseConcMarkSweepGC:启用 CMS - -XX:+UseParNewGC:新生代用 ParNew - -XX:+CMSParallelRemarkEnabled: Remark 并行化 - ParNew 回收 - 作用范围:新生代(Eden+S0+S1) - 算法:标记复制 - 并发:STW,但是 GC 线程并行执行 - 核心流程 - STW(停顿) - 所有应用线程到达 safepoint 暂停,保证 Roots 枚举一致 - 根扫描(Roots) - 扫各线程栈、静态引用等 GC Roots - 额外要处理老年代 -> 新生代的引用 - YoungGC 不能扫描整个老年代,需要靠卡表 Card Table 记录老年代那些卡页变脏,只扫描这些变脏区域找到 Old->Young 的引用作为额外 Roots - 复制存活对象 - Eden 中存活对象复制到 Survivor(或者晋升到 Old) - Survivor(From)中存活对象复制到另一个 Survivor,年龄 + 1 - 复制过程中会设置 forwarding pointer,避免重复复制并更新引用 - 交换 Survivor - Eden 和旧 From Survior 清空 - To Survivor 变成新的 From - 恢复引用线程 - 对象如何从 Young 晋升到 Old - 年龄阈值:对象每熬过一次 YoungGC 年龄 +1,达到阈值 -XX:MaxTenuringThreshold 晋升 - Survivor 放不下:Survivor 容量不足,直接晋升到 old - 大对象直接进老年代:如果开启/配置了相关策略,大对象可能绕过 Young - ZGC - 目标:极低停顿 - 核心:并发标记+并发重定位,依赖读写屏障等机制 - 适合:大堆、强延迟敏感 JIT 与性能# - JIT 与性能 - 解释器 vs JIT - 解释器:启动快、编译开销低,但是每次执行都要逐条解释代码,长期运行慢 - JIT 编译器:把热点方法编译成机器码,长期运行快,但是编译有成本 - 热点探测 - HotSpot 用计数器发现热点 - 方法调用计数器:方法被多次调用 -> 变热 - 会变计数器:循环回跳次数多->循环热点 - 分层编译 - 解释执行(Tier 0):冷启动,收集部分 profile - C1(Client Compiler,Tier 1-3):编译快,优化较轻,适合尽快提升性能并继续收集 profile - C2(Server Compiler,Tier 4):编译慢,优化重,最终追求极致性能 线上排障与工具链# - 线上排障与工具链 - 保存现场 - 采集 4 件套 - GC 日志 - 线程栈 jstack - Heap Dump jcmd GC.heap.dump 或 jmap -dump - 关键进程信息:jcmd VM.flags、VM.command_line - 判断 CPU、内存、锁、I/O - CPU 高:先看线程在干什么 - RT 抖动:优先看 STW + 锁阻塞 + IO 等待 - 内存涨:看堆/元空间/直接内存 + 是否泄露 - Full GC:看触发原因、晋升、存活率 - OOM:先 dump,再分析引用链 - jps:快速找到 Java 进程 - 列出 Java 进程 PID,主类/jar 名称 - jps -l:显示主类全名或 jar - 定位 PID 的第一步,用于后续 jcmd/jstack/jstat - jcmd:万能入口 - jcmd 是 Hotspot 的控制台 - jcmd <pid> VM.version:JDK/Hotspot 版本 - jcmd <pid> VM.command_line: 启动参数 - jcmd <pid> GC.heap_info: 堆概要 - jcmd <pid> GC.class_histogram: 类直方图 - jcmd <pid> GC.heap_dump xx.hprof 生成 heap dump - jcmd <pid> Thread.print: 线程栈 - jcmd <pid> PerfCounter.print: 性能计数器 - jstack: 线程栈、死锁、阻塞、热点定位 - 用途 - CPU 高:找 RUNNABLE 热点线程在跑啥 - RT 抖动:看 BLOCKED/WAITING/TIMED_WAITING - 死锁:Found one Java-level deadlock 直接给出环 - 间隔 5-10s 连续采样 3 次,看热点栈是否稳定 - jstat: 快速查看 GC/类加载/编译统计 - 用户 - 判断 GC 是否频繁、老年代是否持续增长、晋升是否异常 - 看 metaspace/class load 是否异常增长 - 常用命令 - jstat -gcutil <pid> 1s 20: 每秒输出 20 次 GC 利用率 - jstat -gc <pid> 1s 20: 更详细的各区大小/容量 - jstat -class <pid> 1s 20: 类加载数量 - jstat -compiler <pid> 1s 20: JIT 编译情况 - jmap vs jcmd heap_dump: dump 与直方图 - 直方图 - 快速看到哪个类对象最多/占用最大 - 常用于 OOM 前后或内存上涨时的第一眼判断 - heap dump - 确定谁在引用链上把对象留住 - Arthas: 线上动态诊断 - dashboard: CPU/内存/线程/GC 全览 - thread -n 5: 找 CPU 占用最高的线程 - thread <id>: 看指定线程栈 - jvm: JVM 信息、参数、内存、GC、类加载器 - heapdump xx.hprof: 导出 heap dump - trace com.xx.Service method: 方法调用链耗时 - watch com.xx.Service method '{params,returnObj,throwExp}' -x 2: 观察入参/返回/异常 - tt:录制与回放 - 高频场景 - CPU 100% - 目标:找到哪个线程在烧 CPU,以及烧 CPU 的代码路径 - 标准流程: - OS 层确认 - top/htop 看进程 CPU - 定位 java 线程 - Arthas: thread -n 5 - top -H -p <pid> 找到线程 TID - 将 TID 转 16 进制 - printf "%x\n" 12345 - jstack <pid> 或 jcmd Thread.print 中搜索 16 进制 nid - 找到对应线程栈 - 判断栈顶 - 如果是业务循环/正则灾难/大 JSON 解析/排序/加密->代码问题 - 如果是 GC 线程占 CPU -> 看 GC 是否频繁 - 需要更准时,上 profiler - JFR/async-profiler 火焰图确认热点 - RT 抖动/毛刺 - 目标:分辨是 STW、锁阻塞、还是 IO/下游慢 - 标准流程 - 先看是否有 STW - GC 日志看 pause - jstat -gcutil 看 GC 是否在尖峰时频繁 - 非 GC,查看线程状态分布 - jstack 多次采样 - BLOCKED 多:锁竞争 - WAITING/TIME_WAITING 多:可能在等 IO/队列/park - 下游排查 - DB 慢查询、线程池队列积压,连接池耗尽 - 频繁 Full GC - 目标:解释为什么 Full 以及回收效果如何 - 标准流程: - 看 GC 日志:Full GC 的触发原因 - jstat -gcutil 看 - Old 是否增长 - FGC 是否增长 - YGC 后 Old 增长是否很快 - 先做直方图 - jcmd GC.class_histogram 看大对象/大集合 - 必要时 heap dump: - MAT 看 Dominator Tree + Path to Roots