什么是JVM?JVM的作用?

是Java程序的核心运行环境,负责将Java字节码(.class文件)解释或编译为底层操作系统和硬件能理解的指令。JVM是Java实现“一次编写,到处运行”(跨平台)的核心机制。

JVM的运行时数据区域

在这里插入图片描述

区域名称线程共享性存储内容是否内存溢出(OutOfMemoryError)是否栈溢出(StackOverflowError)其他特点
程序计数器线程私有当前线程执行的字节码指令地址唯一无OOME的区域
虚拟机栈线程私有栈帧(局部变量表、操作数栈、动态链接、方法返回地址)是 。不停创建线程是。无限递归每个方法对应一个栈帧;深度超出限制时抛出SOE。
本地方法栈线程私有Native方法的调用信息是 。同上是。同上。与虚拟机栈类似,但服务于Native方法(如C/C++代码)。
线程共享对象实例、数组(通过new创建的对象)是。无限创建对象GC主要区域;通过-Xms-Xmx配置大小。
方法区(元空间)线程共享类信息、运行时常量池、静态变量、即时编译后的代码JDK8后改为元空间(Metaspace),使用本地内存;通过-XX:MetaspaceSize配置。
运行时常量池线程共享字面量(如字符串常量)、符号引用(类/方法/字段的引用)是(属于方法区)。String.intern() 驻留字符串动态扩展(如String.intern()方法添加字符串)。
直接内存线程共享通过DirectByteBuffer分配的堆外内存(用于NIO操作)是 。分配 DirectByteBuffer不属于JVM运行时数据区,但受-XX:MaxDirectMemorySize限制。

StackOverflowError和OutOfMemoryError

  1. StackOverflowError
    触发原因:由线程的栈深度超过JVM允许的最大限制引起,通常由无限递归或过深的方法调用导致。
    发生区域:虚拟机栈(线程私有)。每个方法调用会在栈中压入栈帧,栈帧过多导致栈空间耗尽。

  2. OutOfMemoryError
    触发原因:JVM无法分配足够的内存空间,可能发生在堆、方法区,或线程创建时无法分配栈内存。
    发生区域
    :对象实例分配失败(最常见)。
    方法区:加载过多类或常量(如Java 8的Metaspace)。
    虚拟机栈:线程创建时无法分配足够的栈内存(如线程过多或单个栈设置过大)。

堆内存中分为哪些区域?

因为大多数对象“朝生夕死”,JVM 堆内存通过分代设计,根据对象的存活时间将分为两块堆内存区域“新生代”和“老年代”。
这样JVM对新生代和老年代的对象使用不同的垃圾回收策略,以此优化垃圾回收效率:
新生代:高频 Minor GC 快速回收短期对象。
老年代:低频 Major GC 处理长期存活对象。

区域描述生命周期垃圾回收机制触发 GC 的条件
新生代存放新创建的对象,绝大多数对象在此区域被快速回收。短(对象存活时间短)Minor GC(复制算法)新生代空间不足时触发。
Eden 区对象初次分配的区域,占新生代的绝大部分空间(默认 80%)。极短(多数对象用完即死)回收后存活对象复制到 Survivor 区。Eden 区满时触发。
Survivor 区分为 FromTo 区,存放从 Eden 区或另一 Survivor 区存活的对象。中等(需通过多次 Minor GC 存活)对象在 Survivor 区之间复制,年龄增长。当 Survivor 区不足以存放存活对象时触发。
老年代存放长期存活的对象(如缓存、全局变量),或大对象(直接分配)。长(对象存活时间久)Major GC / Full GC(标记-清除/整理算法)老年代空间不足时触发。
元空间(Metaspace)(注意:Java 8 后不属于堆内存,而是本地内存) 存储类元数据、方法信息等。长(与类生命周期一致)无显式 GC,依赖 JVM 自动管理。元空间内存不足时触发 Full GC。

