GC系统算法分析

GC概述

基于正在使用的对象进行遍历,对存活的对象进行标记,其未标记对象可认为是垃圾对象,基于特定算法进行回收,这个过程称为GC。GC系统实现有如下几个方面:

1)GC判断策略(引用计数,对象可达性分析)

2)GC收集算法(标记-清除,标记-清除-整理,标记-复制-清除)

3)GC收集器(Serial,Parallel,CMS,G1)

手动GC

手动GC即显式的内存分配和内存释放,如果忘了释放,则对应的那块内存不能再次使用,内存一直被占着,却不能使用,这种情况就称为内存泄漏

int send_request() {
    size_t n = read_size();
    int * elements = malloc(n * sizeof(int));
    if(read_elements(n,elements) < n){
        return -1;
    }
    free(elements)
    return 0;
}

手动GC经常会忘记释放内存,这样就会导致内存泄漏

自动GC

自动GC一般是在JVM系统内存不足时,由JVM系统启动GC对象,自动对内存进行垃圾回收

引用计数法

1)绿色云朵是内存中的根对象,表示程序中正在使用的对象

2)蓝色圆圈是内存中活动对象,其中的数字表示其引用计数

3)灰色圆圈是内存中没有活动对象引用的对象,表示未活动对象,也为可回收对象

对于引用计数法,有一个很大的缺陷,就是循环引用,如下图:

其中红色对象实际上是应用程序不使用的垃圾,但由于引用计数的限制,仍然存在内存泄漏,当然也有一些方法来应对,如弱引用,或是其他的算法来排查循环引用等

标记清除

标记清除就是对可达对象进行标记,不可达对象即认为是不使用的垃圾,需要进行清除

1)标记所有可到达的对象

2)清除不可到达对象占用的内存地址

此方法解决了循环依赖问题,但存在短时间的线程暂停,我们一般称这种现象为STW停顿

JVM中包含多种GC算法收集器,虽然实现上略有不同,但理论都采用了以上两种方式

GC过程分析

碎片整理

系统每次执行时每次职系那个清除操作,JVM都必须保证不可达对象占用的内存能被回收再重用。内存是被回收了,但这有可能会产生大量的内存碎片,进而引发两个问题:

1)对象创建时,执行写入操作越来越耗时,因为寻找一块足够大的空闲内存会变得更加麻烦

2)对象创建时,JVM需要在连续的内存块中为对象分配内存,如果碎片问题很严重,直至没有空闲片段能存放新创建的对象,就会发生内存分配错误

为了解决碎片问题,JVM在启动GC执行垃圾收集的过程中,不仅仅是标记和清除,还需要执行内存碎片整理。这个过程会让所有可达对象进行依次移动,进而可以消除内存碎片,并为新对象提供更大并且连续的内存空间。内存整理时会将对象移动到靠近内存地址的起始位置,示意图如下:

分代设想

垃圾收集要停止整个应用程序的运行,假如这个收集的过程需要很长的时间,就会对应用程序产生很大的性能问题,如何解决这一问题?通过实验发现,内存中的对象通常可以分为两大类:

1)存活时间较短(相对较多)

2)存活时间较长(相对较少)

基于如上问题的分析,便提出了分代回收思路,将VM中内存分为年轻代和老年代

纵轴代表对象的数量,横轴代表对象的年龄,可以看出,大多数对象还是分布在年轻代

分代设想将内存拆分为两个可单独清理的区域,允许采用不同的算法来大幅提高GC的性能,但这种放还是也不是没有问题,例如,在不同分代中的对象,可能会互相引用,这样的对象就难以回收。

对象分配

对内存的内存结构图如下:

基于此内存架构,对象内存分配过程:

1)编译器通过逃逸分析,确定对象是在栈上分配还是堆上分配

2)如果在堆上分配,则首先检测是否可在TLAB上直接分配

3)如果TLAB上无法直接分配则在Eden加锁去进行分配(线程共享区)

4)如果Eden区无法存储对象,则执行Yong GC

