JVM GC垃圾回收算法与调优参数

dmzhaoq1
发布于 2020-9-25 14:19
浏览
0收藏

概述


在C++程序当中,对于每一个通过new动态创建的对象,在不再需要时,需要通过delete显示删除对象,释放该对象占用的内存空间,否则会发生内存泄露。在Java当中,为了简化内存管理,JVM提供了自动垃圾回收机制,统一由JVM来进行垃圾对象回收,在Java应用程序当中只需要按需创建对象即可,不需要在应用程序中手动删除对象。

 

由之前的文章分析可知,在JVM的运行时数据区中,主要是在堆中存放新创建的对象,故JVM主要是针对堆进行垃圾回收。即由于每个JVM进程的堆大小通常是固定的,当堆存放了大量对象,没有足够的空闲内存用于存放新创建的对象时,则会触发垃圾回收,销毁不再需要的对象,释放对应的内存空间。

 

GC算法

 

对象可达性

 

JVM在对堆进行垃圾回收时,首先需要确定堆中的哪些对象是可以被回收的,哪些是还需要被引用的,即需要有一种机制来判断某个对象是否存活。

 

对于对象的存活性,一种是使用引用计数的方法,即为每个对象都维护一个引用计数,每次该对象被引用一次则递增,使用完毕则递减,当引用计数为0时,则表示该对象不再被引用,可以被回收;

 

另外一种是基于对象可达性分析来确定,Hotspot虚拟机在进行垃圾回收时就是基于对象可达性分析来确定哪些对象是可以被回收的。

 

GC roots:对象可达性分析首先需要确定一个分析的起点,即GC root根节点对象。对于任何一个对象,判断是否存在一条从GC root可达的路径,即某个对象被GC roots对象自身直接引用或者间接引用,如果存在,则说明该对象是可达的,在本次GC中不能被回收,否则可以被回收。成为GC roots的对象首先在JVM的内存模型当中是使用堆外的对象,即方法区和栈中的对象来担当的,而不能是存放在堆自身的对象,因为GC roots对象是不使用垃圾收集器来回收的,如栈的对象引用是在出栈时被自动销毁的。主要包括以下:栈(或者说栈帧)中的对象引用:当前被执行的方法的对象类型的参数、局部变量,中间变量对应的对象引用;

 

JNI本地方法栈中引用的对象,即JVM自身的方法的对象引用;

 

方法区中的静态和常量对象引用:类的static静态属性的对象引用,类的常量的对象引用。

 

对象引用

 

GC roots是栈或者方法区中的对象引用,而不是这些对象引用所引用的对象,对象引用和对象引用所引用的对象是两种存在,不要混淆。对象引用所引用的对象也是在堆中的。

 

特别需要注意的是类的静态和常量对象引用所引用的对象。在Hotspot实现当中,在JDK8以前,类的静态和常量属性的对象引用所引用的对象是存放在方法区,即PermGen中的,JDK8以后,这些对象都放在了堆中。

 

更多关于Java对象引用的分析,请参考:Java对象的强、软、弱、虚引用

 

分代与垃圾回收算法

 

垃圾回收主要是在堆没有足够的内存空间来存放新创建的对象时被触发。由于堆中的对象生命周期不同,故在进行垃圾回收时,不是所有对象都需要被回收,只是回收不再可达的对象。同时在进行垃圾回收时需要暂停应用程序,故会造成应用在暂停期间不可用,垃圾回收持续的时间越长,则应用不可用越久。而垃圾回收由于要基于对象可达性分析确定需要回收哪些对象,故持续时间与需要扫描和分析的内存区域的大小相关。

 

基于以上分析,为了尽量减少应用停顿时间,JVM根据对象生命周期的长短,基于分代的思路,将堆进一步细分为新生代和老年代两个区域,从而缩小垃圾回收需要扫描和分析的内存范围。新生代和老年代的大小比例可以通过JVM参数:-XX:NewRatio来控制,默认新生代和老年代大小比例为1:2,新生代占堆的1/3,即-XX:NewRatio=2。或者通过 -XX:NewSize来指定新生代的初始大小, -XX:MaxNewSize来指定新生代的最大大小,如果NewRatio和NewSize同时存在,在NewSize和MaxNewSize覆盖NewRatio。

 

