垃圾收集器

1,881 阅读11分钟

引言:更多相关请看 JVM+GC解析系列

谈谈GC垃圾回收算法和垃圾收集器的关系

GC算法(引用计数/复制/标记清除/标记整理)是内存回收的方法论,垃圾收集器就是算法落地实现。
因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集。
4种主要垃圾收集器。

串行垃圾回收器(Serial):它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。
并行垃圾回收器(Parallel):JAVA8默认垃圾回收器,多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景。
并发垃圾回收器(CMS Concurrent Mark Sweep):用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程互联网公司多用它,适用于对响应时间有要求的场景。 G1垃圾回收器:G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。Java8出现,Java9的默认垃圾回收器是G1,Java11的的默认垃圾回收器是G1的高配版。 最终总结: Java8以前主要是串行、并行、并发。Java8及以后主要是G1。

默认垃圾收集器

查看默认垃圾收集器,参数:

java -XX:+PrintCommandLineFlags -version

内容:

-XX:InitialHeapSize=267043712 -XX:MaxHeapSize=4272699392 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC(垃圾回收器,默认串行)
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

默认的垃圾收集器有:UseSerialGC、UseParallelGC、UseConcMarkSweepGC、UseParNewGC、UseParallelOldGC、UseG1GC、UseSerialOldGC(Java8基本被淘汰)。
JDK底层最核心源码:

垃圾回收器图例:
垃圾回收器是用来具体实现GC算法并进行垃圾回收。不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:红色Java8表示从Java8开始,对应的垃圾回收器deprecated不推荐使用。

垃圾收集器部分参数预先说明

DefNew:Default New Generation默认新生代。
Tenured:Old老年代。
ParNew:Parallel New Generation并行新生代。
PSYoungGen:Parallel Scavenge并行清除新生代。
ParOldGen:Parallel Old Generation并行老年代。

Server(重量级)/Client(轻量级)模式

Java8以后默认使用server模式,可在配置文件修改。win32无论配置如何默认使用client;32位其它操作系统,内存2G且两个CPU使用server,低于这个配置使用client;64位系统only server。 查看:

java -version

效果:

java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

新生代垃圾回收器

串行GC(Serial)/(Serial Coping)/ DefNew+Tenured

Serial回收器是最基本、最古老、最稳定、效率最高的垃圾回收器。是单线程的垃圾回收器。由于gc回收线程垃圾清理时,会暂停其它的工作线程(缺点)。Serial回收器不存在线程间的切换,因此,特别是在单CPU的环境下,它的垃圾清除效率比较高。对于Client运行模式的程序,选择Serial回收器是一个不错的选择。也是Client运行模式默认的新生代垃圾回收器。
jvm参数:-XX:+UseSerialGC,开启后会使用:Serial(young区使用)+Serial Old(Old区使用)的收集区组合。
表示:新生代、老年代都会使用串行垃圾收集器,新生代使用复制算法,老年代使用标记整理算法。

案例

代码:

public class GCDemo {

    public static void main(String[] args) throws Exception {
        System.out.println("=============GCDemo==========");
        String s = "root";
        while (true){
            s += s + new Random().nextInt(222222222) + new Random().nextInt(322222222);
        }
    }

}

参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC -Dfile.encoding=GBK

效果:

并行GC(ParNew) / ParNew+Tenured

使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其它所有的工作线程直到它收集结束。 ParNew收集器是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是许多Java虚拟机在server模式新生代的默认垃圾收集器。
JVM参数:-XX:+UseParNewGC,启用ParNew收集器,只影响新生代的收集,不影响老年代。开启-XX:+UseParNewGC参数,会使用ParNew(Young区用)+Serial Old的收集器组合,新生代使用复制算法,老年队使用标记清除算法。
ParNew+Tenured的搭配,Java8不推荐。

Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release

备注:-XX:+ParallelGCThreads限制线程数量,默认开启和CPU数量相同的线程数。

案例

代码:

public class GCDemo {

    public static void main(String[] args) throws Exception {
        System.out.println("=============GCDemo==========");
        String s = "root";
        while (true){
            s += s + new Random().nextInt(222222222) + new Random().nextInt(322222222);
        }
    }

}

参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC -Dfile.encoding=GBK

效果:

并行回收GC(Parallel) / (Parallel Scavenge) / PSYoungGen+ParOldGen