5)如果Yong GC之后Eden区依然不足以存储对象,则直接分配在老年代

在对象创建时可能会触发Yong GC,此GC过程的简易原理图如下:

1)新生代由Eden区和两个幸存区构成,任意时刻至少有一个幸存区是空的,用于存放下次GC时未被收集的对象

2)GC触发时Eden区所有可达对象会被复制到一个幸存区,假设为s1,当幸存区s1无法存储这些对象时会直接复制到老年代

3)GC再次触发时Eden区和s1幸存区中的可达对象会被复制到另一个幸存区s2,同时清空eden区和s1幸存区

4)GC再次触发时Eden区和s2幸存区中的可达对象会被复制到另一个幸存区s1,同时清空eden区和s2幸存区,以此类推

5)当多次GC过程完成后,幸存区中的对象存活时间达到了一定阈值,会被看成是年老的对象直接移到老年代

GC模式分析

垃圾收集事件通常分为:

1)Minor GC(小型GC):年轻代GC事件,新对象分配频率越高,Minor GC的频率就越高

2)Major GC(大型GC):老年代GC事件

3)Full GC(完全GC):整个堆的GC事件

标记可达对象

现在的GC算法,基本都是从标记可达对象开始,这些标记为可达对象即为存活对象。同时我们可以将查找科大对象时的起始位置对象,认为是根对象。基于根对象标记可访问或可达对象,对于不可达对象,GC会认为是垃圾对象。如下图,绿色云朵为根对象,蓝色圆圈为可达对象,灰色圆圈为垃圾对象。

GC遍历内存中整体的对象关系图确定跟对象,GC规范中指定根对象可以是:

1)栈中变量直接引用的对象

2)常量池中引用的对象

其次,确定了根对象以后,进而从根对象开始进行依赖查找,所有可访问到的对象都认为是存活对象,再进行标记。

标记可达对象需要暂停所有应用线程,以确定对象的引用关系,其暂停的时间,与堆内存大小,对象的总数没有直接关系,而且由存活对象的数量来决定。

移除不可达对象

移除不可达对象时会因GC算法的不同而不同,大部分的GC操作大致可分为三类:清除(Mark-Sweep),标记清除整理(Mark-Sweep-Compact),标记复制(Mark-Copy)

标记-清除(Mark-Sweep)

对于标记清除算法应用相对简单,但内存会产生大量的碎片,这样再创建大对象时,假如内存没有足够连续的内存空间可能会出现OutOfMemoryError

标记-清除-整理(Mark-Sweep-Compact)

标记清除整理算法中在清除垃圾对象以后会移动可用的对象,对碎片进行压缩,这样会在内存中构建相对比较大的连续空间便于大对象的直接存储,但是会增加GC暂停时间

标记复制(Mark-Copy)

标记复制算法会基于标记清除整理算法,但是会创建新的内存空间用于存储幸存对象,同时复制与标记可以同时并发执行,这样可以较少GC时系统的暂停时间,提高系统性能

GC算法实现简介

JVM系统在运行时,因新对象的创建,可能会触发GC事件。无论哪种GC都用可能会暂停时间降到最小,这要看我们使用的GC算法。现在对于JVM中的GC算法无非就两大类:一类负责收集年轻代,一类负责收集老年代。假如没有显式指定垃圾回收算法,一般采用系统默认的算法,当然也可以自己指定。JDK8中基于特定垃圾回收算法的垃圾收集器如下:

1)年轻代和老年代的串行收集器:Serial GC

2)年轻代和老年代的并行收集器:Parallel GC

3)年轻代的并行收集器(Parallel New)+老年代的并行收集器(CMS-Concurrent Mark and Sweep)

4)年轻代和老年代的G1收集器,负责回收年轻代和老年代

除了以上几种组合方式外,其他组合方式要么现在已经不支持,要么不推荐,如何对这些组合进行选择,要结合系统的特点。如果系统追求高吞吐量还是响应时间,这两者都要兼顾。总之,对于GC组合的选择,没有最好,只有更好。结合当前系统的环境配置,性能指标以及GC的特点,不断进行GC日志分析,定位系统问题,才是一般选择哪种GC的关键。

