JVM内存模型与垃圾回收机制分析
1、介绍
JVM内存模型涉及运行代码,即运行代码产生的垃圾数据需要回收,堆是对象生存的核心战场,GC 主要在此区域运作,GC解决内存有限性、避免手动管理风险、动态回收垃圾、保障性能与稳定性,让开发者从内存管理中解放,专注于业务逻辑实现
2、JVM运行时的数据区由哪些组成的
1、组成结构
+-------------------+
| 方法区(Method) | <--- JDK8+:元空间(Metaspace)
|-------------------|
| 堆(Heap) | <--- 对象实例、数组(GC主战场)
|-------------------|
| 虚拟机栈(VM Stack)| <--- 线程私有:栈帧(局部变量表/操作数栈/动态链接/返回地址)
|-------------------|
| 本地方法栈(Native)| <--- JNI调用本地方法(C/C++)
|-------------------|
| 程序计数器(PC) | <--- 当前线程执行的字节码行号指示器
+-------------------+
2、堆和栈有什么区别
堆,是线程共享的,用来存储对象的实例,比如Object obj = new Object()出来的对象实例。也存字符串常量的具体值。由GC统一进行管理。
栈,是线程私有的,用来存储基本类型变量和对象的引用信息,比如obj的引用信息。栈信息是自动的分配和释放。
3、方法区的作用是什么,JDK8+之后发生了什么重大变化么
方法区在JDK7中称之为永久代,受JVM最大堆内存限制大小,在JDK8之后名称变成了Metaspace元空间,并且使用的是本地操作系统的本地内存,受操作系统本地可用内存大小限制。存储的内容主要有如下:类的元信息如类全限定名、访问修饰符、字段描述、方法描述
为什么元空间要取代永久代呢?
主要解决永久代的内存溢出和性能调优问题。永久代受限于JVM堆大小,而元空间使用本地操作系统内存,避免了Full GC对元数据回收的影响。同时分离元数据与堆内存,简化了JVM架构。
4、元空间会内存泄漏么?怎么排查
元空间也会出现内存泄漏问题,主要原因是由于代码设计不合理每次运行时都新建一个类加载器并加载了一些类信息放到元空间,并且这个类加载器(自定义或框架的)一直存在强引用,导致GC无法将类加载器回收,其加载的所有元数据信息永久驻留在元空间当中,并且一直增大,最终会导致元空间内存溢出风险。
线上可以通过Arthas工具,执行classloader -t dashboard
,可查看跟踪ClassLoader 内存及使用情况。也可以通过jstat -gcmetacapacity <PID>
观察元空间使用量是否只增不减,且Full GC后不下降。”
如果发现了问题可以修改代码将自定义的类加载器进行缓存处理,避免每次都新建类加载器。用完类加载器其相关的类确保无强引用,这样GC可回收。修复上线之后可观察GC元空间大小是否变小即可。
除了优化代码生产环节最好通过配置-XX:MaxMetaspaceSize=512m
限制元空间上限,监控类加载情况,控制风险。
5、字符串常量是怎么存储的
- JDK6:永久代(方法区)
- JDK7:移动到堆中(通过
-XX:StringTableSize
调整大小) - JDK8+:仍在堆中(元空间只存类元信息,不存字符串常量)
🌰 示例:
String s = "abc"
的"abc"
在堆中,而String.class
的元数据在元空间
3、JVM垃圾回收机制分析(GC)
1、垃圾回收的流程
1、判断可回收对象
通过可达性分析对象是否存活,GC_Roots从栈变量寻找关联对象(如对象A引用了对象C,对象B也在栈变量里),假设对象E不在这个GC_Roots关联链中,判断对象E可回收。
2、清除可回收对象
使用垃圾回收器,回收不可达的对象。主要垃圾回收器如下:
回收器 | 区域 | 算法 | 启动参数 | 特点适用场景 |
---|---|---|---|---|
Serial | 新生代 | 标记-复制 | -XX:+UseSerialGC | 单线程STW(Stop-The-World),**旧版 JDK(≤8)**可通过 分代组合 配置不同回收器,适合客户端模式(如桌面应用) |
ParNew | 新生代 | 标记-复制 | -XX:+UseParNewGC | Serial 的多线程版本,**旧版 JDK(≤8)**可通过 分代组合 配合CMS老年代 回收器使用。 |
Parallel Scavenge | 新生代 | 标记-复制 | -XX:+UseParallelGC | 多线程吞吐量优先,**旧版 JDK(≤8)**可通过 分代组合 配合Parallel Old老年代回收器使用,适合后台计算型应用 |
Serial Old | 老年代 | 标记-整理 | -XX:+UseSerialOldGC | Serial 的老年代版本,**旧版 JDK(≤8)**可通过 分代组合 配置不同回收器,适合客户端模式(如桌面应用) |
Parallel Old | 老年代 | 标记-整理 | -XX:+UseParallelOldGC | 多线程吞吐量优先,**旧版 JDK(≤8)**可通过 分代组合 配合Parallel Scavenge新生代回收器使用,适合后台计算型应用 |
CMS (ConcMarkSweep) | 老年代 | 标记-清除 | -XX:+UseConcMarkSweepGC | 低延迟优先,并发收集(减少 STW),**旧版 JDK(≤8)**可通过 分代组合 配置不同回收器,JDK 14+ 已移除 CMS。适合Web 服务、响应敏感系统 |
G1(Garbage-First) | 全堆 | 分区域标记-整理 | -XX:+UseG1GC | 可预测停顿时间,兼顾吞吐与延迟,JDK 9+ 默认 |
ZGC | 全堆 | 升级版标记-复制(染色指针+读屏障) | -XX:+UseZGC | 超大内存、低延迟系统,TB 级堆,JDK 17+ 推荐使用 ,升级版的标记复制算法体现在消除了转移复制导致的停顿STW,而通过染色指针+读屏障实现并发转移,仅仅需要极短的停顿即可,适合金融交易、实时游戏、大数据分析等系统。 |
Shenandoah | 全堆 | -XX:+UseShenandoahGC | 与 ZGC 类似,RedHat 贡献 |
一个JVM可配置多个垃圾回收器么?在JDK8之前使用分代组合,新生代和老年代可配置不同的垃圾回收器。在JDK9+推荐全堆垃圾回收器。消除了分代组合带来的复杂性。
- 标记-清除(Mark-Sweep)
如CMS垃圾回收器(JDK 14+ 已移除 CMS)采用的标记清除法。并发标记所有可达对象。清除未标记的对象。此方式容易造成内存碎片化(后续需 Full GC 整理),对象越多越慢,如果垃圾产生的速度大于回收速度时,会退化为标记整理法 Serial Old。并发多还会占用线程资源。
具体流程如下
- 初始标记(STW):标记
GC Roots
直接关联的对象。 - 并发标记:遍历对象图(与用户线程并发)。
- 重新标记(STW):修正并发标记期间的变动。
- 并发清除:清理垃圾(并发执行)。
- 标记-复制(Copying)
将内存分为两块(如 Eden
和 Survivor
)。将存活对象复制到另一块内存,清空当前块。
此方式内存不会造成碎片化,效率也比较高,但是导致内存的利用率变低仅 50%。
但是在超大内存、低延迟系统,TB 级堆,JDK 17+ 推荐使用 ZGC垃圾回收器。不在担心内存不够用,而且相比于传统的标记-复制算法,ZGC升级了的标记复制算法体现在消除了转移复制导致的停顿STW,而通过染色指针+读屏障实现并发转移,仅仅需要极短的停顿即可,适合金融交易、实时游戏、大数据分析等系统。
- 标记-整理(Mark-Compact)
标记所有可达对象。将存活对象向一端移动,清理边界外内存。
此方式不会造成碎片化(适用于老年代),但是移动成本比较高
G1垃圾回收器标记-整理进行优化,针对于整个堆,它将堆分成多个区域(默认2048个),通过停顿预测模型计算每个区域的回收价值(回收时间+释放空间),优先回收最有价值的区域。
- 分代收集(Generational Collection)
⚠️ 注意:分代手机已不推荐使用,JDK9+推荐使用G1、ZGC、Shenandoah 全堆回收器,会统一管理新生代和老年代,不能与其他回收器组合。
例如:-XX:+UseG1GC
会覆盖所有分代配置。
分代收集可了解如下:
按对象生命周期划分内存区域(新生代、老年代),对不同区域使用不同算法。
新生代:对象存活率低 → 复制算法(Eden
+ 2个 Survivor
区)。
老年代:对象存活率高 → 标记-清除或标记-整理。
同一个内存区域不可以同时配置多个垃圾回收器,为不同区域配置不同的回收器,这是JVM分代垃圾回收机制的核心设计。
JVM 将堆内存分为 新生代(Young Generation) 和 老年代(Old Generation),允许为两者独立选择回收器:
- 新生代回收器
Serial
,ParNew
,Parallel Scavenge
- 老年代回收器
Serial Old
,Parallel Old
,CMS
组合示例如下:
# 新生代用 ParNew + 老年代用 CMS(经典组合)
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC# 新生代用 Parallel Scavenge + 老年代用 Parallel Old(吞吐量优先)
-XX:+UseParallelGC -XX:+UseParallelOldGC
2、Minor GC vs Full GC 的区别?
当年轻代的Eden区满时,触发Minor GC 回收新生代的区域(Eden + Survivor)
当老年代空间不足或元空间不足或代码主动System.gc(),触发Full GC 回收整个堆(新生代 + 老年代 + 元空间)
G1虽然是全堆垃圾回收器,仍有类似 Minor GC 的行为(Young GC),但物理上不要求连续的新生代内存,且回收机制与传统分代回收器不同。
ZGC:完全不分代,无 Minor GC,ZGC 彻底移除了分代设计(截至 JDK 21),采用 全堆并发回收, 通过 并发复制算法 直接管理全堆,避免分代带来的复杂性和停顿。
3、对象如何晋升到老年代
- 年龄阈值:对象在 Survivor 区每熬过一次 Minor GC 年龄 +1,达到阈值(默认 15)进入老年代。
- 大对象直接进入老年代(如长数组)。
- Survivor 区空间不足时,存活对象直接进入老年代。