JVM堆内存垃圾回收机制详解(Java 8)

目录

基础知识介绍

1.8的变化

移除永久代的原因

分代概念

分代的原因

survivor分为两块相等大小的幸存空间的原因

JVM堆内存常用参数

[垃圾回收算法(GC,Garbage Collection)](#垃圾回收算法(GC,Garbage Collection))

垃圾收集器

[1. Serial 收集器](#1. Serial 收集器)

[2. Parallel 收集器(也称 Throughput 收集器)](#2. Parallel 收集器(也称 Throughput 收集器))

[3. CMS(Concurrent Mark Sweep)收集器](#3. CMS(Concurrent Mark Sweep)收集器)

[4. G1(Garbage First)收集器](#4. G1(Garbage First)收集器)

[5. ZGC 和 Shenandoah](#5. ZGC 和 Shenandoah)

垃圾收集器参数

出现堆内存溢出的原因

内存与垃圾回收配置解析

G1垃圾回收器优化参数

线程与触发机制

日志记录配置


基础知识介绍

Java堆内存管理是程序性能优化的核心环节,堆内存溢出问题在Java开发中频繁出现。深入分析此类故障的前提在于透彻理解Java堆内存的运作机制,通过下图可以清晰地展示Java堆内存的具体划分方式:

  • JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
  • 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
  • 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
  • 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

1.8的变化

JDK 1.8 移除了永久代(PermGen),改为采用元空间(Metaspace)作为方法区的实现。元空间与永久代的核心区别在于存储位置:元空间不再占用 JVM 堆内存,而是直接使用本地内存(Native Memory)。这一改动提升了内存管理的灵活性,减少了因永久代大小限制导致的 OOM 问题。

Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间

Young Generation Space 新生区 Young/New

又被划分为Eden区和Survivor区

Tenure generation space 养老区 Old/Tenure

Meta Space 元空间 Meta

在配置元空间时,需重点关注以下两个参数:

  • MetaspaceSize :初始化元空间大小,控制发生GC阈值;
  • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存。

移除永久代的原因

HotSpot JVM与JRockit VM的技术融合过程中,一个关键改进是移除了永久代设计。JRockit VM原本就不存在永久代结构,采用元空间(Metaspace)作为替代方案后,有效解决了永久代内存溢出(OOM)问题。这一架构变更提升了内存管理的灵活性和可靠性。

分代概念

新生成的对象首先放到年轻代Eden区 ,当Eden区 空间满了,程序仍需要创建对象,触发Minor GC ,JVM的垃圾回收器将对Eden区 进行垃圾回收(Minor GC/YGC ),将Eden区 中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到Eden区 。存活下来的对象会被移动到Survivor0区Survivor0区 满后触发执行Minor GC ,此时上次幸存下来的放到Survivor0区 的,如果没有被回收,就会放到Survivor1区 ,如果再次触发执行Minor GC ,此时幸存的对象会重新放回Survivor0区 ,接着再去Survivor1区 。这样就能保证一段时间内总有一个survivor区 为空,经过多次Minor GC仍然存活的对象就将其移动到老年代。

老年代存储长期存活的对象,占满时会触发Major GC/Full GC ,GC期间会停止所有线程等待GC完成(STW ),所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

Java中Stop-The-World 机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。

  • Minor GC :清理年轻代;
  • Major GC :清理老年代;
  • Full GC :清理整个堆空间,包括年轻代和永久代。

注:所有GC都会停止应用所有线程。

分代的原因

将对象按照存活概率划分为不同类别,存活周期较长的对象分配至固定区域存储。这种分类策略可以有效降低垃圾扫描时间并减少GC触发频率。针对不同分区的对象特性采用差异化的垃圾回收算法,充分发挥各算法的优势并规避其局限性。

survivor分为两块相等大小的幸存空间的原因

内存碎片化问题会导致可用内存空间分散不连续。当程序需要分配较大连续内存块存放新对象时,可能因现有空闲内存均为碎片化的小块而无法满足需求。这种情况下将强制触发垃圾回收机制,来试图整理合并内存碎片以获得足够连续空间。

JVM堆内存常用参数

|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 参数 | 描述 |
| -Xms | 堆内存初始大小,单位m、g |
| -Xmx(MaxHeapSize) | 堆内存最大允许大小,一般不要大于物理内存的80% |
| -Xmn | 设置新生代的大小(初始值及最大值),通常默认即可 |
| -XX:PermSize | 非堆内存初始大小,一般应用设置初始化200m,最大1024m就够 |
| -XX:MaxPermSize | 非堆内存最大允许大小 |
| -XX:NewSize(-Xns) | 年轻代内存初始大小 |
| -XX:MaxNewSize(-Xmn) | 年轻代内存最大允许大小,也可以缩写 |
| -XX:NewRatio | 配置新生代与老年代在堆结构的占比。赋的值即为老年代的占比,剩下的1给新生代 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3 -XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5 |
| -XX:SurvivorRatio=8 | 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1 |
| -XX:MaxTenuringThreshold | 设置新生代垃圾的最大年龄,超过此值,仍未被回收的话,则进入老年代 默认值为15 -XX:MaxTenuringThreshold=0:表示年轻代对象不经过Survivor区,直接进入老年代,对于老年代比较多的应用,可以提高效率 如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率 |
| -XX:HandlePromotionFailure | 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次Minor GC是安全的;如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败; 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC |
| -XX:PretenureSizeThreshold | 可以把它的值设置为字节数,比如"1048576"字节,就是1MB 意思就是如果你要创建一个大于这个大小的对象,比如一个超大的数组,或者是别的对象,此时就直接把这个大对象放到老年代里去,压根不会经过新生代 |
| -XX:+PrintFlagsFinal | 查看所有的参数的最终值(可能会存在修改,不再是初始值) 具体查看某个参数的指令: jps:查看当前运行中的进程 jinfo -flag SurvivorRatio 进程id |
| -XX:+PrintGCDetails | 输出详细的GC处理日志 |

垃圾回收算法(GC,Garbage Collection)

红色是标记的非活动对象,绿色是活动对象。

  • 标记-清除(Mark-Sweep)


    该算法采用两阶段处理流程。第一阶段垃圾收集器会遍历所有的对象图,从根对象(如栈上的局部变量、静态字段等)开始,访问所有可达的对象,并将它们标记为存活对象;第二阶段垃圾收集器会对堆内存进行一次遍历,回收那些未被标记为存活的对象所占用的空间,也就是实际上执行删除操作的对象是那些没有被标记的,即被视为垃圾的对象。该机制执行后会产生内存碎片问题,随着碎片数量增加,可能导致大对象分配失败,进而频繁触发垃圾回收操作。

  • 复制(Copy)


    复制算法采用分块策略,将可用内存划分为两个容量相等的区域。系统运行时仅使用其中一个内存区域,当该区域空间耗尽时,会扫描并复制所有存活对象至另一个空闲区域,随后立即清空原区域的全部内存空间。

    这种设计实现了每次只回收半个内存区域的高效操作,同时完全避免了内存碎片的产生。其优势在于算法实现简单且回收效率高,但代价是需要牺牲双倍的内存空间来维持这种复制机制。

  • 标记-整理(Mark-Compact)


    该算法内存回收过程也分为两个阶段。第一阶段识别并标记所有可回收对象,第二阶段将存活对象向内存空间一端移动,最后清理边界外的废弃内存区域。这种设计有效解决了标记-清除算法产生的内存碎片问题,同时规避了复制算法对额外内存空间的需求。

  • 分代收集


    年轻代 内存区域,垃圾回收后通常只有少量对象存活,采用复制算法 更为高效,仅需复制少量存活对象即可完成回收。
    老年代 内存区域由于对象存活率较高且缺乏额外内存空间,更适合采用标记-清除标记-整理算法进行回收处理。

垃圾收集器

1. Serial 收集器

  • 工作原理:Serial 是一个单线程的收集器,它在进行垃圾收集时会"stop-the-world",即暂停所有应用程序的线程。
  • 适用场景:适合于数据量较小、对停顿时间要求不高的客户端应用。
  • 优点:简单高效,在单 CPU 环境下表现良好。
  • 缺点:由于是单线程,对于多核处理器来说效率较低,并且会造成较长时间的停顿。

2. Parallel 收集器(也称 Throughput 收集器)

  • 工作原理:与 Serial 类似,但 Parallel 使用多个线程并行执行垃圾收集过程。同样会暂停应用程序的所有其他线程。
  • 适用场景:适用于后台运算且对响应时间不太敏感的应用程序。
  • 优点:通过增加线程数可以显著提高垃圾回收的吞吐量。
  • 缺点:虽然提高了吞吐量,但由于其"stop-the-world"的特性,会导致较大的停顿时间。

3. CMS(Concurrent Mark Sweep)收集器

  • 工作原理 :CMS收集器旨在减少停顿时间,尽可能地并发执行大部分垃圾收集的工作,仅在标记初始状态和做一些小的修正时需要短暂的"stop-the-world"。它基于标记-清除算法实现,整个过程分为4个步骤,包括:
    • 初始标记(Initial Mark)标记所有从GC Roots 直接可达的对象。该阶段需要短暂暂停用户线程,只标记起点对象,速度快。
    • 并发标记(Concurrent Mark)从 GC Roots 出发,并发地遍历整个对象图,标记所有存活对象。该阶段允许用户线程和 GC 线程同时运行,耗时较长,但不需要停顿,是 CMS 的核心阶段。
    • 重新标记(Remark)修正并发标记期间因用户线程运行而导致的标记变动。该阶段会暂停用户线程,但时间较短。使用增量更新(Incremental Update)或原始快照(Snapshot-At-Beginning, SATB)技术来记录并发期间对象引用的变化。
    • 并发清除(Concurrent Sweep)清除未被标记的对象(即垃圾对象)。该阶段与用户线程并发执行,不会造成停顿,但会产生内存碎片。
  • 适用场景:对于需要快速响应的应用程序非常有用,如 Web 应用。
  • 优点:减少了应用程序的停顿时间。
  • 缺点:占用更多的 CPU 资源;可能会出现浮动垃圾(Floating Garbage),即在 CMS 运行期间产生的垃圾无法立即被清理;此外,CMS 在堆内存不足的情况下会触发 Full GC,这时会导致较长的停顿。

4. G1(Garbage First)收集器

  • 工作原理 :G1(Garbage First)收集器采用区域化(Region)的堆内存划分方式,将堆划分为多个大小相等的独立区域。其核心优势在于可预测的暂停时间,避免传统垃圾收集器进行全区堆回收的开销。G1 通过动态评估每个 Region 的回收价值(包括可释放空间大小及回收所需时间),在后台维护一个优先级列表。在垃圾回收过程中,G1 根据预设的停顿时间目标,优先选择回收价值最高的 Region 进行清理。这种策略确保在有限时间内最大化垃圾回收效率,同时满足低延迟的应用需求。它基于标记-整理算法实现,整个过程分为4个步骤,包括:
    • **初始标记(Initial Mark)**GC Roots直接关联到的对象会被快速标记,该阶段与CMS算法类似,需要短暂暂停用户线程。
    • **并发标记(Concurrent Mark)**从GC Roots出发遍历对象图,标记所有存活对象。此阶段允许与用户线程并发执行,避免长时间停顿,但由于对象引用关系可能动态变化,标记结果需后续修正。
    • **最终标记(Final Mark)**修正并发标记期间因用户线程运行导致的标记变动,通过增量更新或原始快照(SATB)机制确保标记准确性,需短暂停顿用户线程。
    • **筛选回收(Live Data Counting and Evacuation)**基于回收效益(如Region存活对象比例、回收耗时等)对各Region排序,结合用户设定的期望停顿时间,选择收益最高的Region进行回收。
  • 适用场景:适用于需要大容量内存和高吞吐量的应用程序。
  • 优点:可预测的停顿时间模型,允许用户指定停顿时间的目标;避免了全堆扫描,提高了垃圾回收效率。
  • 缺点:配置复杂度较高;在某些情况下,可能不如 CMS 高效。

5. ZGC 和 Shenandoah

  • 工作原理:这两者都是低延迟垃圾收集器。它们的设计目标是在不影响应用程序正常运行的前提下完成垃圾回收。两者都实现了几乎完全并发的垃圾回收过程,极大地减少了"stop-the-world"的时间。
  • 适用场景:适用于那些对延迟有严格要求的应用程序。
  • 优点:提供了非常低的停顿时间,甚至可以达到毫秒级别。
  • 缺点:目前这些收集器还在不断发展和完善中,可能不是所有平台都支持。

垃圾收集器参数

参数 描述
-XX:+UseSerialGC 串行收集器
-XX:+UseParallelGC 并行收集器
-XX:+UseParallelGCThreads=8 并行收集器线程数,同时有多少个线程进行垃圾回收,一般与CPU数量相等
-XX:+UseParallelOldGC 指定老年代为并行收集
-XX:+UseConcMarkSweepGC CMS收集器(并发收集器)
-XX:+UseCMSCompactAtFullCollection 开启内存空间压缩和整理,防止过多内存碎片
-XX:CMSFullGCsBeforeCompaction=0 表示多少次Full GC后开始压缩和整理,0表示每次Full GC后立即执行压缩和整理
-XX:CMSInitiatingOccupancyFraction=80% 表示老年代内存空间使用80%时开始执行CMS收集,防止过多的Full GC
-XX:+UseG1GC G1收集器
-XX:MaxTenuringThreshold=0 在年轻代经过几次GC后还存活,就进入老年代,0表示直接进入老年代

出现堆内存溢出的原因

在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)。

OOM(Out of Memory)异常常见有以下几个原因:

  1. 老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
  2. 永久代内存不足:java.lang.OutOfMemoryError:PermGenspace
  3. 代码bug,占用内存无法及时回收。

OOM在这几个内存区都有可能出现,实际遇到OOM时,能根据异常信息定位到哪个区的内存溢出。

可以通过添加参数**-XX:+HeapDumpOnOutMemoryError** ,让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。

熟悉了JAVA内存管理机制及配置参数,下面是对JAVA应用启动选项调优配置:

JAVA_OPTS="-server -Xms512m -Xmx2g -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=4 -XX:ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:../logs/gc.log"

内存与垃圾回收配置解析

复制代码
-server

启用服务器模式,优化长时间运行的性能,适用于后端服务场景。

复制代码
-Xms512m -Xmx2g

初始堆内存设置为512MB,最大堆内存设置为2GB。避免堆动态调整的开销,建议生产环境将两者设为相同值。

复制代码
-XX:+UseG1GC

启用G1垃圾收集器,适合大堆内存和低延迟要求的应用,取代了传统的CMS收集器。

G1垃圾回收器优化参数

复制代码
-XX:SurvivorRatio=6

新生代Eden区与Survivor区的比例为6:1,影响对象晋升老年代的速度。较高的值可能减少短期存活对象进入老年代的概率。

复制代码
-XX:MaxGCPauseMillis=400

目标最大GC暂停时间为400毫秒。G1会尝试调整堆分区以达到该目标,但实际效果取决于应用特性。

复制代码
-XX:G1ReservePercent=15

保留15%的堆作为空闲空间,防止晋升失败。该空间在内存不足时作为应急使用。

线程与触发机制

复制代码
-XX:ParallelGCThreads=4

设置并行GC阶段的线程数为4,适用于多核CPU环境。通常建议设为CPU核心数的1/4至1/2。

复制代码
-XX:ConcGCThreads=1

并发标记阶段使用1个线程,减少对应用线程的干扰。该值过大会增加CPU开销。

复制代码
-XX:InitiatingHeapOccupancyPercent=40

当堆使用率达到40%时触发并发标记周期。较低的值能给GC更多时间完成标记,避免Full GC。

日志记录配置

复制代码
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:../logs/gc.log

记录详细的GC日志,包含时间戳和统计信息,输出到指定路径的gc.log文件。需确保日志目录存在且可写。

相关推荐
吃饭只吃七分饱24 分钟前
C++ primer知识点总结
java·开发语言·c++
半部论语1 小时前
Spring Boot 一个注解搞定「加密 + 解密 + 签名 + 验签」
java·spring boot
2301_803554521 小时前
C++中的detach
java·开发语言·jvm
源码_V_saaskw2 小时前
JAVA国际版任务悬赏+接单系统源码支持IOS+Android+H5
android·java·开发语言·javascript·微信小程序
双力臂4043 小时前
Java IO流体系详解:字节流、字符流与NIO/BIO对比及文件拷贝实践
java·开发语言·nio
钮钴禄·爱因斯晨4 小时前
Java API (二):从 Object 类到正则表达式的核心详解
java·开发语言·信息可视化·正则表达式
Monkey-旭4 小时前
Android 蓝牙通讯全解析:从基础到实战
android·java·microsoft·蓝牙通讯
BoneToBone5 小时前
java list 与set 集合的迭代器在进行元素操作时出现数据混乱问题及原因
java·开发语言·list
WanderInk6 小时前
深入解析:Java Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); 一行代码的背后功力
java·后端·算法
O执O6 小时前
JavaWeb笔记四
java·hive·hadoop·笔记·web