目 录CONTENT

文章目录
jvm

jvm垃圾回收机制

半糖
2024-09-19 / 0 评论 / 0 点赞 / 28 阅读 / 31514 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2024-09-21,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

概念

Java 虚拟机(JVM)的垃圾回收机制(Garbage Collection,简称 GC)是自动管理内存的一种方式。在 Java 程序运行过程中,会不断地创建对象,而一些对象在使用完毕后就不再被需要,这些无用的对象如果不及时清理就会导致内存泄漏,JVM 的垃圾回收机制负责识别并回收这些不再使用的对象所占用的内存空间,从而使得 Java 程序员不需要显式地进行内存释放操作。

GC存在的原因

程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。

GC的特点

GC是不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收,垃圾收集器在一个Java程序中的执行是自动的,不能强制执行清楚那个对象,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System.gc()方法来"建议"执行垃圾收集器(FullGC),但是他是否执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

finalize()方法

finalize()方法是在每次执行GC操作之前时会调用的方法,可以用它做必要的清理工作。

它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

代码示例

package jvm;

import org.junit.jupiter.api.Test;

public class GCClient {

    public Object object = null;

    /**
     * 测试finalize()和System.gc()
     */
    @Test
    public void test1() {
        GCClient test = new GCClient();
        test = null;
        System.gc(); // 手动回收垃圾
    }

    @Override
    protected void finalize() throws Throwable {
        // gc回收垃圾之前调用
        System.out.println("gc回收垃圾之前调用的方法");
    }
}

GC内存

以上GC内存结构是根据jdk7、jdk8进行划分的,不同版本会有不一样的地方。点击查看jvm的发展历程

  • GC主要回收(Heap)内存中的对象,同时方法区Method Area,jdk8后为元空间 Metaspace)中的部分数据在满足一定条件下也会被回收。

  • 堆被划分为两个不同的区域:新生代(Young)、老年代(Old)。

  • 新生代又被划分为三个区域:Eden、FromSurvivor、ToSurvivor

  • 默认情况:新生代(Young)与老年代(Old)的比例的值为 1 : 2(通过参数–XX:NewRatio来指定),即默认–XX:NewRatio为2,表示新生代 : 老年代 = 1 : 2

  • 默认情况:Edem : FromSurvivor : ToSurvivor = 8 : 1 : 1(通过参数–XX:SurvivorRatio来指定),即默认–XX:SurvivorRatio为8:Eden=8/10的新生代空间大小,FromSurvivor = ToSurvivo r= 1/10 的新生代空间大小。两个Survivor内存大小相等。

  • JVM每次只会使用Eden和其中的一块Survivor区域来为对象服务,任何时候总是有一块Survivor区域是空闲着的。因此,新生代实际可用的内存空间为9/10(即90%)的新生代空间。

分区GC的目的

可以根据各个年代的特点进行对象分区存储,更便于回收,各分区采用最适当的收集算法。新生代中,每次GC时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。

一次完整的GC流程

新创建的对象一般会被分配在新生代中,常用的新生代的垃圾回收器是ParNew垃圾回收器,它按照8:1:1将新生代分成Eden区,以及两个Survivor区。某一时刻,我们创建的对象将Eden区全部挤满,这个对象就是挤满新生代的最后一个对象。此时,MinorGC就触发了。

正式MinorGC前,JVM会先检查新生代中对象是比老年代中剩余空间大还是小。为什么要做这样的检查呢?原因很简单,假如MinorGC之后Survivor区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。这样就有两种情况:

  • 老年代剩余空间大于新生代中的对象大小,那就直接MinorGC(GC完survivor不够放,老年代也绝对够放);

  • 老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了“老年代空间分配担保规则”,具体来说就是看-XX:-HandlePromotionFailure参数是否设置为true(在 JDK 6 之后这个参数被废弃,默认总是认为HandlePromotionFailure为 true,即总是进行空间分配担保)。

开启了老年代空间分配担保规则,也有两种情况:

  • 老年代中剩余空间大小,大于历次Minor GC之后剩余对象的大小,进行 Minor GC

  • 老年代中剩余空间大小,小于历次Minor GC之后剩余对象的大小,进行Full GC,把老年代空出来再检查(再不行就OOM了)

