java gc机制

目录复制

1、什么是GC

  每个程序员都遇到过内存溢出的情况,程序运行时,内存空间不足时,要把已经死的对象内存空间释放出来,这就是 GC 要做的事,而 GC 是 JVM 自动帮我们完成的,那我们为什么还要了解 GC 呢?

  当需要排查各种内存溢出、内存泄漏问题时,当垃圾收器成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。

  垃圾回收器需要注意三件事:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

2、对象已死?(哪些内存需要回收)

  在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了。

例子:

public class GCTest {
    public static void main(String[] args) throws InterruptedException {

        Object a = new Object();

        Object b = new Object();
        b = null;

        System.gc(); // 手动触发GC,a是活对象,b是死对象应该被释放
        ......
    }
}

2.1 引用计数法

  在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

  它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。但是,在 Java 领域,至少主流的 Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

例子:

-XX:+PrintGCDetails  // 打印GC详细日志

public class GCTest {

    private GCTest a;

    private byte[] b = new byte[1024 * 1024 * 5] ;  // 5m大小

    public static void main(String[] args) throws InterruptedException {

        GCTest g1 = new GCTest();
        GCTest g2 = new GCTest();

        g1.a = g2;
        g2.a = g1;

        g1 = null;
        g2 = null;

        System.gc(); // 手动触发GC,
    }
}

result:

[GC (System.gc()) [PSYoungGen: 15443K->744K(75776K)] 15443K->752K(249344K), 0.0007931 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 744K->0K(75776K)] [ParOldGen: 8K->598K(173568K)] 752K->598K(249344K), [Metaspace: 3118K->3118K(1056768K)], 0.0038518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 75776K, used 1951K [0x000000076bb80000, 0x0000000771000000, 0x00000007c0000000)
  eden space 65024K, 3% used [0x000000076bb80000,0x000000076bd67cb8,0x000000076fb00000)
  from space 10752K, 0% used [0x000000076fb00000,0x000000076fb00000,0x0000000770580000)
  to   space 10752K, 0% used [0x0000000770580000,0x0000000770580000,0x0000000771000000)
 ParOldGen       total 173568K, used 598K [0x00000006c3200000, 0x00000006cdb80000, 0x000000076bb80000)
  object space 173568K, 0% used [0x00000006c3200000,0x00000006c3295a50,0x00000006cdb80000)
 Metaspace       used 3135K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

  从运行结果来看,已经使用内存是远远小于10m的,意味着虚拟机并没有因为这两个对象互相引用就放弃回收它们,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。

2.2 可达性分析算法

  当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

  可达性分析:这个算法的基本思路就是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 引用链(Reference Chain),如果某个对象到 GC Roots 间没有任何 引用链 相连,则证明此对象是不可能再被使用的,是死对象。

可达性分析算法

在这里插入图片描述

  在 java技术体系里,哪些可以作为 GC Roots 对象

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 (PS:JDK8已经没有方法区)
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用(PS:字符串常量池的引用对象可以被回收)。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
    NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JM XBean、JVM TI中注册的回调、本地代码缓存等。

  除了这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性地加入,共同构成完整 GC Roots 集合。

3、垃圾回收算法(如何回收)

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

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数

  这三个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将 Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域(年轻代)中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域(老年代),这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
  整个堆按如下所示划分,eden区、survive区、old区
在这里插入图片描述

  在 Java堆 划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域 ——因而才有了 Minor GCMajor GCFull GC 这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了标记-复制算法标记-清除算法标记-整理算法等针对性的垃圾收集算法。

  • 部分收集(Partial GC):指目标不是完整收集整个J ava堆的垃圾收集,其中又分为:
    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为
  • 整堆收集(Full GC):收集整个 Java堆和方法区的垃圾收集。

3.1 标记-清除算法

  标记-清除(Mark-Sweep)算法如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。

在这里插入图片描述
 过程:

  • 标记: Collector 从 GC Roots根结点开始遍历,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象。

  • 清除: Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收(PS:这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲列表里,还记得指针碰撞和空闲列表吗?)

 它的主要缺点:

  • 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
  • 需要使用空闲列表维护可用内存,使用空闲列表对创建的对象内存分配则更复杂,所有它不适合新生代这种创建对象频繁的区域
  • 清除时对堆内存从头到尾进行线性的遍历,就不太适合内存较大的区域;如果有大量可回收对象,执行效率也是底下的。所有它不适合新生代(大量可回收对象,有更优的算法)

