0%

JVM - 03 java如何进行垃圾回收

垃圾收集器需要处理的三个问题:
(1) 哪些对象需要回收?
(2) 什么时候回收垃圾对象?
(3) 如何回收垃圾对象?

1.哪些对象需要回收-发现垃圾对象

1.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是可被回收的。
引用计数算法(Reference Counting)的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,如Python语言使用了引用计数算法进行内存管理。
但是,Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。

Alt text

当ObjectA释放了对ObjectB的引用后,ObjectB的引用计数器即为0,此时可回收ObjectB所占用的内存。

1.2 根搜索算法

这个算法的基本思路就是通过一系列的名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言里,可作为GC Roots的对象包括下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  2. 方法区中的类静态属性引用的对象。
  3. 方法区中的常量引用的对象。
  4. 本地方法栈中JNI(即一般说的Native方法)的引用的对象。

Alt text
上图中,对象Object6、Object7、Object8虽然互相引用,但他们的GC Roots是不可到达的,所以它们将会被判定为是可回收的对象。

2.什么时候回收垃圾对象

GC的触发时机通常是内存空间不足时。

  • 不同的垃圾收集器有不同的触发策略,例如在Java中:
    新生代(Young Generation)的Eden区满时会触发Minor GC。
    老年代(Old Generation)空间不足时会触发Major GC或Full GC。

  • 现代的并发垃圾收集器(如CMS、G1、ZGC)还会在用户线程运行的同时进行标记和回收过程,以减少应用停顿时间(Stop The World, STW)。

2.1. 分代 (Generational GC)

Alt text

虚拟机中的共划分为三个代:新生代(Young Generation)、旧生代(Old Generation)和持久代(Permanent Generation)。其中持久代与GC关系不大。新生代和旧生代的划分是对垃圾收集影响比较大的。
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

2.1.1. 新生代(Young Generation)

所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。
新生代分三个区。一个Eden区,两个Survivor区(称为S0,S1)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个,这里我们假设为S1),当这个S1满时,此区的存活对象将被复制到S0,当这个S0也满了的时候,从S1复制过来的并且此时还存活的对象,将被复制到”旧生代”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象和从前一个Survivor复制过来的对象,而复制到旧生代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。

2.1.2. 旧生代(Old Generation/Tenuring Generation)

在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到旧生代中。因此,可以认为旧生代中存放的都是一些生命周期较长的对象。
数据进入旧生代的3个途径:

  1. 大数据直接进入Old区
  2. minorGC触发时,交换分区S0或者S1放不下
  3. 足够老的数据,在交换区拷贝次数超过了上限(XX:MaxTenuringThreshold=15)

2.1.3. 持久代(Permanent Generation)

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=和-XX:PermSize=进行设置。

2.2. 分代GC的类型

2.2.1. Minor GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发minor GC,对Eden区域进行GC, 清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对新生代的Eden区进行,不会影响到旧生代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Alt text

2.2.2. Full GC

对整个堆进行整理,包括新生代、旧生代和持久代。
Full GC因为需要对整个对进行回收,所以比minor GC要慢,因此应该尽可能减少Full GC的次数。
在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。
有如下原因可能导致Full GC:
(1) 旧生代被写满
(2) 持久代被写满
(3) System.gc()被显示调用
(4) CMS GC时出现promotion failed和concurrent mode failure
(5) 统计得到的Mionr GC晋升到旧生代的平均大小大于旧生代的剩余空间

2.3. 分代GC的步骤

Alt text

(1) 对象在Eden区完成内存分配;
(2) 如果当Eden区满了,再创建对象,就会因为在Eden区申请不到空间,触发minor GC,进行新生代(eden+1 Survivor)的垃圾回收;
(3) minor GC时,Eden不能被回收的对象被放入到空的survivor(Eden肯定会被清空),另一个survivor里不能被GC回收的对象也会被放入这个survivor,始终保证一个survivor是空的;
(4) 在第3步的时候,如果发现survivor满了,则这些对象被拷贝到旧生代,或者survivor并没有满,但是有些对象已经足够老(-XX:MaxTenuringThreshold=),也被放入旧生代 ;
(5) 当旧生代被放满的之后,就进行完整的垃圾回收Full GC;

2.4 分区垃圾回收 (Region-based GC)

分区回收是一种更现代的内存管理思想,由 G1、Shenandoah 和 ZGC 等收集器采用。它打破了传统的分代固定内存布局,将整个堆划分为大小相等的独立区域(Region)。

核心思想: 不再强调物理上连续的“新生代”和“老年代”大块区域。每个小区域可以独立地被标记为 Eden、Survivor、Old 或 Humongous(巨型对象区)。

工作原理: 收集器(如 G1)可以选择性地回收那些垃圾最多的区域(”Garbage-First”),而不是扫描整个代或整个堆。

优点:

  • 可预测的停顿时间: 通过每次只回收指定数量的区域,可以控制单次GC的停顿时间目标。
  • 避免碎片: 通常使用标记-复制或标记-整理算法在区域内部进行整理,有效避免内存碎片化问题。
  • 并发性: 现代分区收集器(G1, ZGC)引入了大量的并发标记阶段,减少了用户线程的停顿时间。

2.4.1 G1的分代模型

G1也分为年轻代和年老代,但不是固定划分,而是每个Region根据运行情况动态划分。

G1还有一个特殊的区域叫Humongous,G1将超过了一个Region容量一半的大对象,都存放在Humongous区域中,如果对象超过了Region大小,则存放在N个连续的Humongous Region中。G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
Alt text

3.如何回收垃圾对象

3.1. 垃圾收集算法

3.1.1. 标记-清除算法(Mark-Sweep)

标记清除算法是最基础的收集算法,其它收集算法都是基于这种思想。标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。
它的主要缺点:
①.标记和清除过程效率不高
②.标记清除之后会产生大量不连续的内存碎片。
Alt text

3.1.2. 复制算法(copying)

它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆的指针,按顺序分配内存即可,实现简单,运行高效。其带来的成本是增加一块空的内存空间及进行对象的移动。
Alt text

3.1.3. 标记-压缩算法(Mark-Compact)

标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。
主要缺点:
在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。
Alt text

3.1.4. 分代收集算法(Generation Collection)

这种算法没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块(如新生代,旧生代),这样就可以根据各个代的特点采用最适当的收集算法。
新生代中,每次垃圾回收都有大批对象死去,只有少量存活,那就选用复制算法。
旧生代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用”标记-清除”或”标记-压缩”算法来进行回收。

– end –