未开启老年代空间担保规则会先进行MinorGC,如果MinorGC过后suvivor区依然不够存放,会触发MajorGC(或者FULLGC),如果老年代依然不够放,就OOM了。

开启老年代空间分配担保规则只能说是大概率上来说,MinorGC剩余后的对象够放到老年代,所以当然也会有万一,MinorGC后会有这样三种情况:

  • MinorGC之后的对象足够放到Survivor区,皆大欢喜,GC结束;

  • MinorGC之后的对象不够放到Survivor区,接着进入到老年代,老年代能放下,那也可以,GC结束;

  • MinorGC之后的对象不够放到Survivor区,老年代也放不下,那就只能FullGC。

前面都是成功GC的例子,还有3中情况,会导致GC失败,报OOM:

  • 紧接上一节FullGC之后,老年代任然放不下剩余对象,就只能OOM;

  • 未开启老年代分配担保机制,且一次FullGC后,老年代任然放不下剩余对象,也只能OOM;

  • 开启老年代分配担保机制,但是担保不通过,一次FullGC后,老年代任然放不下剩余对象,也是能OOM。

对象分配规则

优先在 Eden 区分配

  • 当在 Java 程序中创建一个新对象时,JVM 首先会尝试将该对象分配到新生代的 Eden 区。这是因为大多数对象的生命周期都比较短,Eden 区是对象诞生的地方。例如,在一个 Web 应用处理用户请求的过程中,大量临时创建的对象(如表示请求参数的对象、局部方法内创建的临时计算对象等)都会优先在 Eden 区分配内存。

  • Eden 区的内存分配是比较快速的,因为它是一块连续的内存空间,分配操作相对简单,不需要复杂的内存查找和整理操作。

大对象直接进入老年代

  • 大对象是指那些需要大量连续内存空间的对象。可以通过 JVM 参数 -XX:PretenureSizeThreshold 设置大对象的阈值,当创建的对象大小超过这个阈值时,该对象就会被直接分配到老年代。例如,在处理一些大型数据结构(如大容量的数组、大的字符串对象等)时,如果其大小超过了设定的阈值,就会直接在老年代分配内存。

  • 这种分配方式的原因在于,在新生代采用的是复制算法进行垃圾回收,如果大对象在新生代分配,在进行 Minor GC(新生代垃圾回收)时,复制大对象会消耗大量的时间和资源。将大对象直接分配到老年代,可以避免这种情况,提高垃圾回收的效率。

长期存活的对象进入老年代

  • 在新生代中,对象经过一次 Minor GC 后如果仍然存活,就会被移动到 Survivor 区(一般有两个 Survivor 区,如 Survivor0 和 Survivor1)。对象在 Survivor 区每经过一次 Minor GC 存活下来,其年龄就会加 1。

  • 可以通过 JVM 参数 -XX:MaxTenuringThreshold 设置对象晋升到老年代的年龄阈值,当对象的年龄达到这个阈值时,该对象就会被晋升到老年代。例如,如果-XX:MaxTenuringThreshold设置为 15,一个对象在 Survivor 区经过 15 次 Minor GC 后仍然存活,就会被移动到老年代。

  • 这种分配规则是基于对象生命周期的考虑,将长期存活的对象移动到老年代,有利于减少在新生代进行垃圾回收的频率,同时老年代相对稳定的内存结构也适合存放这些长期存活的对象。

空间分配担保规则下的对象分配

  • 在新生代发生 Minor GC 时,如果 Survivor 区放不下存活的对象,这些存活对象就需要被转移到老年代。但是老年代可能没有足够的空间来容纳这些对象。

  • 根据老年代空间分配担保规则(默认是开启的),JVM 会检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果是,那么 Minor GC 可以确保是安全的,尽管 Survivor 空间可能不够,也可以放心地将新生代存活对象转移到老年代;如果不是,JVM 会查看HandlePromotionFailure设置,如果允许担保失败(HandlePromotionFailure = true),JVM 会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果是,就尝试进行 Minor GC,否则就进行 Full GC(对整个堆,包括新生代和老年代进行垃圾回收),以腾出足够的空间来容纳新生代的存活对象

GC分类

Minor GC

