JVM 垃圾回收 超详细学习笔记(二)

本篇博客是记录个人学习JVM的笔记,学习的视频是哔哩哔哩中 黑马的jvm教程;

视频链接:jvm学习

上一篇文章:JVM 内存结构 超详细学习笔记(一)未来很长,别只看眼前的博客-CSDN博客 

下一篇文章:JVM 类加载机制 超详细学习笔记(三)_未来很长,别只看眼前的博客-CSDN博客

目录

1、如何判断对象是否可以回收

引用计数法

可达性分析算法(Java中使用的)

Java中的四种引用

软引用的使用案例

2、垃圾回收算法

标记清除

标记整理

复制

3、分代垃圾回收

回收流程

回收流程总结

JVM相关的参数

垃圾回收案例演示

4、垃圾回收器

串行Serial Old

吞吐量优先Parallel Scavenge

响应时间优先CMS(重点)

G1(重点)

G1垃圾回收阶段:

Young Collection:

Young Collection + CM:

Mixed Collection:

Full GC:

Young Collection 跨代引用:

Remark

G1的一些优化

JDK 8u20 字符串去重

JDK 8u40 并发标记类卸载

JDK 8u60 回收巨型对象

JDK 9 并发标记起始时间的调整

5、垃圾回收调优

调优领域

最快的GC是不发生GC

新生代,幸存区调优

老年代调优

三个调优案例


1、如何判断对象是否可以回收

引用计数法

引用计数法:就是只要这个对象被其他对象所引用,那么我就对这个对象进行加一,如果被引用了两次那么就变成了2,当这个对象的引用计数变为0了,那么就意味着没有其他对象再引用它了,那么就可以把它进行垃圾回收;

弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放

Java没有使用这种方法来判断对象是否为垃圾;

可达性分析算法(Java中使用的)

  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收

  • 可以作为 GC Root 的对象

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象(这里说的对象都是指new出来存储在堆中的对象)

    • 方法区中类静态属性引用的对象

    • 方法区中常量引用的对象

    • 本地方法栈中 JNI(即一般说的Native方法)引用的对象

package gc;
import java.io.IOException;
import java.util.ArrayList;
public class Demo1 {
    public static void main(String[] args) throws IOException {

        ArrayList<Object> list = new ArrayList<>(); //注意我们平常说的对象是指堆里面的对象new ArrayList<>(), 这里面的list指的是引用对象(是一个【局部变量】,存储在活动栈中的)
        list.add("a");
        list.add("b");
        list.add(1);
        System.out.println(1);
        System.in.read(); //程序在这里会停止继续往下执行,在控制台敲回车才会继续往下执行

        list = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end");
    }
}

对于以上代码,可以使用如下命令将堆内存信息转储成一个文件,然后使用Eclipse Memory Analyzer(Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation) 工具进行分析。

第一步:使用 jps 命令,查看程序的进程

 第二步:使用 jmap -dump:format=b,live,file=1.bin 21096 命令转储文件

(dump:转储文件,format=b:二进制文件,live表示只要存活的,file表示转存的文件名和路径,这里使用的是相对路径,21096 :进程的id)

第三步:打开 Eclipse Memory Analyzer 对 1.bin 文件进行分析。  

分析 gc roots,找到了 ArrayList 对象,然后将 list 置为null,再次转储,那么 list 对象 就会被回  收。

Java中的四种引用

其实是有五种;

我们平常创建的对象几乎都是强引用;

1.  强引用 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

如上图B、C对象都不引用A1对象时,A1对象才会被回收;

2.  软引用(SoftReference) 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 可以配合引用队列来释放软引用自身

如上图如果B对象不再引用A2对象,且在一次垃圾回收后内存仍然不足时,软引用所引用的A2对象就会被回收;需要注意的是:如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理,如果想要清理软引用,需要使用引用队列

3.  弱引用(WeakReference) 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身