GC收集器应用分析

Serial收集器应用分析

应用特点:

1)内部只使用一个线程执行垃圾回收,无法并行化

2)GC时所有正在执行的用户线程暂停并且可能会产生较长时间的停顿

场景应用:

1)一般可工作在JVM的客户端模式

2)适用于CPU个数或核数较少且内存空间较小的场景

算法应用:

1)新生代使用mark-copy(标记-复制)算法(新生代存活对象较少)

2)老年代使用mark-sweep-compact(标记-清除-整理)算法(老年代对象回收较少,容易产生碎片)

实践应用:

总之,Serial GC一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程。适合单CPU小应用,实时性要求不是那么高场景。一般在JVM的客户端模式下应用比较好。

CMS收集器应用分析

应用特点:

1)使用空闲列表管理内存空间的回收,不对老年代进行碎片整理,减少用户线程暂停时间

2)在标记-清除阶段的大部分工作和用户线程一起并发执行

3)最大优点是可减少停顿时间(可提高服务的相应速度),最大的缺陷是老年代的内存碎片

场景应用:

1)应用于多个或多核处理器,目标降低延迟,缩短停顿时间,响应时间优先

2)CPU受限场景下,因与用户线程竞争CPU,吞吐量会减少

算法应用:

1)年轻代采用并行方式的mark-copy(标记-复制)算法

2)老年代主要使用并发mark-sweep(标记-清除)算法

步骤分析:

1)初始标记:此阶段标记一下GC Roots能直接关联到的对象,速度很快

2)并发标记:此阶段就是进行GC Roots Tracing的过程,从直接关联对象遍历所有可达对象,再进行标记

3)重新标记:此阶段要修正并发标记期间,因用户程序继续运作而导致标记产生的那一部分对象的标记记录

4)并发清除:此阶段与应用程序并发执行,不需要STW停顿,目的是删除未使用的对象,并收回他们占用的空间

5)并发重置:此阶段与应用程序并发执行,重置CMS算法相关的内部数据,同时GC线程切换到用户线程

实践应用:

总之,CMS垃圾收集器再减少停顿时间上做了很多给力的工作,大量并发执行的工作并不需要暂停应用程序,如果服务器是多核CPU,并且主要调优目标是降低延迟,那么使用CMS是个明智的选择。CMS垃圾收集可减少每一次GC停顿的时间,这样直接影响到终端用户对系统的体验,用户会认为系统非常灵敏。但是因为多数时候都有部分CPU资源被GC消耗,所以在CPU受限的情况下,CMS会比并行GC的吞吐量差一些,还有就是老年代内存碎片问题,在某种情况下GC会造成不可预测的暂停时间,特别是堆内存较大的情况下。

Parallel收集器应用分析

应用特点:

1)可利用CPU的多核特性执行多线程的并行化GC操作

2)GC期间,所有的CPU内核都在并行清理垃圾,所以暂停时间较短

3)最大优势是可实现可控的吞吐量与停顿时间

场景应用:

1)GC操作仍需暂停应用程序,所以不适合要求低延迟的场景

2)因其高吞吐量的特性,适用于后台计算,后台处理的弱交互场景的而不是web交互场景

算法应用:

1)在年轻代使用标记-复制算法,对应的是Parallel Scavenge收集器

2)在老年代使用标记-清除-整理算法,对应的是Parallel Old收集器

实践应用:

总之,Parallel GC是一种并行收集器,可利用CPU优势,执行并行GC操作,吞吐量较高,并可有效降低工作线程暂停时长,但因为垃圾收集的所有阶段都不能被打断,所以Parallel GC还是有可能导致长时间应用暂停。Parallel GC适合于需要高吞吐量而对暂停实践不敏感的场合,比如批处理任务。

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

G1收集器应用分析