回收区域:主要针对新生代(Young Generation)进行垃圾回收。新生代又包含 Eden 空间以及两个 Survivor 空间(Survivor0 和 Survivor1)。

触发条件当 Eden 区空间满时,就会触发 Minor GC。在 Minor GC 过程中,Eden 区中存活的对象会被复制到 Survivor 空间(其中一个 Survivor 空间,如 Survivor0),如果 Survivor0 空间不足,存活的对象会被直接晋升到老年代。

特点:Minor GC 的频率相对较高,因为新生代中的对象大多是生命周期较短的临时对象。一般来说,Minor GC 的回收速度相对较快,因为新生代空间相对较小,并且采用复制算法进行回收,该算法在处理大量短生命周期对象时效率较高。

Major GC

回收区域:Major GC 主要是针对老年代(Old Generation)进行垃圾回收。

触发条件老年代空间不足时会触发 Major GC(先尝试触发Minor GC,不足是触发Major GC,还是不足报OOM)。例如,当老年代中对象的数量或占用空间达到一定阈值,就需要进行 Major GC 来清理无用的对象,释放空间。

特点:Major GC 和 Full GC 的回收速度相对较慢(比Minor GC慢10倍以上),停顿时间较长。因为老年代中的对象生命周期较长,数量可能较多,回收时需要更多的标记、整理或清除操作。通常执行MajorGC会连着MinorGC一起执行。

Full GC

回收区域:Full GC 是对整个堆(Heap),包括新生代和老年代进行垃圾回收。

触发条件:对于 Full GC,触发原因较为复杂,主要有以下几种:

  1. 老年代空间不足(大对象、长期存活对象、空间分配担保规则分配的对象进入老年代时发现空间不足)。

  2. MinorGC后存活的对象超过了老年代剩余空间。

  3. 每次晋升到老年代的对象平均大小 > 老年代剩余空间。

  4. 永久代(在 Java 8 之前的方法区)或元空间(Java 8 及以后取代永久代的概念)空间不足。

  5. System.gc () 方法被显式调用(虽然这种调用不保证一定会执行 Full GC)。

  6. 空间分配担保失败。

  7. CMS GC异常

  8. 在CMS等并发收集器中每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行FullGC回收。

特点Full GC 由于涉及整个堆,其影响范围更广,对应用程序的性能影响也更大,所以应尽量避免频繁的 Full GC。

减少Full GC的措施

  1. 增加方法区的空间;

  2. 增加老年代的空间

  3. 减少新生代的空间

  4. 禁止使用System.gc()方法;

  5. 使用标记-整理算法,尽量保持较大的连续内存空间

  6. 排查代码中无用的大对象。

判断对象是否存活

引用计数法

每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为0就代表该对象死亡。

  • 引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。

  • 主流的Java虚拟机里面都没有选用引用计数算法来管理内存。

优点:引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。

缺点:很难解决对象之间相互循环引用的问题,也是主流的Java虚拟机里面没有选用引用计数算法来管理内存的原因。

应用场景:最好不用

代码示例

package jvm;

import org.junit.jupiter.api.Test;

/**
 * 垃圾回收机制
 */
public class GCClient {

    public Object object = null;

    /**
     * 引用计数法不用的原因:循环引用导致计数器失效
     */
    @Test
    public void test2() {
        GCClient a = new GCClient();
        GCClient b = new GCClient();
        /**
         * 循环引用,此时引用计数器法失效
         */
        a.object = b;
        b.object = a;

        a = null;
        b = null;
    }

}

可达性分析法

该种方法是从GC Roots开始向下搜索,搜索所走过的路径为引用链。当一个对象到GC Roots没用任何引用链时,则证明此对象是不可用的,表示可以回收。

  • 上图中Object1、Object2、Object3、Object4、Object5到GC Roots是可达的,表示它们是有引用的对象,是存活的对象不可以进行回收

  • Object6、Object7、Object8虽然是互相关联的,但是它们到GC Roots是不可达的,所以他们是可以进行回收的对象

GCRoots对象

  • 虚拟机栈(栈帧中的本地变量表)、本地方法栈(即一般说的Native方法)中引用的对象。

  • 方法区或元空间中的类静态变量(static修饰的变量)、常量(final修饰的变量)引用的对象。

优点:解决了循环依赖问题

缺点:和引用计数法比没有缺点