对象的分配规则和触发垃圾回收条件

在这里插入图片描述
Eden与Survivor区默认8:1:1
新生代的高频垃圾回收叫做Minor GC。
老年代的低频垃圾回收叫做Major GC。

对象分配规则

  1. 先判断对象是否无逃逸,如果无逃逸而且栈空间够用,则直接在栈上分配
  2. 判断是否为大对象,如果是直接分配到老年代。
  3. 判断是否有线程本地分配缓冲区(TLAB),如果有的话,则直接在Eden区分配内存。如果没有则要通过CAS的方式在Eden区分配内存

对象逃逸分析和标量替换
逃逸分析:如果对象仅在当前方法内部使用,未传递到外部,,JVM 直接在栈帧中分配内存,随栈帧销毁自动回收,无需 GC。
标量调换:在确定对象无逃逸后,JVM 将对象拆解为独立的标量(基本类型或引用),直接分配在栈或寄存器中,避免创建完整的对象。优点:1.无需连续空间。2. 消除对象头内存占用

线程本地分配缓冲区(TLAB)?
线程本地分配缓冲区(Thread-Local Allocation Buffer, TLAB)​​ 是 JVM 为每个线程在年轻代的 ​Eden 区​ 中分配的一块私有内存区域。
​核心目的:减少多线程环境下对象分配时的锁竞争,提升内存分配效率。
​实现方式:JVM 启动时,为每个线程在 Eden 区分配一个初始 TLAB(大小可配置)。线程优先在自己的 TLAB 中分配对象,无需加锁。
若 TLAB 剩余空间不足,触发一次 ​TLAB 分配失败,线程尝试加锁申请新的 TLAB。

触发minor gc的条件:Eden区满了。

对象进入老年代的四种情况:

  1. 年龄太大。 MinorGC的次数超过15次,该阈值可以由参数“-XX:MaxTenuringThreshold”指定;
  2. 动态年龄判断:MinorGC之后,发现Survivor区中的一批对象的总大小大于了这块Survivor区的50%,那么就会将此时大于等于这批对象年龄最大值的所有对象,直接进入老年代。比率可以由-XX:TargetsurvivorRatio指定
  3. 大对象直接进入老年代1M。大对象的阈值可以由参数-XX:PretenureSizeThreshold指定;(前提是Serial和ParNew收集器)。因为大对象在分配内存时的复制操作降低效率。
  4. MinorGC后存活对象太多无法放入Survivor。

动态年龄判断
触发条件:在Minor GC执行后,JVM会对Survivor区进行对象年龄分析‌。Survivor区中存活对象的总大小超过该区域内存的50%(由-XX:TargetSurvivorRatio参数指定,默认50%)‌
晋升规则:JVM会按年龄从小到大累加对象所占空间,直到总大小超过Survivor区的50%‌。例如:年龄1+年龄2+…+年龄n的对象总和超过阈值,则年龄≥n的对象均晋升。满足条件的对象直接进入老年代,无需达到预设的年龄阈值(如默认15次GC)‌。

触发major GC的条件:

  1. 老年代空间不足;两种情况:(1)年轻代晋升老年代时 (2)大对象直接分配到老年代时
  2. 分配担保失败; 执行Minor GC前,JVM会检查老年代剩余空间是否大于年轻代存活对象总大小(避免Minor GC后存活对象无法放入Survivor区)。若老年代剩余空间不足会直接触发Major GC。
  3. 达到垃圾收集器的特定触发条件:(1)CMS收集器:老年代使用率超过阈值(-XX:CMSInitiatingOccupancyFraction,默认92%)时触发收集。(2)G1收集器:根据最大暂停时间目标(-XX:MaxGCPauseMillis)动态调整老年代回收时机。
  4. 代码显式调用System.gc()

老年代分配担保机制(Handle Promotion)