Parallel Scavenge收集器类似ParNew,也是一个新生代垃圾收集器,使用复制算法,也是一个并行多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化,老年代和新生代都用多个线程来收。它重点关注的是:可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间)也即比如程序运行100分钟,垃圾收集时间1分钟,香吐量就是99%)。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
自适应调节策略也是ParallelScavenge收集器与Pardew收集器的一个重要区别。自适应调节策略:處拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。 常用JVM参数:-XX:+UseParallelGC或XX:+UseParallelOldGC(可互相激活)使用Parallel Scavenge收集器。开启该参数后:新生代使用复制算法,老年代使用标记-整理算法。 备注:-XX:ParallelGCThreads=数字N,表示启动多少个GC线程:
cpu大于8 N=5/8
cpu小于8 N=实际个数

案例

代码:

public class GCDemo {

    public static void main(String[] args) throws Exception {
        System.out.println("=============GCDemo==========");
        String s = "root";
        while (true){
            s += s + new Random().nextInt(222222222) + new Random().nextInt(322222222);
        }
    }

}

参数(相互激活,哪一个都行):

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC -Dfile.encoding=GBK

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC -Dfile.encoding=GBK

效果:

老年代垃圾回收器

串行回收GC(Serial Old)/(Serial MSC) / Parallel Scavenge+Serial Old

Serial Old是Serial垃收集器老年代版本,它同样是个单线程的收集器,使用标记整理算法,这个收集器也主要是运行在Client模式的Java虚拟机默认的年老代垃圾收集器。
在Server模式下,主要有两个用途(了解,版本己经到8及以后):
1.在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。(Parallel Scavenge+ Serial Old) 2.作为老年代版中使用CMS收集器的后备垃圾收集方案。
JVM参数(Java8中已经被优化掉,没有了):

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC -Dfile.encoding=GBK

并行GC(Parallel Old) / (Parallel MSC) / PSYoungGen+ParOldGen

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。在JDK1.6之前,新生代使用Parallel Scavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。在JDK1.6之前(Parallel Scavenge+Serial Old)。Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以优先考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。在JDK1.8及后(Parallel Scavenge+ Parallel Old)。 JVM常用参数:-XX:+UseParallelOldGC,使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代 Parallel Old。

并发标记清除GC(CMS) / ParNew+CMS

CMS收集器( Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行
并发标记清除收集器组合:ParNew + CMS + Serial Old

4步过程

流程:
1.初始标记(CMS initial mark)。只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要停所有的工作线程。
2.并发标记(CMS concurrent mark)和用户线程一起。进行GC Roots跟踪的过程,和用户线程一起工作,不需要停顿工作线程。主要标记过程,标记全部对象。
3.重新标记(CMS remark)。为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。
4.并发清除(CMS concurrent sweep)和用户线程一起。清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。

优缺点

优点:并发收集低停顿。
缺点:
1.并发执行,对CPU资源压力大。
由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW(stop the world)的方式进行一次GC,从而造成较大停顿时间。 2.采用的标记清除算法会导致大量碎片。
标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeforeCompaction(默认0,即毎次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。

案例

代码:

public class GCDemo {

    public static void main(String[] args) throws Exception {
        System.out.println("=============GCDemo==========");
        String s = "root";
        while (true){
            s += s + new Random().nextInt(222222222) + new Random().nextInt(322222222);
        }
    }

}

参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -Dfile.encoding=GBK

效果:

上图的1234就是4步过程:1.初始标记(CMS initial mark) -> 2.并发标记(CMS concurrent mark) -> 3.重新标记(CMS remark) -> 4.并发清除(CMS concurrent sweep)

垃圾收集器选择

组合的选择: 单CPU小内存,单机程序。

-XX:+UseSerialGC

多CPU,需要最大吞吐量,如后台计算型应用。

-XX:+UseParallelGC

或者

-XX:+UseParallelOldGC

多CPU,追求低停顿时间,需快速响应如互联网应用

-XX:+UseConcMarkSweepGC

-XX:+ParNewGC

表格:

参数 新生代止级收集器 新生代算法 老年代垃圾收集器 老年代算法
-XX:+UseSerialGC SerialGC 复制 SerialOldGC 标整
-XX:+UseParNewGC ParNew 复制 SerialOldGC 标整
-XX:+UseSerialGC或-XX:+UseParallelOldGC Parallel[Scavenge] 复制 Parallel Old 标整
-XX:+UseConcMarkSweepGC ParNew 复制 CMS+Serial Old(作为CMS出错的后备收集器) 标清
-XX:+UseG1GC G1整体采用标理 局部是复制,无内存碎片