应用场景:主流的jvm

GC算法

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

  1. 标记阶段

    • 从根对象(如全局变量、栈中的变量等)开始,遍历对象图,标记所有可达的对象。

    • 这个过程通常使用深度优先搜索(DFS)或广度优先搜索(BFS)算法来追踪对象之间的引用关系。

  2. 清除阶段

    • 遍历堆内存,回收所有未被标记的对象所占用的内存空间。

优点

  1. 可以解决循环引用的问题

  2. 必要时才回收(内存不足时)。

缺点

  1. 产生内存碎片,因为回收后的内存空间是不连续的,可能会导致后续分配较大对象时失败,即使总的空闲内存足够。

  2. 回收时,应用需要挂起,也就是stop the world。

  3. 标记和清除的效率不高,尤其是要扫描的对象比较多的时候。

应用场景:一般应用于老年代,也就是Major GC,因为老年代的对象声明周期比较长。

标记 - 整理算法(Mark - Compact)

  1. 标记阶段

    • 同标记 - 清除算法,从根对象开始标记所有可达对象。

  2. 整理阶段

    • 将所有存活对象向一端移动,然后直接清除另一端的内存空间。

    • 整理阶段有三个步骤,分别是:移动、更新引用、清除。

优点:解决了内存碎片的问题(标记 - 清除算法的),同时不会浪费一半的内存空间(复制算法)。

缺点:移动对象的开销较大,因为移动了可用对象,所以需要去更新引用(比标记-清除算法更慢,因为多了两个步骤)。

应用场景:一般应用于老年代,也就是Major GC,因为老年代的对象生命周期比较长。

复制算法(Copying)

工作原理:

  • 将内存空间划分为两个相等的区域,例如 A 区和 B 区。

  • 只在其中一个区域(如 A 区)分配内存。当 A 区内存使用到一定程度时,开始 GC 过程。

  • GC 时,将 A 区中存活的对象复制到 B 区,然后一次性回收 A 区的所有内存。

  • 下次分配内存时就在 B 区进行,当 B 区内存使用到一定程度时,再将 B 区存活对象复制到 A 区并回收 B 区内存。

优点:

  1. 在存活对象不多的情况下,性能高(因为复制算法值复制活动对象)。

  2. 能解决内存碎片(因为复制的时候紧凑了一次)。

  3. 能避免引用更新的问题(标记-整理算法最大的缺点)。

  4. 缓存友好,因为对象引用的对象会被复制到相邻位置,便于利用缓存。

缺点:

  1. 内存利用率低,因为始终有一半的内存处于空闲状态等待复制使用。

  2. 如果存活对象的数量比较大,复制算法的性能会变得很差(所以大对象直接进入老年代)。

应用场景:

一般使用在新生代中(fromSuvivor区和toSuvivor区之间),因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。

分代收集算法(Generational Collection)

基本思想

根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。

  1. 新生代的 GC

    • 通常采用复制算法,因为年轻代中大部分对象都是短期存活的,复制算法的效率较高。

  2. 老年代的 GC

    • 可以采用标记 - 清除或标记 - 整理算法,因为老年代中的对象相对稳定,移动或标记清除的频率较低。

