JVM 垃圾回收

文章目录

    • 1. 如何判断垃圾
        • 1.1 引用计数
        • 1.2 可达性分析
        • 1.3 哪些对象可以作为根对象?
    • 2. 四种引用
        • 2.1 强引用
        • 2.2 软引用(SoftReference)
        • 2.3 弱引用(WeakReference)
        • 2.4 虚引用
    • 3. 垃圾回收算法
        • 3.1 标记清理(Mark Sweep)
        • 3.2 标记整理(Mark Compact)
        • 3.3 复制(Copy)
        • 3.4 分代回收
    • 4. 垃圾回收器
        • 4.1 串行
        • 4.2 吞吐量优先
        • 4.3 响应时间优先
        • 4.4 G1
    • JVM调优
    • 小结

1. 如何判断垃圾

1.1 引用计数

给对象设置一个引用计数属性,引用每新增1次计数加1,引用每释放1次计数减1。但是循环引用的对象没法回收。Java中没有采用这种方式。

// 循环引用
public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;

    }
}

1.2 可达性分析

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
JVM 垃圾回收_第1张图片

1.3 哪些对象可以作为根对象?

JVM中的根对象分为这几类:

  • 【System Class】启动类加载器加载的类对象,如Object、System、String等核心类对象
  • 【Native Class】本地方法栈中JNI引用的对象,JVM运行的时候会调用系统方法,系统方法会引用一些Java对象,这些被引用的Java对象也是根对象,比如Class、String等
  • 【Busy Monitor】锁对象,如Lock
  • 【Thread】活动线程中引用的对象,比如主线程中某个方法的局部变量List list = new ArrayList<>(); 这个list是个局部变量,存在栈中,实例化的那个对象放在堆中,这里的根对象指的是堆上的那个实例对象。也就是引用保存在Java栈中,而真正的引用的对象(ArrayList)保存在Java堆中。

2. 四种引用

从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

2.1 强引用

  • 强引用是指常见的普通对象引用,只要还有强引用指向一个对象,这个对象就不会被垃圾收集器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.2 软引用(SoftReference)

  • 软引用所引用的对象,只有当JVM认为内存不足时,才会去回收它。通常,JVM会在抛出内存溢出错误前,清理软引用指向的对象。
  • 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用自身加入到与之关联的引用队列中,后续清理掉软引用自身这个变量。
  • 软引用通常用来实现内存敏感的缓存,如果内存足够,可以保留缓存,当内存不够的时候就适当清理缓存,这样的好处就是能够使用缓存,也不会耗尽内存。

软引用使用场景示例

先将虚拟机栈内存设置为20M:-Xmx20m
设置VM参数查看GC详情:-XX:+PrintGCDetails -verbose:gc

public class Main{
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        List<byte[]> list = new ArrayList<>();
        for(int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }
        System.in.read();
    }
}

如果执行上面这段程序,会触发栈内存溢出错误:
在这里插入图片描述
如果此时我们需要读取一堆图片到这个list集合中,而读取这些图片并不是核心业务,如果使用强引用就会导致内存溢出,像这种不是很重要的资源,在内存紧张的时候其实是可以释放掉的。下面改成软引用:

public class Main{
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for(int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> softReference : list) {
            System.out.println(softReference.get());
        }
    }
}

JVM 垃圾回收_第2张图片
一次GC之后,发现内存不够,会再次触发GC回收软引用占用的内存,所以最后看到前4个变为了null。
最后结合引用队列,将值为null的软引用自身进行清理:

public class Main{
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        //引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for(int i = 0; i < 5; i++) {
            // 关联了引用队列,当软引用所引用的byte[]数组被回收时,
            // 软引用自身会加入到queue中
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println("循环结束:" + list.size());

        // 从引用队列中取软引用自身,然后进行清理
        Reference<? extends byte[]> poll = queue.poll();
        while(poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }
        for (SoftReference<byte[]> softReference : list) {
            System.out.println(softReference.get());
        }
    }
}

JVM 垃圾回收_第3张图片

2.3 弱引用(WeakReference)

  • 弱引用所引用的对象,在发生垃圾回收时,无论内存是否足够,都会回收相应的对象。
  • 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中,后续清理掉弱引用自身这个变量。