每次新创建的对象都放在新生代中,如果新生代没有足够的内存空间来存放新对象时,先只对新生代进行垃圾回收来获取内存空间,这个过程称为Minor GC。如果对新生代进行垃圾回收之后还是没有充足的内存空间,则同时对新生代和老年代,即整个堆进行垃圾回收,这个过程称为Full GC。Minor GC持续时间较短,对应用影响较小,Full GC持续时间较长,对应用影响较大。

 

老年代主要存放年龄较大或者需要占用较大内存空间的对象:通过JVM参数:-XX:MaxTenuringThreshold=xx来控制晋升到老年代对象的年龄,不同GC收集器实现不同,Parallel Scavenge中默认值为15,CMS中默认值为6,G1中默认值为15,如果设置为0则新对象直接放到老年代。每进行一次Minor GC存活对象的年龄加一;
通过JVM参数:-XX:PretenureSizeThreshold=xx(单位为字节)来控制超过这个大小的对象直接在老年代分配内存空间,默认为0,表示都首先在新生代分配。

 

对堆基于分代的思路细分之后,由于新生代和老年代中存放的对象特点不一样,故对新生代和老年代的垃圾回收算法也存在差别,对应新生代是基于复制算法来实现回收的,老年代是基于标记清除或标记压缩算法来回收的。

 

新生代垃圾回收算法

 

复制算法

 

每个新创建的对象都首先在新生代分配内存来存放,新生代的对象的特点是生命周期很短,即大部分都是存放一段时间后变为不可达,即可以被回收,故在对新生代进行垃圾回收时,可以回收大部分的对象。

 

基于新生代存放的对象生命周期短,每次存活对象较少的特点,新生代主要是基于复制算法来实现垃圾回收。算法实现为:将新生代进一步细分为Eden,From Survivor,To Survivor三个区域,新创建的对象都存放在Eden。其中From和To的大小是相同的,可以通过JVM参数:-XX:SurvivorRatio来控制Eden和Survivor大小比例,默认为Eden和Survivor为8 : 2,-XX:SurvivorRatio=8,即Eden大小为新生代的4/5,From和TO各占1/10。

 

算法执行过程为:在进行Minor GC时,在对Eden区域的对象进行可达性分析之后,将存活对象移动到From Survivor并递增存活对象的年龄,如果某个对象的年龄超过了MaxTenuringThreshold,或者某个对象太大From Survivor无法存放,则将该对象移到到老年代。然后清空Eden区域,此时Eden区域和To Survivor区域都是空的,Minor GC完成,此时对下一次Minor GC来说,此次存放从Eden移动对象的From Survivor变成了To Survivor,空的To Survivor变成了From Survivor,即对应基于复制算法的Minor GC来说,每次Eden中存活的对象都是移动到From Survivor的。

 

老年代垃圾回收算法

 

老年代垃圾回收主要是在进行Full GC时触发,触发Full GC主要包括四个:老年代空间不足:在新生代进行Minor GC之后还没有足够空间来存放新对象,将该新对象放到老年代,而老年代也没有足够空间来存放该对象时会触发老年代垃圾回收,即Full GC,此时同时对新生代和老年代进行垃圾回收;

 

老年代空间不足:在进行Minor GC时,将对象从Eden移动到From Survivor,而From Survivor没有足够空间存放该对象,将该对象移动到老年代,而老年代也无法存放该对象,则触发Full GC;

 

在Minor GC时晋升到老年代的对象的平均大小大于老年代当前的空闲空间时,也会触发Full GC。

 

在程序中调用System.gc()函数时,默认也会进行Full GC,不过可以通过JVM参数:-XX:-+DisableExplicitGC 来禁用。

 

