JVM 知识总结
目录
1. 内存模型
1.1 运行时数据区(JDK 8)
| 区域 | 线程是否共享 | 作用 |
|---|---|---|
| 程序计数器 | 私有 | 当前线程执行的字节码行号指示器;Native 方法时为空 |
| Java 虚拟机栈 | 私有 | 存储栈帧:局部变量表、操作数栈、动态链接、方法出口;栈深度超限 → StackOverflowError |
| 本地方法栈 | 私有 | 为 Native 方法服务 |
| 堆 | 共享 | 存放对象实例、数组;无法扩展 → OutOfMemoryError |
| 方法区 | 共享 | 类元信息、常量、静态变量、JIT 编译后的代码;JDK 8 用元空间 MetaSpace 实现,使用本地内存 |
要点:
- 堆、方法区为线程共享;程序计数器、虚拟机栈、本地方法栈为线程私有。
- 直接内存(Direct Memory)不属于运行时数据区,但 OOM 时也可能抛出 OutOfMemoryError。
1.2 堆内存分区
- 新生代(Young Generation)
- Eden:新对象优先分配在这里。
- Survivor:两个 Survivor(From / To),复制算法时用于保留存活对象,默认比例 Eden : From : To ≈ 8 : 1 : 1。
- 老年代(Old Generation)
- 存活多次 GC 的对象晋升到老年代;大对象(超过阈值)可能直接进入老年代。
对象创建简要过程:
- 类加载检查 → 2. 分配内存(指针碰撞 / 空闲列表)→ 3. 初始化零值 → 4. 设置对象头 → 5. 执行
<init>。
对象内存布局:
- 对象头:Mark Word(哈希、GC 年龄、锁信息等)、类型指针。
- 实例数据:字段。
- 对齐填充:8 字节对齐。
1.3 方法区与元空间(JDK 8)
- JDK 7:方法区在堆中,用永久代(PermGen)实现。
- JDK 8:永久代移除,用 元空间(Metaspace) 在本地内存实现,存类元信息等,不再占用堆,避免 PermGen OOM,仅受本地内存限制。
面试重点:
- 堆存对象实例;栈存局部变量和栈帧;方法区/元空间存类信息与常量。
- 为什么要有两个 Survivor:复制算法需要一块“保留区”,两个 Survivor 轮换,避免内存碎片,适合新生代大量“朝生夕死”的对象。
2. 垃圾回收
2.1 如何判定对象可回收
- 引用计数:有循环引用问题,Java 未采用。
- 可达性分析:从 GC Roots 出发,不可达则视为可回收。
- GC Roots:栈中引用、静态变量、常量、JNI 引用等。
- 引用类型:强引用、软引用、弱引用、虚引用;只有强引用会阻止回收。
2.2 垃圾回收算法
| 算法 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| 标记-清除 | 标记可回收,再统一清除 | 实现简单 | 产生碎片 |
| 标记-复制 | 存活对象复制到另一块区域 | 无碎片、高效 | 空间折半 |
| 标记-整理 | 标记后存活对象向一端移动 | 无碎片、连续 | 移动有开销 |
- 新生代:多采用标记-复制(对象存活率低)。
- 老年代:多采用标记-清除或标记-整理(对象存活率高,复制成本大)。
2.3 常见垃圾回收器
| 回收器 | 区域 | 特点 |
|---|---|---|
| Serial | 新生代 | 单线程,STW,适合单核/小堆 |
| ParNew | 新生代 | 多线程版 Serial,常与 CMS 搭配 |
| Parallel Scavenge | 新生代 | 多线程,关注吞吐量 |
| Serial Old | 老年代 | 单线程,标记-整理 |
| Parallel Old | 老年代 | 多线程,与 Parallel Scavenge 搭配 |
| CMS | 老年代 | 并发标记清除,低停顿,有碎片、并发阶段占用 CPU |
| G1 | 全堆 | 分区(Region),可预测停顿,兼顾吞吐与低延迟 |
| ZGC / Shenandoah | 全堆 | 超低延迟,适合大堆 |
面试重点:
- CMS:并发收集,减少 STW;缺点为碎片、并发阶段 CPU 敏感、可能产生浮动垃圾。
- G1:将堆划分为多个 Region,优先回收价值高(回收收益大)的 Region,适合大堆和停顿时间敏感场景。
2.4 Minor GC、Major GC、Full GC
- Minor GC:新生代 GC,较频繁,速度较快。
- Major GC:通常指老年代 GC(有时与 Full GC 混用)。
- Full GC:整堆 + 方法区/元空间回收,STW 最长,应尽量减少发生。
3. 类加载机制
3.1 类加载过程
- 加载(Loading)
将 class 文件读入内存,生成Class对象。 - 链接(Linking)
- 验证:格式、字节码、符号引用等。
- 准备:为类静态变量分配内存并设零值;
static final常量在准备阶段即可赋字面量。 - 解析:将常量池中的符号引用替换为直接引用。
- 初始化(Initialization)
执行<clinit>,为静态变量赋真实初值并执行静态块。
面试重点:
- 准备阶段只做默认零值,不执行赋值语句;初始化阶段才执行静态赋值和 static 块。
- 触发初始化的典型场景:new、反射、调用静态方法/静态字段(非常量)、子类初始化时先触发父类初始化、主类等。
3.2 双亲委派模型
- 定义:类加载请求先交给父加载器;父加载器无法加载时,子加载器才尝试加载。
- 层次:Bootstrap → Extension → Application(系统类加载器)→ 自定义类加载器。
- 作用:避免类被重复加载;保护核心类库不被应用层同名类替换(如自定义
java.lang.String不会被加载)。
破坏双亲委派:
- 如 JDK 1.2 引入双亲委派前已有类加载器未遵循;SPI(如 JDBC)中 Bootstrap 需加载厂商实现,通过线程上下文类加载器(Context ClassLoader)加载。
- 热部署、OSGi 等场景会按模块使用不同类加载器,也会打破“单一委派”的约束。
3.3 自定义类加载器
- 继承
ClassLoader,重写findClass()(建议),在内部调用defineClass()将字节数组转为Class。 - 一般不重写
loadClass(),否则会破坏双亲委派;重写findClass()可在保持委派的前提下自定义“从哪里读 class”。
面试重点:
- 同一类由同一加载器加载时,才认为“相等”(equals);不同加载器加载的同一 class 文件会得到不同类。
4. 性能调优与监控
4.1 常用 JVM 参数(示例)
text
# 堆
-Xms512m # 初始堆
-Xmx512m # 最大堆(建议与 -Xms 相同,避免动态扩展)
-Xmn256m # 新生代(老年代 = 堆 - 新生代)
-XX:SurvivorRatio=8 # Eden : Survivor = 8 : 1 : 1
# 元空间(JDK 8)
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
# GC 选择
-XX:+UseG1GC # 使用 G1
-XX:MaxGCPauseMillis=200 # 期望最大停顿(G1 等)
# GC 日志(JDK 8)
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log
# GC 日志(JDK 9+)
-Xlog:gc*:file=gc.log:time,level,tags4.2 内存泄漏排查思路
- 现象:堆持续增长、Full GC 频繁但回收不多、最终 OOM。
- 步骤:
- 使用
jmap -heap <pid>看堆概况;jmap -dump:format=b,file=heap.hprof <pid>导出堆快照。 - 用 MAT、JProfiler 等分析:查看支配树、大对象、重复/异常集合(如未关闭的集合不断加入对象)。
- 结合业务:未关闭的连接、监听器未移除、静态集合无限增长、ThreadLocal 未 remove 等。
- 使用
4.3 GC 日志简要解读
- 关注:Young GC / Full GC 频率、每次停顿时间、回收前后各区域容量变化。
- 若 Young GC 频繁且回收少:可能是新生代过小或短生命周期对象过多。
- 若 Full GC 频繁且老年代居高不下:可能存在老年代堆积或泄漏。
4.4 常用监控与诊断工具
| 工具/命令 | 作用 |
|---|---|
| jps | 列出 Java 进程 |
| jstat | 查看 GC、类加载、编译等统计(如 jstat -gc <pid> 1000) |
| jmap | 堆信息、堆转储(-heap、-dump) |
| jstack | 线程快照,查死锁、阻塞(jstack <pid>) |
| jinfo | 查看/修改 JVM 参数 |
| VisualVM | 图形化监控堆、线程、CPU、GC |
| Arthas | 在线诊断:反编译、查看堆栈、监控方法调用、追踪等 |
面试重点:
- 线上 CPU 飙高:先用
top定位进程和线程,再用jstack看该线程栈,结合代码定位。 - OOM:堆转储 + MAT 分析,看哪些对象占用量大、是否合理、是否有泄漏路径。
小结
- 内存:堆(对象)、栈(栈帧)、方法区/元空间(类元信息);堆分新生代与老年代,新生代常用复制算法。
- GC:可达性分析判活;算法有标记-清除、标记-复制、标记-整理;回收器按场景选 Serial、CMS、G1、ZGC 等。
- 类加载:加载 → 链接(验证、准备、解析)→ 初始化;双亲委派保证核心类安全;自定义类加载器一般重写
findClass()。 - 调优与排查:合理设置堆与 GC 参数;结合 jstat、jmap、jstack、堆转储与 MAT/Arthas 做内存与 CPU 问题定位。