专业的编程技术博客社区

网站首页 > 博客文章 正文

JVM 深度解析:运行时数据区域、分代回收与垃圾回收机制全攻略

baijin 2025-05-11 14:05:40 博客文章 8 ℃ 0 评论


共同学习,有错欢迎指出。

JVM 运行时数据区域

1. 程序计数器

程序计数器是一块较小的内存空间,可看作当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器通过改变这个计数器的值选取下一条字节码指令,分支、循环、跳转等基础功能均依赖此计数器完成,是线程私有的内存。

2. Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈也是线程私有的,生命周期与线程相同。它描述 Java 方法执行的内存模型:每个方法执行时创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法从调用到执行完成,对应栈帧在虚拟机栈中入栈到出栈的过程。

3. 本地方法栈

本地方法栈与虚拟机栈作用相似,区别在于虚拟机栈为 Java 方法(字节码)服务,而本地方法栈为虚拟机使用的 Native 方法服务。

4. Java 堆

对于大多数应用,Java 堆是 JVM 管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建。此区域唯一目的是存放对象实例,几乎所有对象实例都在此分配内存。

5. 方法区

方法区用于存储已加载的类信息、常量、静态变量(如 static 修饰的变量)。运行时常量池是方法区的一部分,class 文件中的常量池存放编译期生成的字面量和符号引用。

老版 JDK 中,方法区称为永久代

JDK 1.8 后废弃永久代,引入元空间(Metaspace),元空间是 HotSpot JVM 对方法区的实现,使用本地内存,理论上受限于系统虚拟内存大小(需配置参数)。

分代回收

年轻代结构

HotSpot JVM 将年轻代分为 1 个 Eden 区和 2 个 Survivor 区(From 和 To)。新创建的对象通常分配到 Eden 区(大对象特殊处理),首次 Minor GC 后存活的对象移至 Survivor 区。对象在 Survivor 区每熬过一次 Minor GC,年龄增加 1 岁,达到阈值后移至老年代。

复制算法原理

年轻代采用复制算法,将内存分为两块,每次只用一块,满后将存活对象复制到另一块,不产生内存碎片。

GC 流程:

GC 开始时,对象存在于 Eden 和 From 区,To 区为空。

Eden 和 From 区存活对象复制到 To 区,From 区中年龄达阈值的对象移至老年代,未达阈值的复制到 To 区。

GC 后,Eden 和 From 区清空,From 与 To 角色互换,确保 To 区始终为空。

当 To 区填满时,所有对象移至老年代。

动态年龄计算

HotSpot 遍历对象时,按年龄从小到大累积占用大小,当某年龄对象总和超过 Survivor 区一半时,取该年龄与-XX:MaxTenuringThreshold的较小值作为新晋升阈值。设计目的

避免固定阈值导致 “过早晋升” 或 “Survivor 溢出”,动态适应对象生命周期波动。

常见垃圾回收机制

1. 引用计数法

每个对象含引用计数器,引用增加则 + 1,失效则 - 1。计数器为 0 时释放内存。优点是简单,缺点是无法处理循环引用,且计数开销持续存在。

2. 可达性分析算法

GC Roots(如虚拟机栈引用、方法区静态变量、常量引用、本地方法栈 JNI 引用等)为起点,通过引用链判断对象是否可达。不可达的对象判定为可回收。

CMS 收集器执行过程

初始标记(STW initial mark):从 GC Roots 开始,仅扫描直接关联对象并标记,停顿时间短。

并发标记(Concurrent marking):在初始标记基础上继续追溯标记,与用户线程并发执行。

并发预清理(Concurrent precleaning):扫描并发标记阶段新进入老年代的对象,减少重新标记工作量。

重新标记(STW remark):暂停虚拟机,扫描 CMS 堆剩余对象,处理引用变更。

并发清理(Concurrent sweeping):清理垃圾对象,与用户线程并发执行。

并发重置(Concurrent reset):重置 CMS 数据结构,等待下次 GC。

G1 收集器执行过程

标记阶段

初始标记(Initial-Mark,STW):触发一次 Young GC,标记 GC Roots 直接关联对象。

