类加载与字节码
- 类加载与字节码
- 类加载机制
- 类的生命周期
- 流程
- Loading(加载)
- 把 .class 的字节流变成 JVM 内部的 Class 对象
- 验证
- 确保字节码符合 JVM 规范、类型安全、不会破坏虚拟机
- 准备
- 为类变量 static 字段分配内存并设置默认零值
- 解析
- 把常量池中的符号引用(字符串/描述符)转换成直接引用(指针/句柄)
- 初始化
- 执行类初始化方法
- 执行类初始化方法
- 准备阶段:有 static 变量的空间+默认值,初始化阶段执行你的 static 赋值逻辑
- Loading(加载)
- 准备阶段 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)
- 过程:标记可达->把存活对象向一侧移动->整理出连续空闲区
- 优点:无碎片
- 缺点:移动对象成本高,停顿更长
- 复制
- 过程:把存活对象复制到另一块区域,清空原区域
- 优点:分配快、碎片少
- 缺点:需要额外空间
- 标记-清除(Mark-Sweep)
- 分代为什么有效
- 核心假设:大多数对象朝生夕死,少数对象存活很久
- 堆分为
- 新生代(Young):频繁回收,复制算法为主
- 老年代(Old):存活对象多,回收频率低,整理/并发标记策略为主
- STW 从哪里来
- STW 的根源
- 根枚举/安全点:要一致地拿到 GC Roots,需要让线程在安全点停下
- 对象移动/引用更新:复制/整理需要更新指针,必须在一致视图下完成
- STW 的根源
- 并发标记时代的写屏障/记忆集
- 当 GC 和应用线程并发时,会出现对象引用关系变化的一致性问题。需要
- 写屏障:在写入引用字段时执行一小段额外逻辑
- Card Table/Remembered Set (RSet):记录跨代引用,避免全堆扫描
- 为避免扫描整个老年代/全堆,新生代回收时需要知道老年代指向新生代的引用;并发标记需要处理引用更新,因此用写屏障把变更记录到卡表/RSet,保证正确性与效率
- 当 GC 和应用线程并发时,会出现对象引用关系变化的一致性问题。需要
- GC 要解决的问题
- 收集器体系
- G1 (Garbage First)
- 设计目标
- 在吞吐与低停顿之间折中,提供可预测停顿
- 关键机制
- Region 化:堆切成很多等大小 Region
- 按收益优先回收:优先回收“垃圾最多、收益最高“的 Region
- RSet:每个 Region 维护“谁引用我“的集合,避免全堆扫描
- 并发标记:找出老年代可回收对象,随后 Mixed GC 回收部分 old Regions
- 设计目标
- G1 的堆结构
- Region 是物理切片;在不同时间扮演不同角色
- Eden Region
- Survivor Region
- Old Region
- Humongous Region (大对象)
- G1 不是固定的新生代/老年代,而是逻辑分代
- Region 是物理切片;在不同时间扮演不同角色
- 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 时间长
- 会做全堆标记整理
- Full GC 是兜底
- Young GC(仅回收 Young)
- 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=
:老年代占用多少开始并发标记
- 4 个参数
- G1 (Garbage First)
- 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 并行化
- CMS 是什么
- 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
- 恢复引用线程
- STW(停顿)
- 对象如何从 Young 晋升到 Old
- 年龄阈值:对象每熬过一次 YoungGC 年龄 +1,达到阈值 -XX:MaxTenuringThreshold 晋升
- Survivor 放不下:Survivor 容量不足,直接晋升到 old
- 大对象直接进老年代:如果开启/配置了相关策略,大对象可能绕过 Young
- ZGC
- 目标:极低停顿
- 核心:并发标记+并发重定位,依赖读写屏障等机制
- 适合:大堆、强延迟敏感
- GC 基础原理
JIT 与性能
- JIT 与性能
- 解释器 vs JIT
- 解释器:启动快、编译开销低,但是每次执行都要逐条解释代码,长期运行慢
- JIT 编译器:把热点方法编译成机器码,长期运行快,但是编译有成本
- 热点探测
- HotSpot 用计数器发现热点
- 方法调用计数器:方法被多次调用 -> 变热
- 会变计数器:循环回跳次数多->循环热点
- 分层编译
- 解释执行(Tier 0):冷启动,收集部分 profile
- C1(Client Compiler,Tier 1-3):编译快,优化较轻,适合尽快提升性能并继续收集 profile
- C2(Server Compiler,Tier 4):编译慢,优化重,最终追求极致性能
- 解释器 vs JIT
线上排障与工具链
- 线上排障与工具链
- 保存现场
- 采集 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,再分析引用链
- 采集 4 件套
- 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 编译情况
- jstat -gcutil
- 用户
- 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 火焰图确认热点
- OS 层确认
- RT 抖动/毛刺
- 目标:分辨是 STW、锁阻塞、还是 IO/下游慢
- 标准流程
- 先看是否有 STW
- GC 日志看 pause
- jstat -gcutil 看 GC 是否在尖峰时频繁
- 非 GC,查看线程状态分布
- jstack 多次采样
- BLOCKED 多:锁竞争
- WAITING/TIME_WAITING 多:可能在等 IO/队列/park
- 下游排查
- DB 慢查询、线程池队列积压,连接池耗尽
- 先看是否有 STW
- 频繁 Full GC
- 目标:解释为什么 Full 以及回收效果如何
- 标准流程:
- 看 GC 日志:Full GC 的触发原因
- jstat -gcutil 看
- Old 是否增长
- FGC 是否增长
- YGC 后 Old 增长是否很快
- 先做直方图
- jcmd GC.class_histogram 看大对象/大集合
- 必要时 heap dump:
- MAT 看 Dominator Tree + Path to Roots
- CPU 100%
- 保存现场