JVM 之 内存模型与垃圾回收机制(GC)
摘要
-
本文介绍JVM的内存模型与垃圾回收机制
JVM 的参数(三类)
-
标准参数: 以
-
开头,所有 HotSpot 都⽀持。例如java -version
。这类参数可以使⽤java -help
或者java -?
全部打印出来 -
⾮标准参数: 以
-X
开头,是特定 HotSpot版本⽀持的指令。例如java -Xms200M -Xmx200M
。这类指令可以⽤java -X
全部打印出来。 -
不稳定参数: 这也是 JVM调优的噩梦。以
-XX
开头,这些参数是跟特定HotSpot版本对应的,很有可能换个版本就没有了。详细的⽂档资料也特别少。JDK8 中的以下⼏个指令可以帮助开发者了解 JDK8 中的这⼀类不稳定参数。
1 | java -XX:+PrintFlagsFinal -version # 所有参数最终⽣效的值。 |
-
运行
java -XX:+PrintFlagsFinal -version
最后一列的标记说明
标记 | 含义 |
---|---|
{product} |
正式产品参数(用户可用,可通过 -XX: 进行设置) |
{pd product} |
平台相关的正式参数(platform-dependent),在某些操作系统/CPU 上可用 |
{C2 product} |
仅在使用 C2 编译器(优化编译器) 时有效的参数 |
{diagnostic} |
诊断参数,默认不能直接使用,需加 -XX:+UnlockDiagnosticVMOptions 启用 |
{experimental} |
实验性参数,需加 -XX:+UnlockExperimentalVMOptions 才能使用 |
{manageable} |
可通过 JMX 动态管理的参数(运行时可调整) |
{notproduct} |
非产品参数,仅用于 JVM 开发和测试(普通用户无法使用) |
{ARCH product} |
与 CPU 架构相关的产品参数(如 x86、ARM 等) |
{JVMCI} |
与 JVM Compiler Interface 相关的参数 |
{lp64_product} |
仅在 64 位 JVM 上才有效的产品参数 |
{develop} |
开发用参数,仅用于 HotSpot 内部开发(构建时启用) |
{commercial} |
商业特性参数,仅在 Oracle JDK 的商业版中可用(已废弃) |
-
我们只需要关注
{product}
参数,常用 product 参数(可直接使用)
参数 | 说明 | 示例值 |
---|---|---|
-Xms / -Xmx |
初始/最大堆大小 | -Xms2g -Xmx2g |
-Xmn |
新生代大小 | -Xmn512m |
-Xss |
每个线程的栈大小 | -Xss512k |
-XX:SurvivorRatio |
Eden:S0:S1 比例 | -XX:SurvivorRatio=8 |
-XX:MaxTenuringThreshold |
最大晋升年龄阈值 | -XX:MaxTenuringThreshold=15 |
-XX:+UseG1GC / -XX:+UseParallelGC 等 |
选择 GC 类型 | -XX:+UseG1GC |
-XX:MaxGCPauseMillis |
最大 GC 暂停目标 | -XX:MaxGCPauseMillis=200 |
-XX:GCTimeRatio |
GC 时间比率 | -XX:GCTimeRatio=9 |
-XX:+UseStringDeduplication |
启用字符串去重(仅 G1 有效) | -XX:+UseStringDeduplication |
-XX:MetaspaceSize / -XX:MaxMetaspaceSize |
设置元空间大小 | -XX:MetaspaceSize=128m |
-XX:+AlwaysPreTouch |
启动时预分配所有内存页,避免运行期延迟 | -XX:+AlwaysPreTouch |
-XX:+PrintGCDetails |
打印 GC 详情 | - |
-XX:+PrintGCDateStamps |
打印带时间戳的 GC 日志 | - |
JVM虚拟机结构(HotSpot)
-
设置内存分配示例
1 | # jdk1.8+ |
-
各参数含义与默认值(针对 JDK 1.8)
参数 | 含义说明 | 默认值(JDK 1.8) |
---|---|---|
-Xms2048M |
初始堆大小,即 JVM 启动时分配的堆内存大小(这里是 2GB) | 物理内存的 1/64(最小 1MB),推荐配置为与 -Xmx 一致 |
-Xmx2048M |
最大堆内存大小,JVM 允许分配的最大堆内存 | 物理内存的 1/4(受限于 32位/64位) |
-Xmn1024M |
新生代大小(Eden + Survivor)为 1GB | 未显式指定时,通常占堆的 1/3 左右 |
-Xss512K |
每个线程的栈大小(Thread Stack Size),这里设置为 512KB | 1MB(64位系统)或 512KB(32位系统) |
-XX:MetaspaceSize=256M |
元空间初始大小(用于加载类的元数据,不再使用 PermGen) | 21MB(客户端)或 16MB(服务器端),推荐配置为与 MaxMetaspaceSize 一致 |
-XX:MaxMetaspaceSize=256M |
元空间最大大小,超过后触发 Full GC | 无限制(默认只受物理内存约束) ,推荐配置一个合适的数值,比如8G的内存可以配置为256M |
类装载子系统(Class Loading Subsystem)
-
类装载子系统负责将
.class
文件加载到 JVM 中,并进行解析、验证、初始化等过程。 -
加载过程的几个阶段:
阶段 | 说明 |
---|---|
加载(Loading) | 将 .class 文件读取为二进制数据,构造 Class 对象 |
验证(Verification) | 确保字节码文件格式正确、安全合法(防止恶意代码) |
准备(Preparation) | 为类的静态变量分配内存,并设置默认初始值 |
解析(Resolution) | 将常量池中的符号引用替换为直接引用(方法、字段等) |
初始化(Initialization) | 执行 <clinit> 静态初始化方法,对静态变量赋初始值 |
-
类加载器(ClassLoader)体系结构:详细参考 JVM 之 类加载器
类加载器名称 | 加载内容 | 说明 |
---|---|---|
引导类加载器(Bootstrap ClassLoader) | Java 核心类库(如 java.lang.* ) |
由 JVM 自身实现,用本地代码实现,无法直接访问 |
扩展类加载器(Extension ClassLoader) | JAVA_HOME/jre/lib/ext 目录下的类 |
加载标准扩展类库 |
应用类加载器(Application ClassLoader) | 用户应用类路径(CLASSPATH 指定的目录) | 最常用,加载大多数应用代码 |
自定义类加载器(Custom ClassLoader) | 用户手动实现的类加载器 | 可以打破双亲委派机制,实现热加载、加密类加载等 |
类加载采用 双亲委派模型:请求会先向父加载器委托,只有在父加载器加载失败时才尝试自身加载。
字节码执行引擎(Execution Engine)
-
字节码执行引擎负责将 Java 字节码解释或编译为机器代码,并在底层平台上执行。
-
执行引擎的核心模块
组件名称 | 作用说明 | 关键特性 |
---|---|---|
解释器(Interpreter) | 将字节码逐条解释执行 | 启动快,适合冷代码(非热点代码),执行效率相对较低 |
即时编译器(JIT Compiler) | 将热点代码编译为本地机器码,提高执行效率 | 包含 C1(Client)和 C2(Server)两种,支持优化如:方法内联、逃逸分析、循环展开等 |
垃圾收集器(Garbage Collector, GC) | 自动内存管理,负责对象生命周期的回收 | 常见算法包括:Serial、Parallel、CMS、G1、ZGC(低延迟)等,根据不同场景选择 |
本地接口(Native Interface) | 支持 Java 与本地语言(如 C/C++)的互操作 | 通过 JNI(Java Native Interface)实现,调用底层操作系统或第三方库功能 |
JVM 内存模型(Java Memory Model,JMM)
-
JMM 是 Java 虚拟机规范中定义的一种 抽象内存模型,它决定了多线程程序中变量的读写可见性、有序性和原子性。同时,JVM 在物理层也有一个实际的内存结构,称为运行时数据区域(Runtime Data Areas),这两个可以结合理解。
-
JMM 的主要目标
- 保证多线程环境下的数据一致性
- 指导 JVM 和 CPU 的内存交互行为(如重排序、缓存)
-
实际运行时内存结构如下:
内存区域 | 说明 |
---|---|
📌 程序计数器(PC) | 每个线程私有,记录当前执行的字节码指令地址 |
📌 Java 线程栈 | 每个线程私有,方法调用时用于存储局部变量、操作数栈等 |
📌 本地方法栈 | 与虚拟机栈类似,用于 native 方法 |
📌 堆(Heap) | 所有线程共享,存储对象实例、数组等,GC 的主要区域 |
📌 方法区(或元空间) | 所有线程共享,存储类信息、静态变量、常量池等(JDK8 后称为 Metaspace) |
垃圾回收器
-
垃圾回收器负责自动管理内存,回收不再使用的对象,避免内存泄漏和溢出。
什么是垃圾
-
内存中没有被(线程栈变量,静态变量,常量池,JNI指针)引用的地址就是垃圾
-
可达性分析算法:是现代 JVM 判断一个对象是否“还活着”的主要算法。
1 | 基本思想: |
-
在 JVM 中,
GC Roots
是一些 始终可用的、不会被垃圾回收的引用起点,主要包括:
GC Roots 来源 | 说明 |
---|---|
当前线程栈中的引用(局部变量表) | 各个线程正在调用的方法中的局部变量、参数等 |
静态字段引用 | 类的静态字段引用的对象 |
JNI 引用(Native 方法引用) | Java 本地方法中引用的对象 |
常量引用池中的对象 | 字符串常量等可能持有对象引用 |
活动线程对象 | 线程自身在 GC 时不会被回收 |
JVM 内部结构(如系统类加载器等) | JVM 关键系统对象 |
垃圾回收器种类:
-
左边6种叫分代模型,右边的4种叫分区模型
分代模型(Generational Model)
堆内存划分为:
年轻代(Young Generation):存放新创建的对象,分为 Eden 和 Survivor 区。
老年代(Old Generation):存放经过多次 GC 后仍然存活的对象。
分区模型(Region-based Model)
分区模型(如 G1、ZGC、Shenandoah)不再严格按照代划分内存,而是把堆划分为多个 大小相同的 Region。每个 Region 可以在运行时被动态标记为 Eden、Survivor 或 Old。
-
分代模型中,上面3个是新生代垃圾回收器,下面3个是老年代垃圾回收器,可以交叉配对(见上图虚线),但最常用是上下两两配对。
-
CMS即可以作为新生代垃圾回收器,也可以作为老年代垃圾回收器。
-
EpsilonGC,是一个特殊的垃圾回收器,它不回收任何对象,只负责最终记录,做测试用的。
-
目前最先进的模型是
ZGC
,jkd11开始支持,但直到JDK16才比较完善,目前非默认配置,需要手动配置。其与Redhat出品的Shenandoah
是竞争关系。 -
常见垃圾回收器及其分类、JDK版本
垃圾回收器 | 所属模型 | 说明 | 首次出现 JDK 版本 | 启用命令(JVM 参数) | 适合的堆内存大小 |
---|---|---|---|---|---|
Serial | 分代模型 | 单线程,新生代和老年代都使用 Serial,适用于小堆内存 | JDK 1.2 | -XX:+UseSerialGC |
💾 小于 1GB |
ParNew | 分代模型 | Serial 的多线程版本,仅用于 CMS 新生代 | JDK 1.4 | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
💾 1GB ~ 4GB |
Parallel(吞吐量 GC) | 分代模型 | 多线程 GC,适合吞吐量优先的应用场景 | JDK 1.4 | -XX:+UseParallelGC (新生代)-XX:+UseParallelOldGC (老年代) |
💾 2GB ~ 8GB |
CMS(Concurrent Mark Sweep) | 分代模型 | 老年代并发标记-清除,低延迟,但存在碎片,已弃用 | JDK 1.4 | -XX:+UseConcMarkSweepGC |
💾 2GB ~ 8GB(已过时) |
G1(Garbage First) | 分区模型 | 将堆划分为 Region,逻辑分代,支持并发压缩,平衡延迟与吞吐 | JDK 7u4(正式) | -XX:+UseG1GC |
💾 4GB ~ 数十 GB |
ZGC(Z Garbage Collector) | 分区模型 | Region 弹性大小,支持超大堆,低延迟(<10ms 停顿) | JDK 11(实验),JDK 15(正式) | -XX:+UseZGC |
💾 8GB ~ 数 TB(超大堆) |
Shenandoah | 分区模型 | 红帽主导,低延迟,并发回收与并发压缩 | JDK 12(实验),JDK 15(正式) | -XX:+UseShenandoahGC |
💾 2GB ~ 数十 GB |
对于 CMS,启用后会自动使用 ParNew 作为新生代回收器(除非显式禁止)。
CMS 已在 JDK 9 开始标记为弃用,JDK 14 完全移除,不推荐再使用。
对于 Parallel GC 启用-XX:+UseParallelGC
(新生代)会自动启用-XX:+UseParallelOldGC
(老年代)。
对于ZGC,jdk13以前最大支持4T内存,之后最大支持16T内存。
-
各版本默认垃圾回收器及推荐配置(JDK 1.6 起)
JDK 版本 | 默认 GC | 建议使用 GC(按场景分类) |
---|---|---|
JDK 1.6 | Serial / Parallel | - 小型应用(如桌面程序):-XX:+UseSerialGC - 中大型应用(吞吐量优先): -XX:+UseParallelGC |
JDK 1.7 | 同上 | 同 JDK 1.6 |
JDK 1.8 | Parallel | - 吞吐优先:默认 -XX:+UseParallelGC - 响应优先: -XX:+UseConcMarkSweepGC (CMS)- 大堆 + 未来升级考虑: -XX:+UseG1GC (推荐) |
JDK 9-14 | G1 GC | - 一般默认即可:-XX:+UseG1GC (延迟与吞吐平衡)- 极端低延迟要求:升级到 JDK 11+ 使用 ZGC/Shenandoah |
JDK 15+ | G1 GC(默认),ZGC / Shenandoah 可选 | - 延迟敏感(在线服务、RT系统):-XX:+UseZGC 或 -XX:+UseShenandoahGC - 吞吐为主: -XX:+UseParallelGC - 综合平衡: -XX:+UseG1GC (默认) |
-
判断默认 GC 的方式
1 | java -XX:+PrintCommandLineFlags -version |
垃圾回收算法
-
JVM 中常见垃圾回收算法汇总
算法名称 | 核心思想 | 适用阶段/区域 | 优缺点简述 |
---|---|---|---|
标记-清除(Mark-Sweep) | 标记出存活对象,清除未标记对象 | 老年代 | 简单高效,但会产生大量碎片,不适合连续内存分配 |
标记-整理(Mark-Compact) | 标记后移动存活对象,整理碎片 | 老年代 | 消除碎片,代价是移动对象,适用于老年代压缩 |
复制算法(Copying) | 将对象复制到另一块内存(如 Eden → Survivor) | 新生代 | 高效率,适合回收大多数对象短命的新生代,但需要额外空间 |
分代回收(Generational) | 将对象按生命周期划分(新生代/老年代) | 整个堆结构 | 实用性强,结合不同算法应用于不同代,现代 GC 基础 |
分区回收(Region-based) | 将堆划分为若干等大小的 Region 动态分配 | 整个堆(如 G1、ZGC) | 更灵活,支持并发并行,减少 STW 停顿,适用于大堆、低延迟场景 |
增量回收(Incremental) | 分阶段小步执行 GC 以减少单次停顿 | 某些并发/低延迟 GC | 减少暂停时间,但整体效率可能降低 |
并发回收(Concurrent) | 标记、清理等步骤与应用线程并发执行 | CMS、G1、ZGC 等 | 停顿时间短,对响应时间要求高的系统友好 |
三色标记(Tri-color Marking) | 并发标记算法的一种实现思想 | CMS、G1、ZGC 等 | 白(待回收)、灰(已标记未扫描)、黑(已标记已扫描),避免“漏标”问题 |
SATB(Snapshot-At-The-Beginning) | 并发标记的快照策略 | G1、ZGC | 保证在并发标记过程中不遗漏新引用,适合高并发场景 |
Lazy Compaction(延迟压缩) | 不每次 GC 都压缩,视情况而定 | G1 等 | 降低不必要的移动成本 |
-
对应关系:GC 回收器和底层算法
回收器 | 使用的算法组合 |
---|---|
Serial | 新生代:复制算法 老年代:标记-整理 |
ParNew | 新生代:复制算法(多线程) |
Parallel | 新生代:复制算法 老年代:标记-整理 |
CMS | 新生代:ParNew(复制) 老年代:标记-清除 + 三色标记 + 并发 |
G1 | 分区回收 + 三色标记 + SATB + Lazy Compaction |
ZGC | 分区回收 + 并发标记 + SATB + Region Remapping |
Shenandoah | 分区回收 + 并发标记 + 并发压缩 + 三色标记 |
1 | ┌────────────────────────┐ |
什么是 STW(Stop-The-World)
-
STW(Stop-The-World) 指的是:在某些垃圾回收阶段,JVM 会暂停所有应用线程(也叫用户线程),让垃圾回收线程独占 CPU 执行 GC 逻辑。
-
你可以这样理解 STW
- JVM 会“按下暂停键”暂停所有正在运行的 Java 程序代码;
- 然后 专心进行 GC 的某些阶段(如标记、整理、复制等);
- GC 完成后,才会“恢复运行”应用线程。
-
各 GC 中 STW 的存在情况
回收器 | 是否存在 STW? | 说明 |
---|---|---|
Serial | ✅ 是,全停顿,全阶段单线程 | 堆越大 STW 越长 |
Parallel | ✅ 是,全停顿,多线程执行 GC | 提高效率但仍会暂停 |
CMS | ✅ 有,初始标记和最终重新标记是 STW | 大部分阶段并发执行 |
G1 | ✅ 有,但设计为尽可能缩短 STW | 分阶段并发 + 并行处理 |
ZGC | ✅ 极短(<10ms) | 仅个别阶段是 STW,几乎感知不到 |
Shenandoah | ✅ 极短 | 高度并发,STW 时间也极短 |
-
为什么要关注 STW?
- 在响应时间敏感型系统(如在线交易系统、游戏服务器、API 网关)中,长时间的 STW 会造成用户请求卡顿、超时。
- 因此,选择 低 STW 的 GC(如 G1、ZGC、Shenandoah) 对这类系统至关重要。
堆内存结构
-
不同的垃圾回收器决定了堆内存的结构不同,但总体上分为两种类型:分代模型和分区模型。
分代模型
Parallel 垃圾回收器
-
堆结构被划分为
Old Generation(老年代)
和Young Generation(新生代)
两部分。 -
Young Generation
由Eden
和 两个Survivor
组成,其中Eden
是一个连续的内存区域,Survivor
是一个非连续的内存区域。 -
默认情况下,
Young Generation
占堆内存的1/3
,Old Generation
占堆内存的2/3
。 -
默认情况下,
Eden:S0:S1 = 8:1:1
,如果希望为 4:1:1,使用-XX:SurvivorRatio=4
,但实际上这个比例并不是固定的,而是由jvm基于情况自动变化的,因为JVM默认开启了这个参数-XX:+UseAdaptiveSizePolicy
,如果希望固定这个比例,可以设置为-XX:-UseAdaptiveSizePolicy
来关闭这个配置。 -
默认情况下,对象最多经历
15次
Minor GC后进入老年代,可以通过-XX:MaxTenuringThreshold=n
设置。
区域 | 说明 |
---|---|
Eden | 对象首次创建的区域,大部分对象在这里创建并很快被回收 |
S0/S1 | Survivor 区域:两个交替使用的缓冲区(From 和 To),用于拷贝存活对象 |
Old | 老年代:存活次数多、生命周期长的对象会从新生代晋升到老年代,回收频率低 |
-
对象内存分配图解(简化)
1 | 创建对象 |
-
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组),如果对象很大(如超过 Eden 区大小,或是超过 Survivor 区大小),可能直接分配到老年代
也可以通过-XX:PretenureSizeThreshold=<大小,单位字节>
控制超过一定大小的对象是否直接分配到老年代(Old Generation),跳过新生代(Eden),以避免大对象频繁在年轻代造成 GC 压力。这个参数只在 Serial 和ParNew两个收集器下有效。
-
对象动态年龄判断
对象在Survivor区来回移动时,如果这批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,
例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。
对象动态年龄判断机制一般是在minor gc之后触发的。
-
对象逃逸分析
如果对象很小,且只在方法内部使用,并没有被外部引用,则其有可能直接分配到栈内存,而不进入堆内存
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换
优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
- 标量替换
通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。
开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。
- 标量与聚合量
标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),
标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
Parallel GC 参数配置表(吞吐量优先 GC)
参数名 | 说明 | 默认值(如未特别说明) |
---|---|---|
-XX:+UseParallelGC |
开启 Parallel GC,用于新生代收集 | - |
-XX:+UseParallelOldGC |
开启老年代并行收集(Parallel Old) | - |
-XX:ParallelGCThreads |
垃圾回收时的并行线程数(与 CPU 数量相关) | 根据硬件自动配置 |
-XX:MaxGCPauseMillis |
设置 GC 最大暂停时间目标(影响内存分配策略) | 无默认限制(建议按需设置) |
-XX:GCTimeRatio |
设置 GC 时间与应用运行时间的比值(0~100) 值越小,GC 越频繁,值越大 GC 越少 |
99(表示 1% 用于 GC,99% 用于应用) |
-XX:+UseAdaptiveSizePolicy |
启用自适应 GC 策略(根据运行状况自动调整各区域大小) | 默认开启 |
-XX:SurvivorRatio |
Eden 与 Survivor 的内存比例(如 8 表示 Eden:S0:S1 = 8:1:1) | 8 |
-XX:InitialTenuringThreshold |
对象晋升到老年代的初始年龄(会随运行动态调整) | 7 |
-XX:MaxTenuringThreshold |
晋升到老年代的最大年龄(对象在 Survivor 区经历几次 GC) | 15 |
-XX:PretenureSizeThreshold |
设置大对象阈值,超过该大小的对象直接分配到老年代 | 0(即禁用) |
-XX:+ScavengeBeforeFullGC |
在 Full GC 之前是否先执行一次 Minor GC | 开启 |
-XX:+UseFastAccessorMethods |
优化原始类型的 get/set 方法性能 | 开启 |
-XX:+AlwaysPreTouch |
JVM 启动时立即分配并初始化所有内存页,避免运行时首次分配带来的停顿 | 关闭 |
分区模型
G1 垃圾回收器
-
G1 属于 物理上分区,逻辑上分代,真正的分区模型是
ZGC
-
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有
2048
个Region。 -
一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数
-XX:G1HeapRegionSize
手动指定Region大小,但是推荐默认的计算方式。 -
G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。
-
默认年轻代对堆内存的占比是
5%
,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过-XX:G1NewSizePercent
设置新生代初始占比 -
在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过
60%
,可以通过-XX:G1MaxNewSizePercent
调整。 -
年轻代中的Eden和Survivor对应的region也跟之前一样,默认
8:1:1
,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。 -
一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。
-
G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫
Humongous
区,而不是让大对象直接进入老年代的Region中。 -
在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的
50%
,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。 -
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
-
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
G1垃圾收集分类
-
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数-XX:MaxGCPauseMills
设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做YoungGC,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMills
设定的值,那么就会触发Young GC -
MixedGC
不是FullGC,老年代的堆占有率达到参数-XX:InitiatingHeapOccupancyPercent
设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC -
Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)
G1 收集器参数配置表
参数名 | 说明 | 默认值 |
---|---|---|
-XX:+UseG1GC |
使用 G1 垃圾收集器 | - |
-XX:ParallelGCThreads |
指定 GC 工作的线程数量 | 与 CPU 数量相关 |
-XX:G1HeapRegionSize |
设置堆分区大小(1MB~32MB,2 的幂),堆默认划分为 2048 个 Region | 自动计算 |
-XX:MaxGCPauseMillis |
设置 GC 目标最大暂停时间 | 200ms |
-XX:G1NewSizePercent |
新生代初始占比(占整个堆) | 5% |
-XX:G1MaxNewSizePercent |
新生代最大占比(占整个堆) | 60% |
-XX:TargetSurvivorRatio |
Survivor 区填充目标(超过该比例,晋升老年代) | 50% |
-XX:MaxTenuringThreshold |
最大对象年龄阈值(年龄超过将进入老年代) | 15 |
-XX:InitiatingHeapOccupancyPercent |
老年代使用率超过该值触发 Mixed GC | 45% |
-XX:G1MixedGCLiveThresholdPercent |
Mixed GC 中:Region 存活对象占比低于该值才会被回收 | 85% |
-XX:G1MixedGCCountTarget |
每次 Mixed GC 的最大回收轮次 | 8 |
-XX:G1HeapWastePercent |
Mixed GC 回收目标:空闲 Region 达堆总量该百分比就结束混合回收 | 5% |
ZGC 垃圾回收器
-
ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器,ZGC可以说源自于是Azul System公司开发的C4(Concurrent Continuously Compacting Collector) 收集器
Platform | Supported | Since | Comment |
---|---|---|---|
Linux/x64 | ✅ | JDK 11 | |
Linux/AArch64 | ✅ | JDK 13 | |
macOS | ✅ | JDK 14 | |
Windows | ✅ | JDK 14 | Requires Windows version 1803 (Windows 10 or Windows Server 2019) or later. |
-
ZGC收集器是一款基于Region内存布局的,暂时不设分代的(jdk21后支持分代),使用了读屏障、颜色指针等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
-
ZGC的Region可以具有大、中、小三类容量:
- 小型Region(Small Region) : 容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region(Medium Region) : 容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
- 大型Region(Large Region) : 容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。
每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。
-
ZGC 特点:
- 支持极大堆(JDK 15 起支持最高 16TB)
- GC 停顿时间通常低于 1ms(与堆大小基本无关)
- 适合低延迟、高吞吐、高可用的应用场景,如交易系统、广告推荐等
ZGC 收集器参数配置表
参数名 | 说明 | 默认值(如未特殊标注) |
---|---|---|
-XX:+UseZGC |
启用 ZGC 垃圾收集器 | - |
-XX:+UnlockExperimentalVMOptions |
解锁实验性参数(JDK 11~14 使用 ZGC 必须) | - |
-XX:+UseLargePages |
启用大页内存(性能优化) | false |
-XX:+UseTransparentHugePages |
启用透明大页(部分 Linux 上可结合使用) | false |
-Xmx / -Xms |
设置最大/初始堆大小 | 用户配置 |
-XX:ZUncommitDelay=<秒> |
未使用内存释放回操作系统的延迟时间(ZGC 会自动释放内存) | 300(5分钟) |
-XX:SoftMaxHeapSize=<大小> |
软最大堆(Soft Heap Limit):ZGC 尝试将使用的堆控制在该值以内 | 默认等于 Xmx |
-XX:MaxHeapFreeRatio |
最大堆空闲比例(超过此比例可能触发内存释放) | 70 |
-XX:MinHeapFreeRatio |
最小堆空闲比例(低于此比例可能触发扩容) | 40 |
-XX:+ZGenerational |
启用 ZGC 分代收集(从 JDK 21 起支持,默认关闭) | false(JDK 21+) |
-XX:+PrintGC / -Xlog:gc* |
开启 GC 日志输出 | - |
-XX:+ZProactive |
主动回收策略(在系统空闲时尝试回收) | false |
-XX:ZCollectionInterval=<秒> |
主动回收之间的最小时间间隔(配合 ZProactive) | 默认值因版本而异 |
-XX:+ZConcurrentRootsScan |
启用并发根扫描(优化初始标记阶段) | JDK 17+ 默认启用 |
内存溢出(OutOfMemoryError,简称 OOM)
-
JVM 常见内存溢出类型汇总表
溢出类型 | 异常信息 | 触发原因/描述 | 相关参数和建议配置 |
---|---|---|---|
堆内存溢出 | java.lang.OutOfMemoryError: Java heap space |
- 创建大量对象,堆空间不足 - 内存泄漏:对象不再使用却有强引用 - 老年代对象太多无法晋升 |
-Xms / -Xmx 设置堆大小 |
栈溢出(递归) | java.lang.StackOverflowError |
- 方法无限递归或递归层级太深 | -Xss 设置线程栈大小 |
无法创建新线程 | java.lang.OutOfMemoryError: unable to create new native thread |
- 创建线程过多(如线程池配置过大) - 系统或 JVM native 线程资源耗尽 |
控制线程池大小,避免无限新建线程 |
元空间溢出 | java.lang.OutOfMemoryError: Metaspace |
- 动态加载类过多(如使用 CGLIB、JSP 动态生成类) - 类无法卸载(如 ClassLoader 泄漏) |
-XX:MetaspaceSize / -XX:MaxMetaspaceSize |
直接内存溢出 | java.lang.OutOfMemoryError: Direct buffer memory |
- 使用 ByteBuffer.allocateDirect() 分配大量直接内存- Netty 等框架默认使用直接内存 |
-XX:MaxDirectMemorySize |
GC 开销过高 | java.lang.OutOfMemoryError: GC overhead limit exceeded |
- JVM 花费 >98% 的时间 GC 但回收 <2% 的内存,认为进入“GC 死循环” | 分析 GC 日志、优化堆设置 |
类卸载失败(内存泄漏) | (不一定报错,但可能导致 Metaspace OOM) | - Web 容器频繁部署热更新 WAR 包时,ClassLoader 无法卸载,导致类永久驻留 | 优化 ClassLoader 管理,使用内存分析工具 |
-
堆快照(堆转储)文件分析
参数 | 说明 |
---|---|
-XX:+HeapDumpOnOutOfMemoryError |
OOM 时生成堆转储文件 ,建议生产环境开启 |
-XX:HeapDumpPath=xxx.hprof |
指定堆转储文件保存路径 默认保存在当前目录 |
-
常用分析工具与命令
工具/命令 | 简介与功能 | 使用说明 |
---|---|---|
MAT(Memory Analyzer Tool) | Eclipse 出品的强大图形工具,支持泄漏分析、对象引用链分析、内存占用统计等 | 下载地址: https://www.eclipse.org/mat/ 打开后直接导入 .hprof 文件即可分析 |
VisualVM | 官方可视化 JVM 监控工具,支持实时分析、GC 查看、线程状态与堆转储查看 | 附带于 JDK(或单独安装),打开 .hprof 文件进行可视化分析 |
JProfiler | 商业级 Java 性能分析工具,提供堆分析、CPU 分析、线程分析等全套功能 | 支持打开 .hprof 文件,也可在运行时配合使用(需付费或试用) |
YourKit | 商业性能分析工具,界面友好,支持丰富的分析功能 | 支持堆分析,适合内存泄漏排查 |
jhat(过时) | JDK 附带的旧工具,用于分析 .hprof 文件,开启 Web 界面查看(已废弃) |
命令:jhat heapdump.hprof ,浏览器访问 http://localhost:7000 (JDK 8 及以下) |
jmap | 用于生成堆转储文件或查看堆对象统计信息(不是分析工具本身) | 命令:jmap -dump:format=b,file=heap.hprof <pid> 生成转储;配合 MAT 使用 |
jcmd | 更现代的诊断命令工具,可生成 heap dump、执行 GC、打印 VM 状态等 | 命令:jcmd <pid> GC.heap_dump heap.hprof |
GCEasy.io | 在线分析工具,支持 .hprof 文件和 GC 日志上传分析 |
网站:https://gceasy.io |
-
GC日志相关参数(JDK1.8及以下)
参数 | 说明 |
---|---|
-XX:+PrintGC |
打印基本 GC 信息(建议配合 -XX:+PrintGCDetails 使用) |
-XX:+PrintGCDetails |
打印详细的 GC 日志信息(如各区域使用情况、对象分配、晋升等) |
-XX:+PrintGCDateStamps |
在 GC 日志中添加日期时间戳 |
-XX:+PrintGCTimeStamps |
在 GC 日志中添加 JVM 启动以来的时间戳 |
-Xloggc:/var/log/myapp/gc.log |
将 GC 日志输出到指定文件 |
-XX:+UseGCLogFileRotation |
启用 GC 日志文件轮转(适用于大规模系统的 GC 日志管理) |
-XX:NumberOfGCLogFiles=5 |
最多保留 5 个 GC 日志历史文件 |
-XX:GCLogFileSize=10M |
每个 GC 日志文件最大为 10MB |
-XX:+PrintGCCause |
打印 GC 的触发原因(如 Minor GC、System.gc() 等) |
-
GC日志相关参数(JDK1.9+)
1 | -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20M |
部分 | 含义 | 说明 |
---|---|---|
gc* |
日志类别 | 表示记录所有 GC 相关日志(如 gc、gc+start、gc+heap 等)。* 是通配符。 |
file=/var/log/app/gc.log |
输出路径 | 将 GC 日志输出到指定路径,而不是控制台。可以是相对或绝对路径。 |
time |
日志时间戳 | 添加格式化时间(如 2025-05-12T10:21:33.123+0800)。便于定位具体时间。 |
uptime |
JVM 启动以来的时间戳 | 显示 GC 发生时距离 JVM 启动的毫秒数(例如 3.254s )。有助于调试启动期间的问题。 |
level |
日志级别 | 显示日志级别(info、debug、warning 等),默认 info。 |
tags |
日志标签 | 显示日志的模块标签,如 [gc,start] ,帮助快速定位类别。 |
filecount=5 |
文件轮转个数 | 最多保留 5 个日志文件(当前日志 + 4 个历史)。 |
filesize=20M |
单文件最大大小 | 每个日志文件最大 20MB,超出时自动滚动到下一个文件。 |