3.2 标记-复制算法

  标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效(PS:适合新生代

在这里插入图片描述

 它的主要缺点有两个:

  • 不适合多数对象存活的区域:如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销(PS:适合新生代
  • 浪费内存空间:这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。

3.3 标记-整理算法

  标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

在这里插入图片描述

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

  • 如果移动对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被形象地描述为“Stop The World”。
  • 如果不移动对象,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。譬如通过“分区空闲列表”来解决内存分配问题(创建对象)。内存的访问是用户程序最频繁的操作,甚至都没有之一,假如在这个环节上增加了额外的负担,势必会直接影响应用程序的吞吐量。

  基于以上两点,是否移动对象都存在弊端,移动则内存回收会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟CMS收集器则是基于标记-清除算法的,这也从侧面印证这点。

4、JVM的算法实现细节

   固定可作为 GC Roots 的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,尽管目标明确,但查找过程要做到高效并非一件容易的事情,现在Java应用越做越庞大,光是方法区的大小就常有数百上千兆,里面的类、常量等更是恒河沙数,若要逐个检查以这里为起源的引用肯定得消耗不少时间。

  迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此毫无疑问根节点枚举与之前提及的整理内存碎片一样会面临相似的 “Stop The World” 的困扰。

  当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。在HotSpot的解决方案里,是使用一组称为 OopMap 的数据结构来达到这个目的。

  • 一旦类加载动作完成的时候,HotSpot 就会把类内偏移量上是什么类型的数据计算出来,记录到 OopMap
  • 在即时编译过程中,也会在特定的位置生成 OopMap,记录下栈上和寄存器里哪些位置是引用。

  在 OopMap 的协助下,HotSpot 可以快速准确地完成 GC Roots枚举,但一个很现实的问题随之而来:导致 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂。

  实际上HotSpot 也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点Safepoint)。

安全点主要在:

  1. 循环的末尾(非 counted 循环)
  2. 方法临返回前 / 调用方法的call指令后
  3. 可能抛异常的位置

  用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

为什么把这些位置设置为 jvm 的安全点呢?

  主要目的:就是避免程序长时间无法进入 safepoint,比如 JVM 在做 GC 之前要等所有的应用线程进入到安全点后 JVM 线程才能分派 GC 任务 ,如果有线程一直没有进入到安全点,就会导致 GC 时 JVM 停顿时间延长

为什么必须在安全点停止呢?

  主要是保证OopMap数据的准确性

stw优化实战

-XX:PrintSafepointStatisticsCount=1
-XX:+PrintSafepointStatistics

-XX:+PrintGCDetails
public class TestBlockingThread {

    static Thread t1 = new Thread(() -> {
        while (true) {

            for (int i = 1; i <= 1000000000; i++) {
                boolean b = 1.0 / i == 0;
            }
        }
    });

    static Thread t2 = new Thread( () -> {
        while (true) {
            long start = System.currentTimeMillis();
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            byte[] bytes = new byte[1024 * 1024 * 5];
            long cost = System.currentTimeMillis() - start;
            (cost > 1010L ? System.err : System.out).printf("thread: %s, costs %d ms\n", Thread.currentThread().getName(), cost);
        }
    });

    public static void main(String[] args)  {
        t1.start();
        t2.start();
    }
}

-XX:+SafepointTimeout
-XX:SafepointTimeoutDelay=2000

  该代码开始执行 gc 时候,会造成长达 5s以上的 stw,后续一直执行也有可能造成这么长的 stw,你知道原因吗or你知道怎么优化吗?

  答:HotSpot 虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用int类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点。这种循环称为可数循环counted loop),相应使用 long 或范围更大的数据类型作为索引的循环被称为不可数循环Uncounted loop

5、经典垃圾回收器

  如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。

垃圾回收器组合

在这里插入图片描述

3.5.1 Serial 收集器(新生代) + Serial Old 收集器(老年代)

  Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。下图展示了Serial/Serial Old收 集器的运行过程。

在这里插入图片描述

  事实上,迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比)。

3.5.3 parallel Scavenge 收集器(新生代) + Parallel Old 收集器(老年代)

​ 堆分代模型