老年代中主要存放年龄较大,即生命周期较长的对象,还有就是需要占用较大内存空间的对象,由于每次进行垃圾回收时,存活的对象通常较多,如果使用复制算法,首先需要复制大量对象,其次如果存活的对象太大,会From Survivor需要较大的内存空间。基于老年代对象的存活对象多,对象大的特点,所以不适合采用复制算法,否则会造成回收缓慢,造成应用停顿时间长。所以对老年代的垃圾回收通常采用标记压缩和标记清除算法,这两个算法都不需要借助额外的内存空间,如

复制算法中的Survivor,提高了内存的使用率。

 

标记压缩算法和标记清除算法

 

标记压缩和标记清除算法首先需要对老年代中的对象进行可达性分析并对可达(存活)对象进行标记,其中标记过程为从GC roots引用的对象开始,首先标记GC roots引用的对象为存活对象,然后从这些对象继续往下查找存活对象,如果某个对象存在一条从GC roots可达的路径则该对象是存活的,否则是需要被回收的垃圾对象;

 

标记压缩和标记清除算法的不同之处为:标记压缩算法会将所有存活对象移到到一边,然后将边界以外的所有内存空间清空,从而减少老年代的内存碎片。而标记清除算法只是简单地将这些垃圾对象清除销毁,不会对老年代的内存空间进行压缩,故会存在内存碎片问题,但是由于不需要对存活对象进行移动操作,故标记清除算法执行速度较快。

 

GC垃圾回收器

 

垃圾回收器主要是在以上GC算法实现的基础上,增加了执行垃圾回收的线程的个数和是否与应用程序并发执行方法的考虑。

 

新生代垃圾回收器

 

新生代的垃圾收集器主要是基于复制算法实现,在此基础上,根据执行垃圾回收的线程数量,可以分为串行垃圾回收器和并行垃圾回收器。

 

1. 串行Serial垃圾回收器:-XX:+UseSerialGC

 

串行垃圾回收器是单线程垃圾回收器,使用一个线程来执行Minor GC。串行垃圾回收器是运行在client模式的JVM进程的默认新生代垃圾回收器。在进行Minor GC

时,需要暂停所有的应用程序线程。

 

JVM配置参数为:-XX:+UseSerialGC,使用该配置参数,新生代和老年代均使用串行垃圾回收器,其中老年代为基于标记压缩算法实现的Serial Old。

 

2. 并行Parallel垃圾回收器:-XX:+UseParallelGC

 

并行垃圾回收器是多线程版本的基于复制算法实现的垃圾回收器,使用多个线程同时进行垃圾回收。相对于单线程垃圾收集器,多个垃圾回收线程并行执行可以加快Minor GC的速度,减少应用停顿的时间。特别是对于包含多个CPU的服务器来说,一般使用并行垃圾回收器进行垃圾回收。

 

JVM配置参数为:-XX:+UseParallelGC,该配置参数只对新生代有效,即新生代使用并行垃圾回收器,老年代使用Serial Old串行回收器。这个也是运行在server模式的JVM进程的默认垃圾收集器配置,即新生代Parallel,老年代Serial Old。

 

吞吐量优先:Parallel并行垃圾回收器除了提供多线程来执行垃圾回收外,其主要目的是提供高吞吐量,即应用程序执行时间占总执行时间的比例较高,其中吞吐量的计算为:吞吐量=应用程序执行时间 /(应用程序执行时间 + 垃圾回收器执行时间)。所以Parallel关注的是总的应用执行时间,而不是应用的单次响应速度,比较适合需要高CPU利用率的应用,如需要进行大量计算的后台任务,科学计算,保证这些任务尽快完成,而不是用户交互非常频繁且要求响应用户速度快的应用,如网络游戏,电商等web应用。

 

吞吐量目标:Parallel垃圾回收器为了实现可控制的吞吐量,通过JVM参数:-XX:MaxGCPauseMillis来控制垃圾回收的最大停顿时间,-XX:GCTimeRatio直接控制吞吐量的大小。

 