并发标记:全堆扫描,与用户线程并发执行,计算区域对象活性,回收全垃圾区域。

再标记(STW):使用 SATB 算法补充标记并发阶段新增垃圾。

清理阶段(STW):选择活性低的区域,等待下次 Young GC 回收。

回收 / 完成:Young GC 清理选定区域,可能保留部分高活性区域。

G1 与 CMS 对比


特性

CMS

G1

目标

最短回收停顿时间

高吞吐量 + 短停顿,适合大内存

作用区域

老年代

全堆(分 Region)

算法

标记 - 清除,易产生碎片

局部复制 + 整体标记 - 整理,碎片少

浮动垃圾处理

无法处理,需下次 GC

可处理

内存占用

小(Card Table)

大(RSet,约占堆内存 20%+)

默认启用版本

JDK 1.5-1.8

JDK 9+

适用场景

小内存、低延迟场景

大内存(6GB+)、高并发场景

GC Roots 对象

虚拟机栈(栈帧本地变量表)引用的对象。

方法区中类静态属性引用的对象。

方法区中常量引用的对象。

本地方法栈中 JNI 引用的对象。

Stop the World(STW)

GC 执行时,除 GC 线程外的所有用户线程挂起,直至 GC 完成。STW 是保证 GC 正确性的必要机制,但会影响实时性。可通过选择并发收集器(如 CMS、G1)降低停顿时间。

垃圾回收算法

停止 - 复制(Copying):暂停程序,将存活对象从当前堆复制到另一堆,适合新生代短生命周期对象,缺点是空间利用率低。

标记 - 清除(Mark-Sweep):标记存活对象,清除未标记对象,产生内存碎片,适合老年代。

标记 - 整理(Mark-Compact):标记后整理存活对象,压缩内存,避免碎片,适合老年代。

分代收集算法:新生代用复制算法,老年代用标记 - 清除或标记 - 整理算法,结合各代特点优化回收效率。

Minor GC 与 Full GC 触发条件

Minor GC 触发:Eden 区满时触发。

Full GC 触发

调用System.gc()(建议而非强制)。

老年代空间不足。

方法区空间不足(JDK 8 前永久代)。

Minor GC 后进入老年代的对象大小超过老年代可用内存。

新生代复制对象时,目标 Survivor 区空间不足,需晋升老年代但老年代空间不足。

对象进入老年代的条件

大对象直接进入:超过
-XX:PretenureSizeThreshold
(默认 3M)的对象直接在老年代分配。

长期存活对象:年龄超过-XX:MaxTenuringThreshold(默认 15)的对象晋升老年代。

动态年龄判定:Survivor 区中相同年龄对象总和超过该区一半时,大于等于该年龄的对象直接进入老年代。

空间分配担保:老年代最大连续空间不足,但大于历次晋升平均大小时,冒险执行 Minor GC;否则触发 Full GC。

TLAB(Thread-Local Allocation Buffer)

JVM 在新生代 Eden 区为每个线程分配一块私有区域(默认占 Eden 的 1%),用于快速分配小对象。TLAB 分配无需锁同步,提升效率。对象分配流程:

逃逸分析确定对象在堆分配。

若 TLAB 空间足够,直接分配并更新指针;否则重新申请 TLAB。

若 TLAB 仍不足,在 Eden 区加锁分配;Eden 不足则触发 Young GC,GC 后仍不足则分配到老年代。

对象内存分配方法

指针碰撞:堆内存规整时,通过指针移动分配内存(如 Serial、ParNew 收集器)。

空闲列表:堆内存碎片化时,维护可用内存列表分配(如 CMS 收集器)。

JVM 类加载过程

类生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。

加载:通过全限定名获取字节流,生成 Class 对象。

验证:确保字节流合法,防止安全漏洞。

准备:为静态变量分配内存(默认值),实例变量在对象实例化时分配。

解析:将常量池符号引用转为直接引用(如指针)。

初始化:执行类构造器<clinit>(),初始化静态变量和代码块。

双亲委派模型