在这里插入图片描述

  Parallel Scavenge 收集器是一款新生代收集器,它是基于标记-复制算法实现的收集器,是能够并行收集的多线程收集器,整个 ygc 阶段都是 STW 的。

  Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值:

在这里插入图片描述

  如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。

  Parallel OldParallel Scavenge收集器 的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

 工作流程如下图:

在这里插入图片描述

在这里插入图片描述
  parallel Scavenge 收集器(新生代) + Serial Old 收集器(老年代)是JDK8默认的垃圾回收器,它只有 ygc 和 full gc。也就是说,针对它的优化策略是,应该避免本该被回收的垃圾对象进入老年代而触发 full gc

 它的一些策略如下所示:

  • 对象优先在 Eden 分配 :

  对象优先在 Eden 区分配,当 Eden 区不足时,触发 ygc,将存活对象移入 survivor 区,当 survivor 区不足时,则多余的对象直接进入老年代(担保机制)

-XX:+PrintGCDetails
-Xms30m
-Xmx30m
-Xmn10m
-XX:SurvivorRatio=8
-server
public class GCTest0 {

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        byte[] bytes0 = new byte[1024 * 1024 * 2];
        byte[] bytes1 = new byte[1024 * 1024 * 2];
        byte[] bytes2 = new byte[1024 * 1024 * 2];
        byte[] bytes3 = new byte[1024 * 1024 * 2];
    }
}

 容易发生的场景:

  1. 大数据导入,大数据拉取
  2. 接口请求量超级大

  在不改变业务的情况,解决办法:增大 Eden、Survivos区内存,但一味的增大年轻代内存就没问题了吗?这个问题后面分析

  • 大对象机制:

  当 eden 不足放下一个对象时,会判断这个对象是否大于等于 eden 区一半,如果是,则直接放入老年代;反之,触发 yong gc。

-XX:+PrintGCDetails
-Xms20m
-Xmx20m
-Xmn10m
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=10m  // 这个参数无效
-server
-XX:+UseParallelGC
public class GCTest1 {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        byte[] bytes0 = new byte[1024 * 1024 * 6];
    }
}

  这种机制在实际业务中根本不太需要考虑,因为现在生产环境 Eden 少则 2G,也就是说你的一个对象至少要达到 1G 才会触发这种机制。

  • 对象动态年龄判断:

  虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代
  它还有一套动态年龄判断的机制,针对 ParNew 和 parallel Scavenge,它们的动态年龄机制还不一样。

  • ParNew 对象动态年龄判断:

  发生 YGC 后,把幸存的对象年龄从小到大进行累加,当加入某个年龄段后,累加和超过 survivor 区域 * TargetSurvivorRatio 的时候,就从这个年龄段往上的年龄的对象进行晋升。

动态年龄判断

-Xms20M
-Xmx20M
-Xmn10M
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-XX:+PrintTenuringDistribution
-XX:MaxTenuringThreshold=15
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC

public class GCTest2 {

    public static void main(String[] args) {
        System.gc(); // 去除 java启动内置对象影响,把它们都放进 old区

        byte[] bytes1 = null;
        byte[] bytes2 = null;
        byte[] bytes3 = null;
        for (int i = 0; i < 22; i++) {
            byte[] bytes = new byte[1024 * 1024];
            if(i == 0) {
                bytes1 = new byte[1024 * 1024 / 4];
            }
            if(i == 9) {
                bytes2 = new byte[1024 * 1024 / 5]; // 第二次执行调整为 1024 * 1024 / 4
            }
            if(i == 15) {
                bytes3 = new byte[1024 * 1024 / 4];
            }
        }
        if(bytes1 != null || bytes2 != null || bytes3 != null) {

        }
    }
}

第一次执行:

Desired survivor size 524288 bytes, new threshold 3 (max 15)
- age   1:     262160 bytes,     262160 total
- age   2:     209736 bytes,     471896 total
- age   3:     262160 bytes,     734056 total
: 8041K->716K(9216K), 0.0002977 secs] 8870K->1545K(19456K), 0.0003116 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

  age 1 + age 2 < 1024k(S区) * 0.5(TargetSurvivorRatio),所以 age 3 不会晋级到老年代

第二次执行:

Desired survivor size 524288 bytes, new threshold 2 (max 15)
 - age   1:     263088 bytes,     263088 total
 - age   2:     263400 bytes,     526488 total
