再有人问你对GC怎么理解,就这样答

69 阅读10分钟

GC主要是为了解决人为开辟的内存忘记释放的内存泄漏的问题,JVM垃圾回收机制是Java应用稳定运行的重要保障。说GC之前得先了解jvm的内存管理

4.1. 内存结构:JVM的内存布局

JVM的内存主要划分为以下几个部分:

  • 堆(Heap) :存放对象实例,是垃圾回收的主要区域。
  • 方法区(Method Area) :存储类信息、常量、静态变量。
  • 虚拟机栈(Stack) :方法调用的生命周期内使用,存放局部变量表、操作数栈、动态连接、方法出口等信息。
  • 本地方法栈(Native Method Stack) :用于支持Native方法。
  • 程序计数器(Program Counter) :当前线程所执行的字节码的行号指示器记录,即当前线程执行的位置。

4.2. GC算法:内存回收的智慧

  1. 标记-清除算法
    • 工作原理:标记无用对象,清除回收空间。
    • 缺点:内存碎片化严重。
  1. 复制算法
    • 适用场景:新生代内存回收。
    • 优势:高效,减少碎片。
  1. 标记-整理算法
    • 工作原理:标记后,存活对象向前移动,清理边界外内存。
    • 适用场景:老年代回收,避免碎片。
  1. 分代收集算法
    • 策略:根据对象存活周期,新生代用复制,老年代用标记-整理。

4.3. 垃圾回收器:内存管理的执行者

  1. Serial回收器
    • 特点:单线程,简单高效,适合单核CPU。
  1. ParNew回收器
    • 特点:新生代多线程回收,STW。
  1. Parallel回收器
    • 特点:多线程,新生代和老年代并行回收。
  1. CMS(Concurrent Mark Sweep)回收器
    • 特点:并发标记,减少停顿,适合高并发场景。
  1. G1(Garbage-First)回收器
    • 特点:分区回收,减少停顿,适合大内存应用。
年轻代(Young) GC算法年老代(Tenured) GC算法对应JVM选项
Incremental(增量GC)Incremental-Xinqgc
SerialSerial-XX:+UseSerialGC
Parallel ScavengeSerial-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel NewSerialN/A
SerialParallel OldN/A
Parallel ScavengeParallel Old-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel NewParallel OldN/A
SerialCMS-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
Parallel ScavengeCMSN/A
Parallel NewCMS-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1-XX:+UseG1GC

主要组合:

年轻代和老年代的串行 GC(Serial GC) 

年轻代和老年代的并行 GC(Parallel GC) 

年轻代的并行 GC(Parallel New) + 老年代的 CMS(Concurrent Mark and Sweep) 

G1, 负责回收年轻代和老年代

查询当前使用的垃圾回收器命令:java -XX:+PrintCommandLineFlags -version

4.3.1. CMS回收过程

整个过程分为4个步骤,包括:

1.初始标记:仅标记一下GC Roots能直接关联到的对象,会STW;

2.并发标记: 从GCRoots的直接关联对象开始遍历整个对象图的过程,耗时较长,但无需停顿用户线程,另外此阶段会将对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;

3.重新标记:修正并发标记期间被用户操作改变的对象标记,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,会STW

4.并发清除:并发处理标记的对象

5.重置:重置线程,为下一次GC做准备

4.3.2. G1回收过程

G1 (Garbage-First) 是一款面向服务端的垃圾收集器,同时注重吞吐量(Throughput) 和低延迟(Low latency) ,可以指定: 在任意 xx 毫秒的时间范围内, STW 停顿不得超过 x 毫 

秒。

整体上是标记+整理算法,两个区域之间是使用复制算法

G1会将堆划分为多个大小相等的区域Region(每个区都可以根据需要,扮演新生代的Eden区、Survivor区或老年代空间),收集器能够根据扮演不同角色的Region采用不同的策略去处理,

逻辑上, 所有的 Eden 区和 Survivor 区合起来就是年轻代, 所有的 Old 区拼在一起那就是老年代。G1中还有一种特殊的区域,叫Humongous Region,专门存放短期存在的巨型对象。

G1收集器之所以能建立可预测的停顿时间模型,是因为它将Regin作为单次回收的最小单元,可以有计划地避免在整个Java堆中进行全区域的垃圾收集,同时G1收集器跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,保证在有限的时间内尽获取可能高的收集效率。

开启G1垃圾回收器的参数:- XX: +UseG1GC 在JDK9之前需要手动启用G1回收

主要有四个步骤:

1.初始标记:仅标记一下GC Roots能直接关联到的对象,并修改TAMS指针的值,会STW但很短

2.并发标记:从GC Root开始对堆中的对象进行可达性分析,递归扫描整个堆中的对象图,扫描结束后,还需重新处理SATB(原始快照)记录下的在并发时有引用变动的对象

3.最终标记:处理并发阶段结束后仍遗留下来的少量SATB记录,会STW

4.筛选回收:更新Region统计数据,对各个Region的回收价值和成本进行排序,根据用户设置的停顿时间(默认200毫秒)制定回收计划,将需要回收的Region中的存活对象复制到空Region中,并清除旧的Region,会STW

