垃圾回收,几种GC算法及GC机制

什么是垃圾回收?如何触发垃圾回收? 

垃圾回收(GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少内存泄漏和内存管理错误的可能性。可以通过多种方式触发:

  • 内存不足时:当 JVM 检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
  • 手动请求:虽然垃圾回收是自动的,开发者可以通过调用 System.gc()或 Runtime.getRuntime().gc()建议JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。
  • JVM 参数:启动 java 程序时可以通过 JVM 参数来调整垃圾回收的行为,比如:-Xmx(最大堆大小)、-Xms(初始堆大小)等。
  • 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。

垃圾判断方法

  • 引用计数法
    原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加 1;当引用失效时,计数器减 1。当计数器为 0 时,表示对象不再被任何变量引用,可以被回收。
    缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为 0,导致对象无法被回收
  • 可达性分析法
    原理:从一组称为 GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及
    这些对象引用的其他对象,以此类推。如果一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。
    GCRoos 对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、本地方法栈中 JNl 引用的对象、活跃线程的引用等。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
    B b;
}
class B {
    C c;
}
class C {
    无任何引用
}
public class GCTest {
    public static void main(String[] args) {
        A a = new A();
        a.b = new B();
        a.b.c = new C();
        // 在此时,a, b, c 这些对象都被 GC Roots 引用,暂时不会被回收。
    }
}

//若a=null,那么a和gc root引用断开,那么a、b和c就无法到达gc root,不可达,会被回收。

//若 a=null,那么 a 和 gc root 引用断开,那么 a、b 和 c 就无法到达 gc root,不可达,会被回收。

垃圾回收算法

  • 标记-清除算法

    标记清除算法分为“标记”和"清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记清除算法有两个缺陷,一是标记和清除的过程效率都不高,二是清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。

  • 复制算法

    为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时只能使用一半的内存空间,内存利用率严重不足。

  • 标记-整理算法

    复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记整理算法”。标记整理算法的“标记”过程与”标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端,移动结束后直接清理掉剩余部分。

  • 分代回收算法
    分代收集是将内存划分成新生代和老年代。分配的依据是对象的生存周期或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄+1。当年龄超过一定值(默认是 15,可以通过参数 XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

垃圾回收器

垃圾回收机制

Minor GC

Minor GC 主要负责回收年轻代的垃圾对象。年轻代通常分为三个区域:一个伊甸区和两个幸存区(一般称为 From 区和 To 区)。当对象被创建时,先被分配到伊甸区。在年轻代的垃圾回收过程中,先对伊甸区进行垃圾回收,将存活的对象复制到一个空闲的幸存区中(通常是 From 区),同时清空伊甸区。Minor GC 会一直重复这样的过程,直到 From 区被填满,则进入 To 区,To 区被填满之后,则将所有对象移动到老年代中。

经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代,或者 Survivor 空间中某个年龄段的对象总大小超过了 Survivor 空间的一半,那么该年龄段及以上年龄段的所有对象都会在下一次垃圾回收时被晋升到老年代。

  • 作用范围:针对年轻代进行回收,包括 Eden 区和两个 Survivor 区(S0 和 S1)。
  • 触发条件:当 Eden 区空间不足时,JVM 触发一次 Young GC,将 Eden 区和一个 Survivor 区中的存活对象移动到另一个 Survivor 区或老年代。
  • 特点:通常发生的非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。

Major GC

Major GC 是指对老年代进行的垃圾回收操作。在 Java 堆内存中,老年代用于存放生命周期较长的对象或者经过多次 Minor GC 后仍然存活的对象。Major GC 的执行时间一般比 Minor GC 更长,因为它需要处理较多的对象和进行更复杂的内存整理操作。在 Major GC 期间,应用程序的执行将会暂停,直到垃圾回收操作完成。

  • 作用范围:针对老年代进行回收,但不一定只回收老年代。
  • 触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发 Major GC。
  • 特点:相比 Minor GC,Major GC 发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。

 Full GC

当新生代空间不足时,会触发 Minor GC,只清理新生代内存。而老年代空间不足或者为了整理碎片化的内存,会触发 Full GC,对整个堆内存进行回收。

Full GC 可能会导致较长的停顿时间,因为它需要扫描整个堆内存,标记可回收对象,并进行内存整理。这意味着在 Full GC 过程中,应用程序的执行会被暂停。

Full GC 的频率会受多种因素影响,如堆内存的大小、JVM 配置参数、对象分配速度等。如果 Full GC 发生过于频繁或耗时过长,可能会导致应用程序的性能下降。

  • 作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。
  • 触发条件:
    1. 直接调用 System.gc()或 Runtime.getRuntime().gc()方法时,虽然不能保证立即执行,但 JVM 会尝试执行 Full GC。
    2. Minor GC 时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发 FuGC,对整个堆内存进行回收。
    3. 当永久代(Java8 之前的版本)或元空间(Java8 及以后的版本)空间不足时。
  • 特点:Full GC 是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少 Full GC 的触发。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计