: 8161K->532K(9216K), 0.0003287 secs] 9481K->2109K(19456K), 0.0003445 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

  age 1 + age 2 > 1024k(S区) * 0.5(TargetSurvivorRatio),所以 age 3 会晋级到老年代

  • parallel Scavenge 对象动态年龄判断:

  parallel Scavenge 会动态调整 new threshold 的值 ,发生 YGC 后,会把新生代中 age > new threshold 的对象移到老年代。

  调整规则如下

  1. young_gc_time > full_gc_time*1.1,则threshold降低。即YoungGC的时间太多,就降低 TenuringThreshold的值,让更多的对象进入老年代。
  2. full_gc_time > young_gc_time*1.1,则threshold提高。即Full GC的时间太多,则增加 TenuringThreshold的值,让更少的对象进入老年代。

引用 https://blog.csdn.net/lirenzuo/article/details/77529025

  测试代码:

-Xms20M
-Xmx20M
-Xmn10M
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-XX:+PrintTenuringDistribution
-XX:MaxTenuringThreshold=15

public class GCTest2 {

    public static void main(String[] args) {
        byte[] bytes2 = null;
        byte[] bytes3 = null;
        for (int i = 0; i < 60; i++) {
            byte[] bytes = new byte[1024 * 1024];
            byte[] bytes5 = new byte[1024 * 1024];
            if(i == 0) {
                byte[] bytes1 = new byte[1024 * 1024 * 8];
            }
            if(i == 22) {
                bytes2 = new byte[1024 * 1024 / 5]; // 第二次执行调整为 1024 * 1024 / 4
            }
            if(i == 28) {
                bytes3 = new byte[1024 * 1024 / 4];
            }
        }
        if( bytes2 != null || bytes3 != null) {

        }
    }
}

顺便要说的是,如果用的ps,那么因为没有用ageTable里面的那种计算方法,就算加了PrintTenuringDistribution也就不会打印对象age分布图

  这种机制比较适合一些本地缓存操作,比如你生成一个 map缓存,整个对象大小几十上百M,parallel scavenge 垃圾回收器可以适当减少分代年龄,让这种长期存活对象快速去老年代,以减少 ygc 的耗时。
  但这又会有另外一个场景,假如这个 map,1个小时/2个小时使用替换引用方式更新一次呢,这会造成上一次 map记录的对象在老年代全部变成垃圾对象了,这种情况怎么解决?

  • 空间分配担保机制:

  在发生 Minor GC 之后,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那下一次Minor GC可以确保是安全的。如果不成立,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,那下一次 Minor GC 可以确保是安全的,尽管下一次 Minor GC 是有风险的;如果小于,那这时就会提前触发进行一次 Full GC。

-Xms20M
-Xmx20M
-Xmn10M
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-server
public class GCTest3 {
    public static void main(String[] args) {
        byte[] bytes0 = new byte[1024 * 1024 * 2];
        byte[] bytes1 = new byte[1024 * 1024 * 2];
        byte[] bytes2 = new byte[1024 * 1024 * 2];
        bytes2 = null;

        for (int i = 0; i < 1; i++) {
            byte[] bytes3 = new byte[1024 * 1024 * 2];
            byte[] bytes4 = new byte[1024 * 1024 * 2];
            byte[] bytes5 = new byte[1024 * 1024 * 2];
            bytes5 = null;
            byte[] bytes6 = new byte[1024 * 1024 * 2];
        }
    }
}
触发full gc。

public class GCTest3 {
    public static void main(String[] args) {
        byte[] bytes0 = new byte[1024 * 1024 * 2];
        byte[] bytes1 = new byte[1024 * 1024 * 1];
        byte[] bytes2 = new byte[1024 * 1024 * 3];
        bytes2 = null;

        for (int i = 0; i < 1; i++) {
            byte[] bytes3 = new byte[1024 * 1024 * 2];
            byte[] bytes4 = new byte[1024 * 1024 * 1];
            byte[] bytes5 = new byte[1024 * 1024 * 3];
            bytes5 = null;
            byte[] bytes6 = new byte[1024 * 1024 * 2];
        }
    }
}
不触发full gc

  Parallel Scavenge收集器 提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数。

  Parallel Scavenge收集器 是怎么控制最大垃圾回收时间的呢?答:通过动态的控制 Eden 区大小来控制的。所以 -XX:MaxGCPauseMillis 值不是设置越小越好,设置太小,垃圾回收器完成不了任务;设置的小对于回收同样的垃圾,gc 触发的次数会更多,从而降低吞吐量

  由于与吞吐量关系密切,Parallel Scavenge收集器 也经常被称作“吞吐量优先收集器”。除上述两个参数之外,Parallel Scavenge收集器 还有一个参数 -XX:+UseAdaptiveSizePolicy,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)