分代回收机制(引出跨代问题)

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  • 弱分代假说(WeakGenerationalHypothesis):绝大多数对象都是朝生夕灭的。

  • 强分代假说(StrongGenerationalHypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代(YoungGeneration)和老年代(OldGeneration)两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。分代收集并非只是简单划分一下内存区域那么容易,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用。假如要现在进行一次只局限于新生代区域内的收集,但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GCRoots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。遍历整个老年代所有对象的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:

  • 跨代引用假说(IntergenerationalReferenceHypothesis):跨代引用相对于同代引用来说仅占极少数。

依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(称为“记忆集”,RememberedSet),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

垃圾收集器

垃圾收集器是垃圾回收算法(引用计数法、可达性分析法、标记清楚法、标记整理法、复制算法、分代算法)的具体实现,不同垃圾收集器、不同版本的JVM所提供的垃圾收集器可能会有很大差别。

概要

这里以JDK8作参考:

新生代收集器:Serial、ParNew、ParallelScavenge

老年代收集器:CMS、SerialOld、ParallelOld

整堆收集器G1两个收集器间有连线,表明它们可以搭配使用(jdk9的默认GC)

JDK8中默认的是UseParallelGC即 ParallelScavenge + ParallelOld

简要归纳和描述

工作线程则:是由 Java 运行时环境创建和管理的线程,用于执行一些后台任务,例如垃圾回收、线程池管理等。

用户线程:是由应用程序创建和管理的线程,用于执行应用程序的具体任务,例如处理用户输入、执行计算、访问数据库等。

种类

Serial

它是一个新生代的单线程收集器,使用复制算法,它只会使用一个CPU或者线程去完成垃圾收集工作,而且在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,它是发展历史最悠久的收集器。

特点

  • 新生代单线程收集器,使用复制算法。

  • GC工作时,其它所有线程都将停止工作。

  • 简单高效,适合单CPU环境。单线程没有线程交互的开销,因此拥有最高的单线程收集效率。

使用方式

设置垃圾收集器:"-XX:+UseSerialGC" --添加该参数来显式的使用改垃圾收集器;

ParNew

它是一个新生代多线程收集器,使用复制算法,是Serial的多线程版本,同时启动多个线程去进行垃圾收集。

特点

  • 新生代多线程收集器,使用复制算法。

  • 除了多线程外,其余的行为、特点和Serial收集器一样。

  • 在单个CPU环境中不如Serail,多线程使用更好。

使用方式

  1. 设置垃圾收集器:"-XX:+UseParNewGC"--强制指定使用ParNew。

  2. 设置垃圾收集器:"-XX:+UseConcMarkSweepGC"--指定使用CMS后,会默认使用ParNew作为新生代收集器;(这里是被动使用ParNewGC。

  3. 设置垃圾收集器参数:"-XX:ParallelGCThreads"--指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同。

Parallel Scavenge

它是一个新生代的多线程收集器,使用复制算法,和ParNew的关注点不一样,该收集器更关注吞吐量,尽快地完成计算任务

特点

  • 新生代多线程收集器,使用复制算法。

  • 与ParNew不同的是:以高吞吐量为目标,(减少垃圾收集时间,让用户代码获得更长的运行时间)

Serial Old

它是一个老年代的单线程收集器,使用标记-整理算法,是Serial的老年代版本。

特点

  • 老年代单线程收集器,使用标记-整理算法

使用方式

在JDK1.5及之前,与ParallelScavenge收集器搭配使用,在JDK1.6后有ParallelOld收集器可搭配。现在作为CMS收集器的后备预案,在并发收集发生ConcurrentModeFailure时使用。

Parallel Old

它是一个老年代的多线程收集器,使用标记-整理算法,是Parallel Scavenge的老年代版本。

特点

  • 老年代多线程收集器,使用标记-整理算法。

  • 单个CPU环境中不如Serial Old,多线程使用更好。

使用方式

设置垃圾收集器:"-XX:+UseParallelOldGC":指定使用ParallelOld收集器;

CMS

它是一个老年代的多线程收集器,使用标记-清除算法,是一种以获取最短回收停顿时间为目标的收集器,适用于互联网站或者B/S系统的服务端。

流程

  1. 初始标记:仅仅只是标记一下GCRoots能直接关联到的对象,速度很快,需要StopTheWorld。

  2. 并发标记:就是从GCRoots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行

  3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

  4. 并发清除清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发运行的。

由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过上图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。

CMS收集器还远达不到完美的程度,它至少有以下三个明显的缺点

首先,CMS收集器对处理器资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。

然后,由于CMS收集器无法处理“ 浮动垃圾(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop TheWorld”的Full GC的产生。

还有最后一个缺点,CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

特点

  • 老年代多线程收集器,使用标记-清除算法。

  • 垃圾收集线程与用户线程基本上可以同时工作。

  • 不进行压缩操作,产生内存碎片。

  • 以获取最短回收停顿时间为目标。

  • 并发收集、低停顿。

使用方式

设置垃圾收集器:"-XX:+UseConcMarkSweepGC":指定使用CMS收集器。

G1

它是一个新生代和老年代的多线程分代收集器,使用标记-整理 和 复制算法,是当今收集器技术发展最前沿的技术之一,G1(Garbage First)可以说是CMS的终极改进版,解决了CMS内存碎片、浮动垃圾等问题,虽然流程与CMS相似,但底层原理和结构已经完全不同。

G1(GarbageFirst)是一款主要面向服务端应用的垃圾收集器,JDK9发布之日,G1宣告取代ParallelScavenge加ParallelOld组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。G1收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

结构

虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden、Survivor、Old、Humongous(大对象)收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的HumongousRegion之中,G1的大多数行为都把HumongousRegion作为老年代的一部分来进行看待,如上图所示。

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“GarbageFirst”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

记忆集

为了实现 Region 之间对象引用的快速定位,G1 引入了 Remembered Set。每个 Region 都有一个对应的 Remembered Set,用于记录该 Region 中的对象被其他 Region 中的对象引用的情况。

当进行垃圾回收时,通过查询 Remembered Set 可以快速确定哪些 Region 中的对象是存活的,从而避免全堆扫描,提高垃圾回收的效率。

在对象引用发生变化时,G1 会通过写屏障(Write Barrier)技术来更新 Remembered Set。写屏障是一种在对象赋值操作时执行的额外操作,用于记录对象引用的变化情况,并将这些变化反映到 Remembered Set 中。

收集集合

收集集合是一组 Region 的集合,在垃圾回收过程中,G1 会选择一部分 Region 加入到 Collection Set 中进行回收。

收集集合的选择是根据垃圾回收的目标和停顿时间要求来确定的,通常会选择垃圾占用比例较高、回收价值较大的 Region。

通过限制回收的 Region 数量,可以减少垃圾回收的时间和工作量,同时也可以更好地控制垃圾回收的停顿时间。

流程

  1. 初始标记(Initial Marking)

    • 标记从 GC Root 开始直接可达的对象,这个阶段会伴随一次新生代 GC,速度很快,并且会产生短暂的停顿。

  2. 并发标记(Concurrent Marking)

    • 从初始标记的对象开始,进行可达性分析,找出所有存活的对象,这个阶段可以和应用程序并发执行。

  3. 最终标记(Final Marking)

    • 对并发标记阶段中用户线程变动的对象进行标记修正,这个阶段会产生短暂的停顿。

  4. 筛选回收(Live Data Counting and Evacuation)

    • 首先对各个 Region 的回收价值和成本进行排序,根据用户设定的停顿时间来制定回收计划,然后选择一部分 Region 进行回收,这个阶段也会产生停顿,但可以与用户程序并发执行。

特点

  1. 并行与并发

    • G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-The-World 停顿的时间,部分阶段可以和应用程序并发执行。

    • 例如,在并发标记阶段,G1 可以和应用程序同时运行,不会完全暂停应用程序的执行。

  2. 分代收集

    • G1 依然遵循分代收集的思想,能够同时管理新生代和老年代的内存空间。

    • 但是与其他垃圾回收器不同的是,G1 将整个堆划分为多个大小相等的 Region(区域),新生代和老年代不再是物理上连续的区域,而是由一系列 Region 组成。

  3. 空间整合

    • G1 采用 “标记 - 整理” 算法,不会产生内存碎片。

    • 在回收过程中,G1 会将存活对象复制到新的 Region 中,并对空闲 Region 进行合并,从而实现空间的高效利用。

  4. 可预测的停顿时间

    • G1 可以建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。

    • 这是通过维护一个优先列表来实现的,每次根据允许的收集时间,优先选择回收价值最大的 Region,从而保证在有限的时间内可以回收尽可能多的垃圾。

适用场景

  1. 大内存应用

    • 对于需要处理大量数据、占用较大内存空间的应用程序,G1 能够有效地管理内存,减少垃圾回收的停顿时间。

  2. 对响应时间有要求的应用

    • 如交互式应用、实时系统等,G1 可以提供更可预测的停顿时间,保证系统的响应性能。

  3. 多处理器服务器

    • 在多处理器环境下,G1 能够充分发挥并行回收的优势,提高垃圾回收的效率。

使用方式

  • 设置垃圾收集器:"-XX:+UseG1GC":指定使用G1收集器;

  • 设置垃圾收集器参数:"-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;

  • 设置垃圾收集器参数:"-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;

  • 设置垃圾收集器参数:"-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region

0
jvm
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区