类加载器优先委托父加载器加载类,递归至顶层,父加载器无法加载时才由子类加载器尝试。该模型避免类重复加载,保护核心类(如java.lang.*)。破坏场景:JNDI、JDBC 等 SPI 机制通过线程上下文类加载器逆向加载第三方库,突破双亲委派限制。

JVM 锁优化与膨胀过程

自旋锁:获取锁失败时忙循环尝试,减少线程阻塞开销;自适应自旋根据前次自旋次数动态调整。

锁粗化:扩大加锁范围,避免频繁申请 / 释放锁(如循环内的同步块合并)。

锁消除:逃逸分析确认对象无竞争时,移除不必要的锁。

偏向锁:无竞争时锁关联线程,再次访问无需抢占,适用于单线程场景。

轻量级锁:竞争较小时通过 CAS 自旋获取锁,自旋超阈值升级为重量级锁。

重量级锁:依赖操作系统 MutexLock,阻塞线程,适用于高竞争场景。

i++ 操作的字节码指令

将 int 常量加载到操作数栈顶。

取出操作数栈顶值,存储到局部变量表 Slot 1。

从 Slot 1 取出值,放回操作数栈顶。

Slot 1 中的值加 1。

将操作数栈顶值存回 Slot 1(即 i 变量)。

JVM 性能监控工具

1. 命令行工具

jps:查看 JVM 进程及 LVMID。

jstat:监控类加载、内存、GC 数据(如jstat -gcutil <pid> 1000)。

jinfo:查看 / 调整 JVM 参数。

jmap:生成堆转储快照(jmap -dump:file=heapdump.hprof <pid>)。

jhat:分析堆快照,通过 HTTP 服务器查看结果。

jstack:生成线程快照,排查死锁或阻塞(jstack <pid> > thread.log)。

2. 可视化工具

JConsole:JDK 自带图形化监控工具,支持内存、线程、类加载监控。

VisualVM:功能强大,支持插件扩展,可分析性能瓶颈、内存泄漏。

JVM 常见参数


参数

说明

-Xms20M

设置初始堆大小为 20M

-Xmx20M

设置最大堆大小为 20M(与 - Xms 相同可避免动态扩容)

-verbose:gc

输出 GC 详细日志

-Xss128k

设置虚拟机栈大小为 128k(HotSpot 不区分本地方法栈,-Xoss 无效)

-XX:MetaspaceSize=10M

设置元空间初始大小为 10M(JDK 8+)

-XX:+HeapDumpOnOutOfMemoryError

OOM 时生成堆转储快照

-XX:+UseG1GC

启用 G1 收集器

-XX:MaxTenuringThreshold=1

对象年龄大于 1 时进入老年代

-XX:PretenureSizeThreshold=3145728

大于 3M 的对象直接进入老年代(单位:字节)

-XX:+PrintGCDetails

打印 GC 详细信息

JVM 调优目标与场景

调优时机

老年代内存持续接近最大值。

Full GC 频繁触发。

GC 停顿时间超过 1 秒。

应用抛出OutOfMemoryError

本地缓存占用大量内存。

系统吞吐量或响应时间下降。

实战案例

1. Minor GC 频繁

原因:Eden 区过小,对象分配速率高。

优化:增大新生代空间(-Xmn)或调整-XX:SurvivorRatio,减少 GC 次数。

2. STW 过长

原因:老年代碎片多或大对象触发 Full GC。

优化:启用 G1 收集器(-XX:+UseG1GC),设置-XX:MaxGCPauseMillis=100控制停顿时间。

3. 元空间溢出(JDK 8+)

原因:动态类加载过多,元空间内存不足。

优化:调整-XX:MetaspaceSize-XX:MaxMetaspaceSize,或优化类加载逻辑。

4. 外部命令导致性能下降

问题:频繁调用Runtime.exec()执行 Shell 脚本,创建进程开销大。

解决方案:改用 Java API 获取信息,避免外部进程调用。

5. Windows 虚拟内存导致 GC 停顿

问题:程序最小化时内存交换到磁盘,GC 时因页面文件恢复导致长时间停顿。

解决方案:添加参数
-Dsun.awt.keepWorkingSetOnMinimize=true
,避免内存交换。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表