3.5.4 G1

  完全不一样的分代模型,它开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。
  G1的内存分布如下图:
在这里插入图片描述

  • G1算法将堆划分为若干个等大区域(Region)1~32m,是2的幂次方;也可以通过-XX:G1HeapRegionSize=N 设置每个 Region 大小
  • 对于 Region 来说,它会有一个分代的类型,并且是唯一一个。即,每一个 Region,它要么是 Eden、survivor,要么是 old 的。还有一类十分特殊的 Humongous。
  • 一个对象如果超过 Region 大小的 50%以上,那么它将会直接用一个或多个连续的 Region 存放,这类存放了大对象的 Region 标记为 Humongous
      G1 适用于多处理器机器、大内存机器。可以做到可预测的停顿,根据用户设置符合实际的停顿时间来收集。那么它是怎么做到的?
      答:因为它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1的工作流程如下图
在这里插入图片描述

G1一些优化参数

  • -XX:+UseG1GC:使用G1垃圾回收器
  • -XX:MaxGCPauseMillis=N:设置最大停顿时间,默认值为200毫秒。
  • -XX:G1HeapRegionSize=N:设置每个region的大小,范围在1-32m,是2的幂次方
  • -XX:ParallelGCThreads=8:设置G1 GC回收线程数
  • -XX:ConcGCThreads=2:并发标记阶段,设置 gc 工作线程数量。设置过多影响用户线程的效率,设置过少,并发标记时间过长,可能导致这阶段不断触发 ygc 导致 old区满了而触发 Full gc
  • -XX:InitiatingHeapOccupancyPercent:默认是 45%,这个占比跟并发周期的启动相关,当空间占比达到这个值时,会启动并发周期。如果经常出现 FullGC,可以调低该值,尽早的回收可以减少 FullGC 的触发,但如果过低,则并发阶段会更加频繁,降低应用的吞吐。
  • -XX:G1MixedGCLiveThresholdPercent:在混合垃圾收集周期中的old region的占用阈值,默认值为85,意思是如果一个 Old Region 中的存活对象大于 Region 大小的85%的话,就不去回收这个Region,不加入Cset。否则回收时将85%的存活对象放入另一个Region中,得不偿失。
  • -XX:G1MixedGCCountTarget = 8:设置在标记周期完成之后混合收集的次数,默认是 8次。如果单次回收 CSet + 年轻代时间远远超过最大停顿时间,则会触发多次 mixed GC 每次回收部分 CSet,以达到最大停顿时间目的
  • -XX:G1HeapWastePercent=5:设置浪费的堆内存百分比,在全局并发标记结束后统计出所有可被回收的垃圾占Heap的比例值,如果不超过 5%,那么就不会触发 Mixed GC。也就是并发标记后,并不一定会触发mixed GC。

G1一些要注意的问题

  1. 就内存占用来说,虽然 G1 和 parallel 都使用卡表来处理跨代指针,但 G1 的卡表实现更为复杂,而且堆中每个 Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致 G1 的记忆集(和其他内存消耗)可能会占整个堆容量的 20% 乃至更多的内存空间

  2. Humongous引发的问题:

  • 任何超过 region 大小一半的对象都被视为巨型对象。这些巨型对象直接分配到 humongous regions 中,并且是连续的 region。假如巨型对象过多,堆内存即时空闲很多,但不够完整,没有连续的 region 存放巨型对象,则会触发 to-space exhausted(空间耗尽),连续的 to-space exhausted则可能发生 full gc。

  • 在分配大对象 region 区之前,G1 会先判断是否达到开启标记周期的阈值,在必要时会启动并发标记周期。
    解决办法:通过-XX:G1HeapRegionSize=N 增大单个 region 的大小or加大堆内存等。PS:少量的大对象还是没问题的。

  • to-space exhausted(空间耗尽):ygc 新生代对象复制到老年代发现内存不足,一般可能是并发标记太晚了。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值