分配担保机制是JVM垃圾回收(GC)中的一种内存管理策略,主要用于在Minor GC(年轻代回收)过程中,确保存活对象能安全晋升到老年代,避免因内存不足导致回收失败或直接触发Full GC。其核心思想是老年代为年轻代的内存分配提供“担保”
在JDK 6及之前,分配担保机制可以用参数-XX:-HandlePromotionFailure**开启,JDK 7及之后该参数已失效,JVM始终启用分配担保机制。

Minor GC发生前,JVM会检查以下两个条件,判断是否允许直接进行Minor GC:

  1. 老年代剩余连续空间 > 年轻代存活对象总大小
    • 如果成立,说明老年代有足够空间容纳晋升对象,Minor GC可安全执行。
  2. 老年代剩余连续空间 > 历次Minor GC后晋升到老年代对象的平均大小
    • 如果成立,即使本次存活对象略多,老年代也可能容纳(基于历史统计的乐观预测)。

若任一条件成立,JVM允许执行Minor GC;否则触发Full GC(回收整个堆,包括老年代和年轻代)。

如何判断对象可以被回收?

对象不在被任何地方引用,即代表对象可以被回收。
一般两种方法:

  1. 引用计数(因为无法解决循环引用的问题,一般不使用这种方法)
  2. 可达性分析

如何判断一个对象是否不再被引用?

JVM 垃圾回收机制使用可达性分析方式判断对象是否存活。 GC Roots 是可达性分析的根节点。所有从 GC Roots 出发通过引用链能访问到的对象都是存活对象,不可达的对象会被回收。
GC Root有哪些?

  1. ​虚拟机栈中的本地变量​
  2. 方法区中的静态变量
  3. ​JNI 全局引用的对象
  4. 系统类加载器加载的类
  5. ​Java 本地方法栈中的引用
  6. 活跃线程对象
  7. 被锁持有的对象

垃圾回收算法有哪些?

(1) 标记-清除(Mark-Sweep)
原理:1. 标记阶段:从 GC Roots 出发,标记所有可达对象。2. 清除阶段:遍历堆内存,回收未被标记的对象。
优点:实现简单,无需移动对象。
缺点:1、产生内存碎片,可能触发频繁 GC。 2、两次遍历(标记 + 清除),效率较低。

(2) 标记-复制(Mark-Copy)
原理

  1. 将内存分为两块(From 和 To)。
  2. 将 From 区的存活对象复制到 To 区,并清空 From 区。
  3. 交换 From 和 To 的角色。

优点
• 无内存碎片,适合年轻代(对象存活率低)。
• 只需遍历存活对象,效率高。
缺点
• 内存利用率低(默认浪费 50% 空间)。
• 复制大对象开销大。

(3) 标记-整理(Mark-Compact)

原理

  1. 标记阶段:标记所有存活对象。
  2. 整理阶段:将存活对象向内存一端移动,清理边界外的内存。

优点:无内存碎片,适合老年代
缺点:移动对象需更新引用地址,开销较大。

垃圾回收器有哪些?

serial收集器

  • 特点:单线程收集器
  • 收集过程:运行->新生代采用复制算法(单线程、STW)->运行->老年代使用标记整理算法(单线程、STW)->运行
  • 适用于client端java程序

ParNew

  • 新生代收集器、复制算法、多线程。
  • 其实是Serial收集器的多线程版本。
  • 收集过程:运行->新生代采用复制算法(多线程、STW)->运行

Parallel Scavenge

  • 新生代收集器、复制算法、多线程。
  • 特点是:可以通过参数指定吞吐量和停顿时间。
  • 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

Serial Old

  • 老年代、单线程、“标记整理”算法。
  • Serial Old是Serial收集器的老年代版本。

Parallel Old收集器

  • 老年代、多线程、“标记整理”算法
  • Parallel Scavenge收集器的老年代版本。