可控制吞吐量的实现:通过JVM参数:-XX:+UseAdaptiveSizePolicy来开启动态调整堆的大小来达到吞吐量控制目的,此时不需要配置堆的新生代,老年代的大小,如新生代大小-Xmn,Survivor大小-XX:SurvivorRatio,新生代晋升到老年代阀值-XX:PretenureSizeThreshold,只需要配置基本的堆配置,如最大大小-Xmx。通过JVM参数: -XX:ParallelGCThreads=20配置并行收集器的线程数,一般设置为和处理器数量相同。

 

3. 并行ParNew垃圾回收器:-XX:+UseParNewGC

 

ParNew是串行Serial垃圾回收器的多线程版本,也是基于复制算法实现,主要和老年代的并行并发垃圾回收器CMS一起使用。

 

与Parallel垃圾回收器不同的是,ParNew只是单纯的多线程版本的Serial垃圾收集器,并不具备可控的吞吐量和动态调整堆的新生代和老年代大小的功能。

 

配置方式:如果老年代配置了使用CMS垃圾回收器,则新生代默认使用ParNew,不需要显示配置。如果需要显示配置,则JVM参数为:-XX:+UseParNewGC。其中ParNew和CMS的组合是响应时间优先的。如果年轻代的并行GC不想开启,可以通过设置-XX:-UseParNewGC来关掉。

 

老年代垃圾回收器

 

老年代垃圾回收器是基于标记压缩或者标记清除算法实现的。

 

1. 串行Serial Old垃圾回收器:-XX:+UseSerialGC

 

Serial Old为老年代的垃圾回收器,是使用单线程的基于标记压缩算法实现的对老年代进行垃圾回收的。

 

JVM进程运行在client模式时,默认采用Serial Old作为老年代垃圾回收器。

 

配置方式为:-XX:+UseSerialGC,此时老年代和新生代均使用串行垃圾回收器。

 

Serial Old垃圾回收器还作为CMS的一个后备垃圾回收器,即当使用CMS对老年代进行垃圾回收时,如果发生了Concurrent Mode Failure时,则降级为使用Serial Old对老年代进行垃圾回收。

 

2. 并行Parallel Old垃圾回收器:-XX:+UseParallelOldGC

 

Parallel Old是一个多线程的基于标记压缩算法的老年代垃圾回收器。通常与新生代的并行Parallel垃圾回收器一起使用来实现可控的高吞吐量目的。

 

当新生代使用:-XX:+UseParallelGC 开启时,老年代使用的还是Serial Old,故需要显示配置:-XX:+UseParallelOldGC来指定老年代使用并行Parallel Old垃圾回收器。

 

3. 并行并发CMS垃圾回收器:-XX:+UseConcMarkSweepGC

 

详细分析请参考:并行并发CMS垃圾回收器:-XX:+UseConcMarkSweepGC

 

G1垃圾回收器:-XX:+UseG1GC

 

G1垃圾回收器是更加智能的垃圾回收器,在实现层面,新生代和老年代在物理上的区分已经去除了,取而代之的是将堆划分为大小相同的一个个区域。不过在逻辑上仍属于分代回收,新生代和老年代由多个这种区域组成。

 

G1的设计规则就是可以通过简单明了的方式来进行性能调优,典型配置只需要如以下配置:指定堆的最大大小,指定GC的最大停顿时间,则G1垃圾收集器会想办法满足这个目标。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200


G1是JDK9的默认垃圾回收器。对于原有项目是否需要升级为G1,根据官方的建议是,如果当前项目使用CMS或者ParalleOldGC运行良好,并且没有因为GC问题导致长时间停顿,则建议保持现状,不需要升级为G1。如果存在以下情况,则可以尝试升级为G1,并可以基于GC日志来分析效果:

 

实时数据占用了超过半数的堆空间;
对象分配率或者对象晋升速度变化明显;
期望消除耗时较长的停顿或GC(0.5s ~ 1s)

 

 

作者:服务端开发

来源:CSDN

分类
收藏
回复
举报
回复
    相关推荐