G1收集器是一种工作于服务端模式的垃圾回收器,主要面向多核,大内存的服务器。G1在实现高吞吐的同时,也最大限度满足了GC停顿时间可控的目标。G1收集器主要有如下需求的程序设计:

1)可以像CMS收集器一样能同时和应用线程一起并行的执行

2)减少整理内存空间时的停顿时间

3)要满足可预测的GC停顿时间需求

4)不能牺牲太多的吞吐性能

未来G1计划要全面取代CMS,G1相比CMS有更多的优势,G1是压缩型收集器,可以实现更有效的空间压缩,消除大部分潜在的内存碎片问题。G1提供了更精准的可预测的垃圾停顿时间设置,可满足用户在指定垃圾回收时间上的需求。

在G1中,堆不再分成连续的年轻代和老年代空间,而是划分为多个可以存放对象的小堆区,每个小堆区都可能是Eden区,Survivor区和Old区,在逻辑上,所有的Eden区和Survivor区合起来就是年轻代,所有的Old区拼在一起那就是老年代。图下图所示:

 这样的划分使得GC不必每次都去收集整个堆空间,而是以增量的方式来处理。GC时每次只处理一部分小队区,称为此次的回收集,GC事件的每次暂停都会收集所有年轻代的小堆区,同时也可能包含一部分老年代小堆区,下图所示

G1以一种和CMS相似的方式执行垃圾回收,G1在并发标记阶段估算每个小堆区存活对象的总数,垃圾最多的小堆区会被优先收集,这也是G1名称的由来。顾名思义,G1将其收集和压缩活动集中在堆中可能充满可回收对象的区域上。G1通过用停顿预测模型来满足用户自定义的停顿时间目标,它基于设定的停顿时间来选择要回收的regions数量。

G1基于标记,清理对应的regions时,会将对象从一个或多个region里复制到另一个region里,在这个过程中会伴随着压缩和释放内存。清理过程在多核机器上都采用并行执行,来降低停顿时间,增加吞吐量。因此G1在持续的运行中减少碎片,满足用户自定义停顿时间需求。这种能力是以往的回收器所不具备的。

G1不是一个实时的收集器,它只是最大可能来满足设定的停顿时间。G1会基于以往的收集数据,来评估用户指定的停顿时间可以回收多少regions,需要花费的时间,再切丁停顿时间内可以回收多少个regions。

特点:

1)将java堆分成大小相同的多个区域(region,1M-32M,最多2000个,最大支持内存64G)

2)内存应用具备极大地弹性(一个或多个不连续的区域共同组成eden,survivor或old区,但大小不再固定)

3)相对CMS有着更加可控的暂停时间(pause time)和更大的吞吐量(throughput)以及更少的碎片(标记整理)

4)支持并行和并发,可充分利用多CPU,多核优势,降低延迟,提高响应速度

应用场景:

1)Full GC发生相对比较频繁活消耗的总时长过长

2)对象分配率或对象升级至老年代的比例波动较大

3)较长时间的内存整理停顿

应用算法:

1)年轻代标记复制算法

2)老年代标记清除整理算法

步骤分析:

1)初始标记:属于Young GC范畴,是stop-the-world活动,对待有老年代对象引用的Survivor区进行标记

2)根区扫面:并行执行,扫描那些对old区有引用的Survivor区,在Young GC发生之前该阶段必须完成

3)并发标记:并发执行,找出整个堆中存活的对象,将空区域标记为X,此阶段也可能会被Young GC中断

4)再次标记:完全完成对heap存活对象的标记,采用snapshot-at-the-beginning算法完成,比CMS用的算法更快

5)清理:并发执行,统计小堆区中所有存活的对象,并对小堆区进行排序,优先清理垃圾多得多小堆区,释放内存

6)复制/清理:对小堆区未被清理对象进行复制,再清理

应用分析:

G1是HotSpot中最先进的准产品级垃圾收集器,最重要的是,HotSpot工程师的主要精力放在不断改进G1上面,在新的java版本中,将会带来新的功能和优化。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值