CMS收集器

  • 多线程、标记清除算法、老年代
  • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
  • 整个过程分为4步
  1. 初始标记(STW);标记GC Roots能直接关联的对象;
  2. 并发标记;进行GC Roots Tracing
  3. 重新标记(STW);修正并发标记期间因为用户程序继续运作而导致标记变动的那一部分对象的标记记录。
  4. 并发清除

** 缺点**

  1. CMS收集器对CPU资源非常敏感。并发阶段占用CPU资源会导致应用程序总吞吐量下降。
  2. CMS收集器无法处理浮动垃圾。在并发清理阶段,程序运行的垃圾无法清理。
  3. CMS基于“标记清除”算法。清除后会产生大量内存碎片,无法为大对象分配内存,不得不提前进行Full GC。为解决这个问题,CMS收集器提供了参数XX:+UseCMSCompactAtFullCollection和-XX:CMSFullGCsBeforeCompaction=N用于在FullGC时触发标记整理的回收方式。内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间会延长。

G1收集器

G1(Garbage-First)是 JDK 7 引入、JDK 9+ 默认的垃圾收集器,专为大内存(数十 GB 至 TB 级)和低延迟场景设计。其核心目标是平衡 吞吐量停顿时间,同时提供可预测的停顿模型。可用于新生代和老年代。

使用的算法:一般场景使用复制算法,Full GC时使用标记整理算法。

1. 核心设计思想

分 Region 内存布局
堆内存被划分为多个 固定大小的 Region(默认 2048 个 Region,大小通过 -XX:G1HeapRegionSize 设置,范围为 1MB~32MB)。
Region 类型
Eden(年轻代)
Survivor(存活区)
Old(老年代)
Humongous(巨型对象区,占用连续多个 Region)
动态分代:Region 可动态分配给不同代,无需物理隔离。
在这里插入图片描述

停顿时间可控
通过 -XX:MaxGCPauseMillis=N(默认 200ms)设定目标最大停顿时间,G1 优先回收垃圾比例高的 Region(Garbage-First 名称来源)。
MaxGCPauseMillis参数的性能权衡‌
设置过低:可能导致GC频率增加,降低整体吞吐量‌
设置过高:单次停顿时间延长,但GC次数减少‌

2. 工作流程
(1) Young GC(年轻代回收)

触发条件:Eden 区占满时触发。
过程

  1. 并行复制存活对象:将 Eden 和 Survivor 区的存活对象复制到新的 Survivor 区(或晋升到 Old 区)。
  2. 更新引用:更新老年代到年轻代的跨代引用(通过 Remembered Set 记录)。 参考资料:https://blog.csdn.net/bookssea/article/details/132448692
  3. 回收 Region:清空原 Eden 和 Survivor 区,标记为可分配。
(2) Mixed GC(混合回收)

触发条件
• 堆内存使用比例达到阈值(-XX:InitiatingHeapOccupancyPercent=N,默认 45%)。
• 并发标记完成后,回收包含最多垃圾的 Old 区和巨型对象区。
过程

  1. 并发标记
    初始标记(STW):标记 GC Roots 直接关联的对象。
    并发标记:与用户线程并发,标记全堆存活对象。
    最终标记(STW):处理剩余对象。
  2. 筛选回收(STW):选择高垃圾比例的 Region 回收(复制存活对象到空闲 Region)。
(3) Full GC

触发条件
对象分配失败就会进入Full GC。
对象分配的流程:TLAB分配 --> 扩展TLAB进行分配 --> 申请新的TLAB --> 从自由分区获取新的Region给新生代–> 堆内存扩展分区给新生代 --> 垃圾回收(YGC + MGC) --> FGC
• Mixed GC 回收速度跟不上对象分配速度。
• 内存碎片导致无法分配大对象(Humongous 分配失败)。
风险:G1 的 Full GC 退化为单线程的 Serial Old GC,停顿时间长,需尽量避免。


还有更新的垃圾收集器:ZGC、Shenandoah

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