JVM性能优化------垃圾回收器

前面我们使用到了System.gc();方法,可以回收垃圾。
那么他具体是怎样的呢?我们来看看吧。
在这里插入图片描述
在这里插入图片描述
可以看到他是C语言写的,但我现在要说的是,我们调用了System.gc();的话,垃圾并不是立马回收,而是告诉虚拟机需要进行回收了。
测试代码:

/**
 * @author 龙小虬
 * @date 2021/4/16 22:54
 */
public class SystemGC {
     
    public static void main(String[] args) {
     
        new SystemGC();
        System.gc();
    }
    
	// 垃圾回收器回收的时候会走这个方法
    @Override
    protected void finalize() throws Throwable {
     
        System.out.println("重写finalize方法");
    }
}

它会有两种情况:

  1. 没有立马进行回收
    JVM性能优化------垃圾回收器_第1张图片
  2. 立马回收
    JVM性能优化------垃圾回收器_第2张图片

如果我们一定要立马回收呢?那就需要调用System.runFinalization(),即可。第一种情况是概率性的,如果想要真的看到结果,需要多次测试。

7种核心的收集器

  1. 串行回收器:Serial、Serial old
  2. 并行回收器:ParNew、Parallel Scavenge、Parallel old
  3. 并发回收器:CMS、G1(分区算法)
    JVM性能优化------垃圾回收器_第3张图片

为什么有着这么多的垃圾回收器,第一个回收器在jdk1.3就出来了,之后在2017年频繁更新垃圾回收器,为什么?这是因为17年之后,微服务框架已经兴起了。为了更好的适应微服务框架的到来,所以不断地更新,那为什么这么频繁,他们有什么目的?目的主要就是我们前面文章提到的Stop-The-World机制,因为触发它会导致用户线程全部暂停不能正常的访问堆内存,所以为了更好地适应微服务,不断地想办法降低stw机制的阻塞时间。

这样我们也就了解了,我们需要怎么去在生产环境下调优JVM。

在生产环境下调优jvm
前面提到的垃圾回收器不断更新是因为stw机制,那么我们要怎么去减少机制的触发,从而使阻塞时间变短,这就是我们调优JVM的一个要点。
我们都知道,stw机制的触发是因为内存不足,或者扩容的情况下才会触发,所以我们在生产环境下,不要频繁的触发垃圾回收或者是降低用户线程阻塞的时间,因为所有垃圾回收的时候都会暂停用户线程。那怎么避免频繁的触发垃圾回收?
那肯定就是尽量不扩容嘛:

  1. 提升新生代、老年代的内存大小
  2. 生产环境中,初始容量和最大容量保持一致
  3. 生产环境中不要调用System.gc()

垃圾回收器与垃圾回收算法区别

  1. 垃圾收集器:串行、并行收集、CMS、G1\ZGC。能够降低对用户线程暂停的时间或者用户线程和GC线程同时运行
  2. 垃圾收集算法:标记清除、标记整理、标记复制、分代算法
    垃圾收集器中包含了垃圾收集算法。

串行、并行的区别

  1. 串行就是GC单线程回收垃圾(单个线程回收)
    优点:简单而高效
    缺点:只有一个GC线程清理堆内存垃圾,一旦堆内存过大,从而导致stw时间过长
    应用场景:堆内存比较小的项目或者是桌面应用程序
  2. 并行就是GC多线程方式回收垃圾(多个线程同时回收,java8默认使用并行收集器)
    优点:如果堆内存空间比较大,采用并行收集器可以提高清理堆内存空间的效率
    缺点:开启多个线程同时清理垃圾,可能会很消耗CPU资源
    应用场景:多核多线程情况下

并发、并行的区别
并行、串行收集器当GC的线程在清理堆内存垃圾的时候,都会暂停用户线程
并发在GC的线程清理堆内存垃圾的时候,不会暂停用户线程(垃圾清理过程中会有非常短暂的暂停用户线程)代表:CMS、G1

怎么查看默认收集器
+代表使用了,-代表未使用
1.cmd命令
java -XX:+PrintCommandLineFlags -version
jinfo -flag UseParallelGC (线程pid)
2.idea
-XX:+PrintCommandLineFlags
3.jdk自带的jconsole
例如:
在这里插入图片描述
既然我们了解了默认的额垃圾回收器,那么就该知道一下垃圾回收的步骤

清理堆垃圾的步骤
根据GCRoot查找到整个引用链,只要有被GCRoot关联对象,则标记为可用对象,如果GCRoot引用链过长,那么就会导致遍历的时间过长,stw时间过长,这也是为什么串行、并行回收慢的原因

CMS收集器原理

  1. 初始标记(CMS initial mark)
    标记GCRoot能直接关联的对象,但是也会让所有用户线程暂停,暂停用户线程时间非常短

  2. 并发标记(CMS concurrent mark)
    用户线程与GC线程同时运行,此时根据初始标记得到直接引用对象,查询整个链与GCRoot能够关联的对象

  3. 重新标记(CMS remark)
    在初始标记之后,在并发标记过程中,并发标记线程和用户线程同时运行中,有可能会发生引用改变,所以需要重新修正,并且暂停用户线程,但是时长会比并发标记时间短,比初始标记时间长

  4. 并发清除(CMS concurrent sweep)
    用户线程和GC线程同时运行,GC线程同时运行清理堆内存垃圾,用户线程正常运行。并发清除过程中还会产生垃圾,因为它使用的是标记清除算法

    在这第四步,为什么不使用标记整理算法?标记整理算法会更改原对象的内存地址,但是现在的用户线程同时在运行,所以不能使用,否则会发生用户线程引用不到数据。cms核心理念:减少stw阻塞等待时间,如果使用那岂不是没有达到减少阻塞的等待时间的效果吗?

缺点:产生碎片化问题
处理方案:直接触发FullGC,采用老年代串行回收器清理整个堆内存垃圾,采用标记整理算法,导致所有用户线程阻塞。这也就是我们所说的,老年代使用CMS的时候,拥有备胎收集器老年代串行收集器
JVM性能优化------垃圾回收器_第4张图片

stw机制
那说了这么多的理念,怎么证明stw机制会导致线程暂时阻塞呢?
代码:

import java.util.ArrayList;

/**
 * @author 龙小虬
 * @date 2021/4/16 23:34
 */
public class STWBlock {
     
    public static void main(String[] args) {
     
        new MyThread().start();
        new Thread(() -> {
     
            ArrayList<byte[]> bytes = new ArrayList<>();
            while (true) {
     
                for (int i = 0; i < 20; i++) {
     
                    bytes.add(new byte[10 * 1024 * 1024]);
                }
                if (bytes.size() > 20) {
     
                    bytes.clear();
                    System.gc();
                }
            }
        }).start();
    }

    static class MyThread extends Thread {
     
        long startTime = System.currentTimeMillis();

        @Override
        public void run() {
     
            while (true) {
     
                try {
     
                    long end = System.currentTimeMillis();
                    System.out.println(end / 1000 + "." + end % 1000);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }
    }
}

在这里就发现,时间上的差距就越来越参差不齐了。
JVM性能优化------垃圾回收器_第5张图片
这也说明了,线程在某个时间会发生阻塞,才会导致这个问题。

你可能感兴趣的:(JVM性能优化源码)