弱引用和软引用的区别在于:弱引用所引用的对象生命周期更短,无论内存是否足够,垃圾回收器都会回收弱引用所引用的对象。

  • 弱引用的一个使用场景(引用自:https://www.jianshu.com/p/825cca41d962):
static Map<Object,Object> container = new HashMap<>();
public static void putToContainer(Object key,Object value){
    container.put(key,value);
}

public static void main(String[] args) {
    //某个类中有这样一段代码
    Object key = new Object();
    Object value = new Object();
    putToContainer(key,value);

    //..........
    /**
     * 后来程序员发现这个key指向的对象没有用了,
     * 为了节省内存打算把这个对象抛弃,然而下面这个方式真的能把对象回收掉吗?
     * 由于container对象中包含了这个对象的引用,所以这个对象不能按照程序员的意向进行回收.
     * 并且由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值 对无法从映射中删除。
     * 很可能会造成内存泄漏。
     */
    key = null;
}

解决上述内存泄漏的办法之一是使用WeakHashMapWeakHashMap是通过WeakReference和ReferenceQueue实现的,使用table保存键值对。WeakHashMap的key是“弱键”,即WeakReference类型,ReferenceQueue是一个队列,保存被GC回收的“弱键”。
当某个“弱键”不再被其他对象引用并被GC回收时,这个“弱键”会被添加的ReferenceQueue中,当下次操作WeakHashMap时,会根据ReferenceQueue中记录的“弱键”去删除table中被回收的键值对。

2.4 虚引用

  • “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。无法通过虚引用访问对象的任何属性或方法。
  • 必须配合ReferenceQueue使用
  • 虚引用只是用来提供一种确保对象被回收之后能做些事情。比如NIO中的缓冲区ByteBuffer,因为ByteBuffer内存可以分配在直接内存上,直接内存不由JVM直接管理,JVM会用一个虚引用指向这块直接内存,当虚引用被回收时,会将虚引用自身加入ReferenceQueue中,然后后台线程ReferenceHandler会执行相应的方法对直接内存进行清理回收

3. 垃圾回收算法

3.1 标记清理(Mark Sweep)

  • 速度快
  • 会造成内存碎片

JVM 垃圾回收_第4张图片

3.2 标记整理(Mark Compact)

  • 速度慢
  • 没有内存碎片

JVM 垃圾回收_第5张图片

3.3 复制(Copy)

  • 不会有内存碎片
  • 需要占用双倍内存空间

JVM 垃圾回收_第6张图片

3.4 分代回收

JVM的做法是协同上面三种做法,把整个堆内存分为新生代和老年代,新生代存放用完即可丢弃的对象,老年代存放需要长时间使用的对象。新生代又划分成了幸存区From和幸存区to。这样,就可以针对不同对象的生命周期特点,采用不同的回收机制。
JVM 垃圾回收_第7张图片
分代回收步骤:

  1. 新对象首先分配在Eden区
  2. 当Eden去内存不足时,会触发minor gc。Eden区和幸存区from中存活的对象复制到幸存区to,并且将幸存区from中存活的对象存活年龄加1,然后清理Eden区和幸存区from中剩下无用对象。最后交换幸存区from和幸存区to的位置,即让幸存区to指向空的区域
  3. 当幸存区对象存活年龄超过阈值(默认是15)的时候,会将这个对象晋升到老年代
  4. 当一个对象不足够放在新生代时,会尝试往老年代存放,老年代也放不下时,会先尝试触发minor gc,如果此时空间还是不足,就触发full gc,stw时间更长。
  • 新生代采用复制算法,老年代采用标记-清除或者标记-整理算法
  • Eden区和幸存区大小默认是8:1,比如10M内存,8兆给Eden区,剩下2M分别给两个幸存区
  • minor gc 会触发stop the world,暂停用户线程,让垃圾回收线程去执行。因为垃圾回收过程会涉及对象的移动,如果用户线程还在执行,就会导致无法相应的内存。对于新生代stw时间较短。
  • 幸存区对象晋升到老年代也不一定非要达到阈值,当幸存区空间紧张的时候,对象会提前晋升到老年代
  • 大对象会直接放到老年代,此时不会触发GC或者Full GC

相关VM参数
JVM 垃圾回收_第8张图片
一个线程内的OutOfMemoryError不会导致整个进程的退出
JVM 垃圾回收_第9张图片

4. 垃圾回收器

串行

  • 单线程的垃圾回收器,当垃圾回收发生时,其他线程都停止,然后这个单线程来进行GC
  • 在单个CPU并且需要回收的内存区域很小的情况下,效率很高

吞吐量优先

  • 多线程
  • 适合和用户线程交互少的后台计算上,因为它的响应速度并不是很好
  • 在单位时间内stop the world时间最短

响应时间优先

  • 多线程
  • 适合用于和用户交互的业务
  • 和吞吐量优先不同的是,尽可能让单次的stop the world时间最短

举个例子,吞吐量优先垃圾回收器:单次STW时间是0.2s,单位时间内只进行了2次,总共使用了0.4s。 响应时间优先垃圾回收器:单位时间内触发了5次垃圾回收,让单次STW时间最短,是0.1s,单位时间内使用了0.5s。总体来看,吞吐量优先垃圾回收器在单位时间上优于响应时间优先垃圾回收器。

吞吐量可以理解为使用用户线程执行时间/(用于线程执行时间+垃圾收集器线程执行时间)。

4.1 串行

开启串行垃圾回收器:-XX:+UseSerialGC = Serial + SerialOldSerial工作在新生代,采用复制算法。SerialOld工作在老年代,采用标记+整理算法。新生代和老年代的垃圾回收器是分别运行的。
下图是串行垃圾回收器的工作流程,新生代和老年代都是这个流程。只有一个垃圾回收器线程在运行,运行期间,其他线程都要阻塞。
JVM 垃圾回收_第10张图片

4.2 吞吐量优先

JVM 垃圾回收_第11张图片
JVM 垃圾回收_第12张图片
参数说明:

  • -UseParallelGC: 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收。-UseParallelOldGC: 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行垃圾回收。新生代采用复制算法,老年代采用标记-整理算法。
  • - XX:+UseAdaptiveSizePolicy参数打开之后,就不需要手动指定新生代的大小,Eden和Survivor区的比例,晋升老年代对象等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式成为GC自适应的调节策略
  • 根据一段时间内垃圾回收所花费的时间调整堆的大小
  • 最大stop the world 时间,默认200ms,和第三个参数配合使用
  • 设置线程数。垃圾回收线程个数默认和CPU个数相同,垃圾回收过程中,CPU占用率达到100%。

4.3 响应时间优先

JVM 垃圾回收_第13张图片
参数说明:

  • ConcMarkSweep简称CMSC代表concurrent,并发,指某些时刻垃圾回收器线程和用户工作线程并发运行,都要去抢占CPU,这种方式减少了stop the world时间。UseConcMarkSweepGC运行在老年代,采用标记+清除算法;UseParNewGC运行在新生代,采用复制算法;SerialOld运行在老年代,采用标记+整理算法。
    【CMS并发失败】 由于CMS采用标记+清除算法,可能产生很多内存碎片,老年代就会因为碎片多导致无法分配大对象,这样的话CMS并发就会失败,CMS收集器就会退化到SerialOld收集器,基于标记+整理算法。一旦退化到SerialOld收集器,CMS的响应时间就会达到很高,这就是CMS最大的一个问题。
  • 并行线程数(一般设置为CPU核数);并发线程数:(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时,垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU数量不足4个时,CMS对用户程序的影响就可能变的很大。如果CPU负载本来就很大,还要分出一半的运行能力去执行收集器线程,就可能导致用户程序的执行能力下降50%。
    因此,虽然UseConcMarkSweepGC能够做到响应时间优先,但是由于CPU执行时间要分给垃圾收集线程,导致用户线程吞吐率收到影响
  • CMS执行时机,当老年代内存占用达到百分之percent的时候执行
  • 由于在重新标记阶段,新生代可能还有对象引用老年代对象,这就需要对新生代对象到老年代对象做可达性分析,但是新生代对象很多,可达性分析就会耗费很多时间。因此通过最后这个参数,表示在CMS重新标记之前,对新生代做一次垃圾回收,也就是触发UseParNewGC执行,这样,新生代对象少了,就能提高重新标记的性能。

JVM 垃圾回收_第14张图片
UseConcMarkSweepGC工作流程分为四步

  1. 初始标记,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。此时需要stop the world。
  2. 并发标记,和用户线程并发执行。标记GC roots 关联到的对象 的引用对象有哪些。比如说 A -> B (A 引用 B,假设 A 是 GC Roots 关联到的对象),那么这个阶段就是标记出 B 对象, A 对象会在初始标记中标记出来。
  3. 重新标记,修正并发标记期间因用户程序继续执行而导致标记产生变动的那一部分对象的标记记录,以及并发标记期间用户线程可能产生的新对象,重新标记就是对这些对象进行标记。此时需要stop the world。
  4. 并发清理,和用户线程并发执行,回收所有的垃圾对象。

由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。特点是并发收集、低停顿。

CMS详细内容可以参考:
https://www.jianshu.com/p/86e358afdf17
http://blog.sina.com.cn/s/blog_df25c55f0102wxh3.html

4.4 G1

Carbage First,JDK9默认的垃圾收集器。G1兼顾了吞吐量和响应时间。并发执行。
适用场景:

  • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region。每个Region都可以独立的作为Eden、幸存区、老年代、巨型对象区域
  • 整体上是标记+整理算法,两个Region之间采用复制算法

使用-XX:+UseG1GC开启G1

1. G1垃圾收集阶段

  • 在Young GC时会进行GC Root的初始标记
  • 当老年代占用堆空间比例达到阈值时(阈值默认是45%),在新生代垃圾收集的同时进行并发标记(不会STW)
  • 进行混合收集,收集新生代和老年代垃圾,之后再次回到新生代垃圾收集

整体流程:
JVM 垃圾回收_第15张图片
2. Young Collection

工作一段时间后,Eden内存紧张,就会触发新生代垃圾回收(短暂的STW)。具体步骤是:

  • 把Eden区和幸存区的存活对象复制到另一个幸存区中
  • 当幸存区中的对象年龄超过阈值时,会晋升到老年代

注:下面的图少了一个巨型对象区域,G1中的巨型对象是指,占用了Region容量的50%以上的一个对象。Humongous区,就专门用来存储巨型对象。如果一个H区装不下一个巨型对象,则会通过连续的若干H分区来存储。因为巨型对象的转移会影响GC效率,所以并发标记阶段发现巨型对象不再存活时,会将其直接回收。

JVM 垃圾回收_第16张图片

3. Young Collection + CM(并发标记)

  • 在Young GC是会进行GC Root的初始标记,标记的是GC Root直接引用的对象
  • 当老年代占用堆空间比例达到阈值时(阈值默认是45%),在新生代垃圾收集的同时进行并发标记(不会STW),标记的是GC Root直接引用的对象 所引用的对象

JVM 垃圾回收_第17张图片
4. Mixed Collection

这个阶段会对Eden(E)、Survival(S)、Old(O)进行全面垃圾回收
JVM 垃圾回收_第18张图片
具体过程是:

  • 最终标记(重新标记),修正并发标记期间因用户程序继续执行而导致标记产生变动的那一部分对象的标记记录,以及并发标记期间用户线程可能产生的新对象,重新标记就是对这些对象进行标记。
  • 把Eden区和Survival区的存活对象复制到一个幸存区中,同时,Survival区中年龄达到阈值的对象会晋升到老年代。这是混合收集阶段的新生代垃圾回收。
  • 部分老年代的存活对象复制到一个老年代中。之所以是一部分,是因为G1会根据设置的最大暂停时间选择回收价值最高的垃圾进行回收,否则,如果堆内存大,老年代垃圾多,全部回收就会导致超过最大暂停时间。但是如果回收所有的垃圾也能在最大暂停时间内完成,还是会回收所有老年代垃圾的。

老年代的垃圾收集和CMS类似,G1老年代占用达到阈值时,会进行并发收集,当并发收集速度赶不上垃圾产生速度时,就会退化为串行收集,需要更长时间的STW,导致响应时间变慢

老年代优先选择回收的机制就是Garbage First的名字由来

5. 新生代垃圾回收时的跨代引用问题

对新生代进行垃圾收集进行可达性分析的时候,新生代对象的根对象可能来自老年代,老年代的对象很多,如果去遍历整个老年代查找新生代的根对象,效率很低。

G1的做法是采用卡表技术,将老年代区域再进行细分成一个个的Card,每个Card大约是512k,如果老年代中的某个对象引用了新生代对象,那么对应的Card称为脏卡,这样就不用去遍历整个老年代对象,而是遍历脏卡区域对象即可,提高效率。

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

维护记忆集数据的正确性采用的方法是写入屏障,当对象的引用关系发生变更时,都要去更新脏卡,这是个异步操作,更新脏卡的任务是放在一个队列中,由后台线程去完成。

下图中粉色区域就是脏卡区域:
JVM 垃圾回收_第19张图片
6. Remark(重新标记)

并发标记阶段之后要进行重新标记,修正并发标记期间因用户程序继续执行而导致标记产生变动的那一部分对象的标记记录,以及并发标记期间用户线程可能产生的新对象,重新标记就是对这些对象进行标记。
并发标记采用三色标记法:

  • 白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
  • 灰色:正在搜索的对象
  • 黑色:搜索完成的对象(不会当成垃圾对象,不会被GC)

JVM 垃圾回收_第20张图片

  • 标记A为灰色,并标记A所引用的对象B为灰色,A标记完成后变成黑色(C不是A的直接引用,所以此时不会搜索到,等并发标记B的时候才会处理C)
  • 当标记完A后,开始标记B,此时用户线程把A -> B -> C 改成了 A -> C,同时 B 不再引用C。标记B的时候,发现B没有引用的对象了,B标记完成变成黑色

此时C还是白色,但是引用它的A已经是黑色了,不会再次进行并发标记了。解决这个问题就用到写入屏障机制,当对象间的引用关系发生了变化(比如A -> B -> C 改成了 A -> C),写入屏障机制就会把C加入到一个队列中,并把C变成灰色,表示C还没处理完成。等到整个并发标记过程结束进入重新标记阶段后,重新标记检测这个队列,发现有对象被引用,就会处理这些对象,最终变成黑色。

JVM调优

  • 新生代的内存分配多大比较好?
    新生代设置的太小,会触发频繁的minor gc,如果设置的太大,那么老年代内存就相对变小,新生代对象晋升到老年代或者大对象直接分配到老年代内存不足就很可能会触发full gc,full gc的代价更大。Oracle官方建议的是新生代大小为堆内存大小的1/4~1/2倍
  • 幸存区内存分配多大比较好?
    幸存区大小应该能保留 当前活跃对象+需要晋升的对象。如果幸存区太小,会导致存活对象的年龄还没达到阈值就晋升到老年代,只有等到full gc的时候这个对象才会被回收,这样就毫无意义的延长了对象的生存时间。
  • 晋升阈值适配得当,让长时间存活对象尽快晋升
    如果对象的晋升阈值太大,因为新生代一般采用复制算法,如果一个对象长时间存在新生代中,复制的开销就会增大。默认是15。
    JVM 垃圾回收_第21张图片
    图中第一个参数为显示设置阈值的最大值,第二个参数打印出下面系统中新生代对象的信息,分别是年龄、当前这个对象的大小、所有对象的总大小。通过观察这些值,来合理设置阈值。
  • 老年代调优
    • CMS老年代内存越大越好。
      CMS用于老年代,采用标记-清除算法,由于CMS垃圾收集线程执行期间,用户线程也会执行,这样就会产生“浮动垃圾”,新生代对象晋升到老年代的速度就会更快,就需要更大的老年代预留空间存储这些对象,否则,如果老年代内存不足,就会造成“CMS并发失败”,然后退化成SerialOld垃圾收集器,这个效率就会降低很多。
    • 观察发生full gc时 老年代的内存占用,将老年代内存预设调大原来的1/4~1/3倍
    • 这个值表示当老年代垃圾对象占用老年代内存百分之多少时,会触发老年代用CMS收集器进行回收在这里插入图片描述

案例分析

(1)Full GC 和 Minor GC频繁
分析:新生代内存很快就满了,会导致频繁的minor gc,也会导致对象提前晋升到老年代,这样的话,老年代就会有很多生命周期短的对象,导致老年代空间很快被占满,然后触发full gc。
解决:用监测工具查看新生代空间大小、对象晋升情况等,适当增大新生代内存、幸存区大小、阈值。

(2)请求高峰期发生Full GC,单次暂停时间特别长(业务需要低延迟,选择的是CMS)
分析:CMS四个阶段中,最慢的是重新标记,因为重写标记要扫描新生代和老年代。在业务高峰期的时候,新生代对象产生的速度比较快,扫描的时间就会增长。
解决:通过查看GC日志,可以看到每个阶段耗费的时间,如果真是重新标记耗费时间长,可以通过下面这个参数设置在重新标记发生之前先对新生代做一次垃圾回收。在这里插入图片描述

(3)老年代充裕的情况下,发生Full GC(CMS JDK7)
JDK7中采用永久代作为方法区实现,永久代内存不足也会触发Full GC。而JDK8中,采用元空间作为方法区实行,垃圾回收不由GC这边控制和管理,而且元空间一般比较充裕。可以增大元空间的初始值和最大值

小结

用线连接的垃圾收集器表示可以组合使用
JVM 垃圾回收_第22张图片

  • 红色线在JDK8中弃用(弃用但是依旧能用)了,在JDK9中移除了
  • 绿色线在JDK14中弃用了
  • CMS在JDK9中被弃用,在JDK14中删除了

JDK8中用的是Parallel Scavenge+Parallel Old GC;G1是JDK9中默认的GC,直到目前JDK14默认也是用的G1;最强大的ZGC目前还在预使用期。

你可能感兴趣的:(Java,#,JVM)