4.  虚引用(PhantomReference) 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时(因为被引用对象被回收后,其关联的直接内存是无法被直接释放的,因为这个直接内存不受jvm管控,所以为了回收这个直接内存就需要这个虚引用和队列配合起来使用),会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

5.  终结器引用(FinalReference) 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,先让终结器引用入队(被引用对象此时还没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收终结器指向的引用对象。 而且这个Finalizer 的优先级很低,可能会导致终结器引用指向的对象迟迟不被回收;所以在实际开发中不推荐使用Finalizer 来回收垃圾;

软引用的使用案例

/**
 * 演示 软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2 {

    public static int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        method2();
    }

    // 设置 -Xmx20m , 演示堆内存不足,
    public static void method1() throws IOException {
        ArrayList<byte[]> list = new ArrayList<>();

        for(int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }
        System.in.read();
    }

    // 演示 软引用
    public static void method2() throws IOException {
        //使用软引用对象 list和SoftReference之间是强引用,而SoftReference和byte数组之间则是软引用
        ArrayList<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[]> ref : list) {
            System.out.println(ref.get());
        }
    }
    
}

method1 方法演示解析:

首先会设置一个堆内存的大小为 20m(通过设置jvm参数来设置的),然后运行 mehtod1 方法,会抛异常,堆内存不足,因为 mehtod1 中的 list 都是强引用,不会触发垃圾回收。

method2 方法演示解析: 在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。细节如图:

先为jvm设置参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc

我们可以从上面的图片发现虽然软引用指向的对象被垃圾回收了,但是软引用这个引用本身还是在集合中的,我们想把这个内存也给释放掉(毕竟还是占内存的), 所以就可以配置队列来把它进行回收;

// 演示 软引用 搭配引用队列
    public static void method3() throws IOException {
        ArrayList<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());
        }

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while(poll != null) {
            list.remove(poll);
            poll = queue.poll(); //再从队列中获取软引用
        }

        System.out.println("=====================");
        for(SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }

程序执行完后内存情况(这个执行结果是没有使用队列来清除软引用本身的):

2、垃圾回收算法

标记清除

沿着GC root对象往下寻找,看有没有和gc root对象建立联系,如果没有就可以把这个对象标记为垃圾;

工作步骤:先标记,然后再清除;

  • 优点:速度较快

  • 缺点:会产生内存碎片(因为空间不联系,可能会导致空间不能被合理的利用从而产生内存碎片) 

标记整理

标记-整理 会将不被GC Root引用的对象回收,清楚其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是因为需要移动对象(这个过程会导致对象地址发生变化)的空间来进行整理所以需要消耗一定的时间,所以效率较低

  • 缺点:速度慢

  • 优点:没有内存碎

复制

 将内存分为相等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。

  • 不会有内存碎片

  • 需要使用双倍的内存空间

3、分代垃圾回收

长时间使用到的对象放在老年代中,而把那些用完了就可以丢弃的对象放入到新生代中,并且新生代进行垃圾回收可以更加频繁的进行;

回收流程

1.新创建的对象都被放在了新生代的伊甸园

 2.当伊甸园中的内存不足时,通过可达性算法分析哪些对象可以作为垃圾进行回收,然后进行一次垃圾回收,这时的回收叫做 Minor GC

 3.Minor GC 会将伊甸园和幸存区FROM【存活的对象】复制到 幸存区 TO中, 并让其寿命加1,再交换两个幸存区(交换的时候地址值不变,就是地址值得指向发生了变化)

 4.进行第二次垃圾回收:

然后又经过一段时间使用内存,导致再次创建对象的时候新生代的伊甸园又满了(或者是不能提供新建对象所需要的空间),则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1; (这里的寿命是指经过垃圾回收的次数还没有被清理的对象,新生代的对象的寿命会有一个阈值,当某个对象的寿命超过了这个阈值(最大为15,4bit),那么这个对象就会被放到老年代中)

 5.如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收;

回收流程总结

  • 新创建的对象首先分配在 伊甸园 区

  • 新生代空间不足时,触发 minor gc ,伊甸园区 和 from 区存活的对象使用 - copy(复制算法) 复制到 to 中,存活的对象年龄加一,然后交换 from to

  • minor gc 会引发 stop the world,暂停其他线程,等垃圾回收结束后,再恢复用户线程运行

  • 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit,因为这个是保持在对象头中,对象头比较金贵,不能把数值设置过大)

  • 当老年代空间不足时,会先触发 minor gc,如果空间仍然不足,那么就触发 full fc ,停止的时间更长(因为老年代清理垃圾的频率比较低,所以可能存储了更多可以被回收的对象)!

  • 补充:直接将大对象晋升到老年代:就是当每一个对象的大小直接超过了新生代的空间,也就是说新生代再怎么垃圾回收还是不能把这个新创建的对象放进去,所以这个时候是不会进行垃圾回收的,而是直接把大对象晋升到老年代;

JVM相关的参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

垃圾回收案例演示

要求:可以看懂垃圾回收打印的日志;

package gc;

import java.util.ArrayList;
import java.util.List;
public class GcDemo {
        private static final int _512KB = 512 * 1024;
        private static final int _1MB = 1024 * 1024;
        private static final int _6MB = 6 * 1024 * 1024;
        private static final int _7MB = 7 * 1024 * 1024;
        private static final int _8MB = 8 * 1024 * 1024;

        // -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
        public static void main(String[] args) {
            List<byte[]> list = new ArrayList<>();
            list.add(new byte[_7MB]);
            list.add(new byte[_1MB]);
            list.add(new byte[_6MB]);
            list.add(new byte[_512KB]);
            list.add(new byte[_6MB]);
        }
}

1.先把main方法中的代码全部注释掉,然后运行:

 2.往集合中添加一个7mb大小的对象:

 3.再次往集合中添加一个1mb的对象:

4.直接将大对象晋升到老年代:就是当每一个对象的大小直接超过了新生代的空间,也就是说新生代再怎么垃圾回收还是不能把这个新创建的对象放进去,所以这个时候是不会进行垃圾回收的,而是直接把大对象晋升到老年代;  

 5.如果进行了gc但是还是内存不够就会导致OOM ,不过在抛出OOM异常之前JVM还是会进行一次自救:

6.注意:如果刚刚那一段代码是在另一个线程中运行,那么该线程报OOM后会不会影响主线程的继续运行呢?

答案是不会,主线程会继续往下执行; 所以这个说明了一个线程的OOM是不会导致整个程序都停止的

4、垃圾回收器

相关概念:

  • 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

  • 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上

  • 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。

串行Serial Old

  • 单线程

  • 内存较小,个人电脑(CPU核数较少)

-XX:+UseSerialGC=serial + serialOld

 进行垃圾回收的时候为什么需要让其他线程停下:因为在进行垃圾回收的时候可能(标记整理会导致地址值得的变化)会导致地址的改变

在垃圾回收线程运行的时候其他线程都需要阻塞,等垃圾线程运行结束后,其他用户线程才能恢复运行;

安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象, 因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

吞吐量优先Parallel Scavenge

  • 多线程

  • 使用场景:堆内存较大,需要多核cpu支持 (适合工作在服务器上)

  • 单位时间内,STW的时间最短

  • JDK1.8默认使用的垃圾回收器

特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)

GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间

  • XX:GCRatio 直接设置吞吐量的大小

-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC   //设置使用其中一个,另一个会自动关联使用
-XX:+UseAdaptiveSizePolicy  //自适用调整伊甸园和幸存区的比例
-XX:GCTimeRatio=ratio // 主要是调整吞吐量的目标,比如你可以设置相关的目标值,如果达不到你设置的目标,那么jvm就会动态的去调整  公式:1/1+ratio  一般这个ratio的默认值是99,但是这个很难达到,取99的时候表示100分钟里面有1分钟都在暂停进行垃圾回收(这个很难达到),我们这个ration一般是设置19(表示100分钟内有5分钟都在暂停进行垃圾回收)
-XX:MaxGCPauseMillis=ms // 最大暂停的时间,最大值是200ms  这个和上面那个是相冲突的,只能根据自己的业务去取一个折中
-XX:ParallelGCThreads=n

响应时间优先CMS(重点)

Concurrent Mark Sweep(清除),一种以获取最短回收停顿时间为目标的老年代收集器

  • 特点:基于【标记-清除】算法实现。并发收集、低停顿,但是会产生内存碎片

  • 应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务

  • CMS 收集器的运行过程分为下列4步:

    • 初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。

    • 并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。

    • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题

    • 并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会并发失败然后就会退化为 serial Old(串行) 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!(这个是cms最大的一个缺点)

虚拟机参数:

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld  //可以并发执行,就是这个垃圾回收线程可以和用户线程在某些时刻并发执行
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads //第一个是指并行的时候的垃圾回收线程数,并行线程数一般设置与cpu核数相同,第二个参数是并发线程数,并发线程数一般设置为并行线程数的1/4
-XX:CMSInitiatingOccupancyFraction=percent  //控制什么时候来进行cms垃圾回收的, 表示执行cms的时候的内存占比, 比如设置为80,表示的就是老年代的内存使用到80%就进行一次cms垃圾回收,这样可以留一些空间给那些浮动垃圾
-XX:+CMSScavengeBeforeRemark  //在重新标记阶段,可能会出现新生代的对象引用老年代的对象,那么就会出现先去老年代中查找引用,然后因为新生代垃圾回收比较频繁,然后又把新生代的一些对象给清除了,这样就会导致前面做一些无用功       这个参数就是用来避免这种情况的,【这个参数的作用是在进行重新标记之前进行一次垃圾回收,这样就可以把新生代的一些垃圾给回收,这样当我们扫描对象的时候就可以少扫描一些对象】

 CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。

  • 响应时间

  • 使用场景:堆内存较大,需要多核cpu支持(适合工作在服务器上)

  • 尽可能让单次STW的时间最短

G1(重点)

定义: Garbage First 适用场景:

  • 【同时】注重吞吐量和低延迟(响应时间)

  • 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域

  • 整体上是标记-整理算法,两个区域之间是复制算法

JDK 9以后默认使用,而且替代了CMS 收集器;

JDK8 并不是默认开启的,所需要参数开启:

-XX:+UseG1GC
-XX:G1HeapRegionSize=size   //设置区域的大小
-XX:MaxGCPauseMillis=time

G1垃圾回收阶段:

Young Collection:

下面图中的字母代表:

E:伊甸园 S:幸存区 O:老年代

  • 会触发STW(stop the world)

Young Collection + CM:

这个阶段是新生代回收和并发标记阶段; CM:concurrent Mark

  • 在 Young GC (新生代GC就发生了)时会对 GC Root 进行初始标记

  • 在老年代占用堆内存的比例达到阈值时,进行并发标记(不会STW(不会影响用户工作线程))(这个并发标记是指从根对象出发,顺着引用链去找其他的标记对象),阈值由下面的 JVM 参数决定:

    -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

Mixed Collection:

混合搜集;

会对 E S O 进行全面的回收

  • 最终标记会 STW (是为了保证并发标记阶段漏掉的一些对象可以被搜集,因为在并发标记的时候用户线程也是在工作的,可能这个时候会产生一些新的垃圾,然后改变一些对象的引用,所以在这个阶段需要进行时间暂停来进行一个最终标记)

  • 拷贝存活会 STW

-XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间!

问:为什么有的老年代被拷贝了,有的没拷贝?(下面图片中橙色的表示的是不进行复制的老年代) 因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

Full GC:

G1在老年代内存不足时(老年代所占内存(在堆中的占比)超过阈值

  • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理

  • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC

Young Collection 跨代引用:

  • 新生代回收的跨代引用(老年代引用新生代)问题

 回想一下新生代的垃圾回收的过程:

找到根对象---》 然后对根对象进行可达性分析 ---》 找到存活对象 ---》对存活对象进行复制,复制到幸存区;

这里就产生了一个问题:就是在寻找根对象的时候,根对象存在的时间非常的长,那么就意味着寻找根对象的时候根对象的数目是非常多的,我们不可能对它们都进行遍历(因为这个效率是非常低的),因此G1采用的是对老年代进行再细分(每一个是512k),如果这个卡表里面的卡的老年代引用了新生代的对象,那么就把该卡标记为脏卡;

这样做的好处是:在寻找GC root的时候就不需要去寻找遍历整个老年代了,我们只需要去关注这个脏卡就行(通过这个),这样就可以减少搜索的范围;

  • 卡表与Remembered Set

    • Remembered Set 存在于E中,用于保存新生代对象对应的脏卡

      • 脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡

  • 在引用变更时通过post-write barried(写屏障) + dirty card queue(脏卡队列)

  • concurrent refinement threads 更新 Remembered Set(在每次引用的对象发生变更单时候就会通过异步去更新这个脏卡,会把这个标记脏卡的指令放在一个队列中,将来会由一个线程来执行队列中的标记)

Remark

重新标记阶段; 使用 pre-write barrier + satb_mark_queue 来完成这个重新标记阶段;

在并发标记时,对象的状态:

黑色:已被处理,需要保留的 灰色:正在处理中的 白色:还未处理的

 但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前B又引用了C,这个时候开始对c进行垃圾标记就是不合适的,因为此时c还有强引用和它联系;

当对象的引用发生改变的时候,那么JVM就会给这个对象加入一个写屏障(只要对象的引用发生了变化,那么这个写屏障的指令就会被执行),写屏障负责把这个对象加入到一个队列中并且把这个对象标记为‘灰色’(正在处理中的),当整个并发标记结束后,接下来会进入重新标记阶段,这个阶段会让所有的用户线程都STW,然后对对队列中的对象做判断,看它们有没有强引用和它们关联,如果有强引用那就把对象标记为黑色(被处理,需要保留的);

G1的一些优化

JDK 8u20 字符串去重

去重过程:

  • 将所有新分配的字符串(底层是char[])放入一个队列

  • 当新生代回收时,G1并发检查是否有重复的字符串

  • 如果字符串的值一样,就让他们引用同一个字符串对象

  • 注意,其与String.intern的区别

    • intern关注的是字符串对象

    • 字符串去重关注的是char[]

    • 在JVM内部,使用了不同的字符串标

优点与缺点

  • 节省了大量内存

  • 新生代回收时间略微增加,导致略微多占用CPU

-XX:+UseStringDeduplication

JDK 8u40 并发标记类卸载

在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类;

-XX:+ClassUnloadingWithConcurrentMark  默认开启

JDK 8u60 回收巨型对象

  • 一个对象大于region的一半时,就称为巨型对象

  • G1不会对巨型对象进行拷贝

  • 回收时被优先考虑

  • G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JDK 9 并发标记起始时间的调整

这是为了尽量避免full GC 的发生;

  • 并发标记必须在堆空间占满前完成,否则退化为 FulGC

  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 来设置栈参数(表示老年代在堆内存中的占比,jkd8默认是45%,当超过这个阈值后就会进行垃圾回收)

  • JDK 9 可以动态调整

    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值

    • 进行数据采样并动态调整

    • 总会添加一个安全的空挡空间

5、垃圾回收调优

预备知识:

  • 掌握gc相关参数的vm参数,会基本都空间调整

  • 掌握相关的工具

  • 明白一点:调优跟应用,环境有关,没有万金油的法则; 比较凭经验的活!

查看当前环境的虚拟机参数命令: (前提是得配置了jdk的环境变量)

java -XX:+PrintFlagsFinal -version | findstr "GC"

调优领域

  • 内存

  • 锁竞争

  • CPU占用

  • IO

  • GC

确定目标:低延迟/高吞吐量? 选择合适的GC

  • CMS G1 ZGC

  • ParallelGC

  • Zing

最快的GC是不发生GC

首先排除减少因为自身编写的代码而引发的内存问题;

  • 查看Full GC前后的内存占用,考虑以下几个问题

    • 数据是不是太多?

    • 数据表示是否太臃肿

      • 对象图

      • 对象大小 包装类Integer 24个字节 int 4个字节

    • 是否存在内存泄漏

      • 考虑第三方的缓存实现(因为Java并不是专门做缓存的)

      • static Map map …

      • 软引用

      • 弱引用

新生代,幸存区调优

  • 新生代的特点

    • 所有的new操作分配内存都是非常廉价的

      • TLAB

    • 死亡对象回收零代价

    • 大部分对象用过即死(朝生夕死)

    • MInor GC 所用时间远小于Full GC

  • 新生代内存越大越好么?

    • 不是

      • 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降

      • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长

    • 新生代内存设置为内容纳[并发量*(请求-响应)]的数据为宜

  • 幸存区需要能够保存 当前活跃对象+需要晋升的对象

  • 晋升阈值配置得当,让长时间存活的对象尽快晋升

-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution   

老年代调优

以 CMS 为例:如果老年代的内存不是很大,那么如果产生的浮动垃圾又导致老年代的内存不足,就会导致cms并发失败,从而退化成serals old (串行)

  • CMS 的老年代内存越大越好

  • 先尝试不做调优,先让程序运行如果没有 Full发生full GC, 那么程序的老年代的内存已经足够了就不要先去调老年代了,可以先尝试调优新生代。

  • 如果对新生代已经调过多次,但是还是经常发生full gc ,那么就要观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

-XX:CMSInitiatingOccupancyFraction=percent  //这个是老年代的内存占堆中内存的比例阈值,程序运行的时候达到这个阈值就会触发垃圾回收,阈值越小就会越提前进行垃圾回收

三个调优案例

案例1:Full GC 和 Minor GC 频繁

这种情况一般是内存紧张,所以我们要先分析是哪一个区域的内存紧张,如果是新生代的内存紧张,那么到业务的高峰期来的时候,大量的对象被创建那么很快就把新生代的内存给挤满了,这就会导致幸存区的对象的晋升的阈值变小,然后导致晋升在老年代的对象中有一部分使用周期不是很长的对象晋升到老年代中,又因为老年代发生垃圾回收的频率比较低,所以就会导致老年代存储大量生命周期不是很长的对象;就会导致频繁的发生 full gc;

解决方法:先使用工具查看各个区域的内存使用情况,然后尝试提高新生代的内存空间,以及调高晋升老年代的阈值;

案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)

单次暂停时间特别长,得先去分析是那一部分时间耗费比较长,先看日志,通过这个cms的一些特点我们可以知道,cms在重新标记是比较耗时的,因为这里是会stw来扫描整个堆里面的对象(包括新生代和老年代中的对象),如果新生代的对象比较多,那么扫描的时间就会变长(因为还会去找对象的引用),我们可以尝试在重新标记之前进行一次新生代的垃圾回收:使用vm指令设置(-XX:+CMSScavengeBeforeRemark);

案例3:老年代充裕情况下,发生 Full GC(jdk1.7)

通过日志排除我们并没有发现并发失败的日志,说明老年代的空间充裕;而我们的jdk8是有一个元空间作为方法区的实现,而jdk1.7方法区是使用永久代作为方法区的实现;在jdk1.7及之前的版本,永久代的空间不足也会导致full gc的发生;

猜你喜欢

转载自blog.csdn.net/weixin_53142722/article/details/125418216