类加载与字节码

  • 类加载与字节码
    • 类加载机制
      • 类的生命周期
        • 流程
          • Loading(加载)
            • 把 .class 的字节流变成 JVM 内部的 Class 对象
          • 验证
            • 确保字节码符合 JVM 规范、类型安全、不会破坏虚拟机
          • 准备
            • 为类变量 static 字段分配内存并设置默认零值
          • 解析
            • 把常量池中的符号引用(字符串/描述符)转换成直接引用(指针/句柄)
          • 初始化
            • 执行类初始化方法
          • 准备阶段:有 static 变量的空间+默认值,初始化阶段执行你的 static 赋值逻辑
        • 准备阶段 vs 初始化阶段
          • 默认值发生在准备阶段
          • 显式赋值发生在初始化阶段
          • 常量折叠
        • 类初始化方法
          • 是编译器合成的类初始化方法
          • 来自于所有 static 字段的显式赋值语句和所有 static{} 块
          • 按源码出现顺序合并执行
          • JVM 保证同一个类的 在多线程下只会被一个线程执行,其他线程阻塞等待
        • 初始化触发时机
          • 初始化只在主动使用时触发
          • 常用触发初始化场景
            • 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,类指针等元信息
        • 执行 :构造方法把字段设置为期望的初始值
      • 内存分配方式:指针碰撞 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=: 停顿目标
          • -Xms/-Xmx: 堆大小
          • -XX:InitiatingHeapOccupancyPercent=:老年代占用多少开始并发标记
    • 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 VM.version:JDK/Hotspot 版本
      • jcmd VM.command_line: 启动参数
      • jcmd GC.heap_info: 堆概要
      • jcmd GC.class_histogram: 类直方图
      • jcmd GC.heap_dump xx.hprof 生成 heap dump
      • jcmd Thread.print: 线程栈
      • jcmd 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 1s 20: 每秒输出 20 次 GC 利用率
        • jstat -gc 1s 20: 更详细的各区大小/容量
        • jstat -class 1s 20: 类加载数量
        • jstat -compiler 1s 20: JIT 编译情况
    • jmap vs jcmd heap_dump: dump 与直方图
      • 直方图
        • 快速看到哪个类对象最多/占用最大
        • 常用于 OOM 前后或内存上涨时的第一眼判断
      • heap dump
        • 确定谁在引用链上把对象留住
    • Arthas: 线上动态诊断
      • dashboard: CPU/内存/线程/GC 全览
      • thread -n 5: 找 CPU 占用最高的线程
      • thread : 看指定线程栈
      • 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 找到线程 TID
          • 将 TID 转 16 进制
            • printf “%x\n” 12345
          • jstack 或 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