优点:

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间

分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果

空间整合:G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的

可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型

4.4. 垃圾回收过程

一般堆内存可以分为新生代,老年代,永久代(java8以后废弃)

4.4.1. Eden 新生代

Eden 是内存中的一个区域, 用来分配新创建的对象,通常会有多个线程同时创建多个对象,线程本地分配缓冲区(Thread Local Allocation Buffer, 简称 TLAB),通过这种缓冲区划分,大部分对象直接由 JVM 在对应线程的 TLAB 中分配, 避免与其他线程的同步操作

如果 TLAB 中没有足够的内存空间, 就会在共享 Eden 区(shared Eden space)之中分配。

如果共享 Eden 区也没有足 够的空间, 就会触发一次 年轻代 GC 来释放内存空间。

如果 GC之后 Eden 区依然没有足够的空闲内存区域, 则对象 

就会被分配到老年代空间(Old Generation)

当 Eden 区进行垃圾收集时, GC将所有从 root 可达的对象过一遍, 并标记为存活对象。标记阶段完成后, Eden 中所有存活的对象都会被复制到存活区(Survivor spaces)里面。整个 Eden 区就可以被认为是空的, 然后就能用来分配新对象。这种方法称为 “标记-复制”(Mark and Copy): 存活的对象被标记, 然后复制到一个存活区(注意,是复制,而不是移动)。

4.4.2. Survivor Spaces 存活区

存活的对象会在两个存活区(survivor1、survivor2)之间复制多次, 直到某些对象的存活 时间达到一定的阀值,具体的提升阈值由 JVM 动态调整,但也可以用参数 ‐XX:+MaxTenuringThreshold 来指定上限。如果设置 ‐ 

XX:+MaxTenuringThreshold=0 , 则 GC时存活对象不在存活区之间复制,直接提升到老年代。现代 JVM 中这个阈值默认设置为 15 个 GC周期。

如果存活区空间不够存放年轻代中的存活对象,提升(Promotion)也可能更早地进行

4.4.3. 老年代(Old Generation)

老年代内存空间通常会更大,里面的对象是垃圾的概率也更小,因为预期老年代中的对象大部分是存活的, 所以不再使用标记和复制 

(Mark and Copy)算法。而是采用移动对象的方式来实现最小化内存碎片

4.4.4. 永久代(Permanent Generation)

在 Java 8 之前有一个特殊的空间,称为“永久代”,存储元数据(metadata)的地方,比如 

class 信息等。因为很难去计算这块区域到底需要占用多少内存空间。预测失败导致的结果就是 

产生 java.lang.OutOfMemoryError: Permgen space 这种形式的错误

4.4.5. 元数据Metaspace

估算元数据所需空间非常复杂, Java 8 直接删除了永久代,改用 Metaspace。从此以 

后, Java 中很多杂七杂八的东西都放置到普通的堆内存里。元数据区位于本地内存(native memory), 不再影响到普通的 Java 对象。默认情况下, Metaspace 的大小只受限于 Java 进程可用的本地内存。

可以通过下面这样的方式来限制 Metaspace 的大小, 如 256 MB: 

‐XX:MaxMetaspaceSize=256m

4.5. GC优化:性能调优的关键

  1. 优化目标
    • 降低停顿时间:提升用户体验。
    • 减少GC频率:降低资源消耗。
    • 提升吞吐量:保证系统高性能。
  1. 优化策略
    • 选择合适的回收器:根据应用场景选择。
    • 调整堆内存大小:合理设置Heap大小。
    • 避免内存泄漏:及时释放不再使用的对象。
    • 使用对象池:复用对象,减少GC压力。
    • 减少对象创建频率:降低GC负担。
  1. 查看GC日志

Serial GC

XX:+PrintGCDetailsXX:+PrintGCDateStampsXX:+PrintGCTimeStampsXloggc:<filename>

Parallel GC

XX:+UseParallelGCXX:+UseParallelOldGC

CMS

XX:+UseConcMarkSweepGC

4.6. 实践案例:GC调优实例

案例背景
某Java应用在高并发场景下,用户反馈响应时间变长,怀疑GC问题。
分析过程

  1. 观察GC日志:发现Full GC频繁,每次停顿约2秒。
  2. 检查堆内存设置:发现Heap设置过小,导致Full GC频繁。
  3. 调整参数:增大Heap大小,同时启用G1回收器。
  4. 结果:Full GC次数减少,停顿时间降低至500ms以内,系统稳定性提升。
    调优参数示例
-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200

4.7. GC技术的发展

  1. JVM垃圾回收器的演进
    • ZGC和Shenandoah:实时垃圾回收器,解决长期存在的停顿问题。
    • Rust语言:通过所有权模型,从语言层面避免GC的必要。
  1. 发展趋势
    • 低延迟:实时垃圾回收成为主流。
    • 高吞吐量:提升GC效率,减少资源消耗。
    • 智能化:自适应调节,减少人工调优需求。