再战JVM (9) 垃圾收集器

26 篇文章 0 订阅

如果说收集算法是内存回收的方法论,那么垃圾收集器则是内存回收的具体实现。

一. Serial 收集器

Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法来回收新生代的收集器,同时他也是单线程工作的收集器。曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择

它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)

下图展示了Serial 收集器(老年代采用Serial Old收集器)的运行过程
在这里插入图片描述

使用 -XX:+UseSerialGC 参数指定新生代和老年代都是用串行垃圾收集器,即Serial+Serial Old

优点 & 缺点

优点:

  • 额外消耗内存最小的垃圾收集器

  • 单线程,没有垃圾收集线程交互的开销

缺点:

  • 收集时会暂停其他所有的工作线程,直到收集结束(这个暂停是所有垃圾收集器都要暂停的,但是在Serial收集器上暂停的相对要久)

总结:

Serial收集器适合在用户的桌面应用场景中,因为内存分配给虚拟机管理的内存一般不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本不会再大了),停顿时间完全可以控制在几十毫秒最多一百毫秒以内,只要不频繁发生,这点停顿时间可以接收,所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

二. Serial Old 收集器

Serial Old收集器 是Serial收集器的老年代版本 ,它同样是一个 单线程收集器 ,使用 标记-整理(Mark-Compact) 算法

此收集器的主要意义也是在于给Client模式下的虚拟机使用

如果是在Server模式下,它还有两大用途:

  • 在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
  • 作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用

三. ParNew收集器

ParNew收集器是Serial收集器的多线程版本,ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,另外一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep) 配合工作,并且ParNew收集器是CMS收集器的默认新生代收集器

ParNew收集器的工作过程如下图(老年代采用Serial Old收集器):
在这里插入图片描述
使用:-XX:+UseParNewGC 参数 启用ParNew只影响新生代的收集,不影响老年代。
-XX:ParalletGCThreads 参数设定并行垃圾收集的线程数量 (默认开启的线程数等于cpu数)

优点 & 缺点

优点:

  • cpu多核心的情况下,性能要比Serial好
  • CMS收集器的默认新生代收集器

缺点:

  • 单CPU的环境中绝对不会有比Serial收集器更好的效果
  • 收集线程间有线程切换的开销

总结:

成也CMS,败也CMS,随着垃圾收集器技术的不断增长,更先进的G1收集器取代了CMS垃圾收集器,官方还取消了 ParNew + Serial Old 以及 Serial + CMS 这两种组合,ParNew 合并入 CMS 成为CMS 处理新生代的收集器

ParNew 是HotSpot中 第一款退出历史舞台的垃圾收集器

四. Parallel Scavenge 收集器

Parallel Scavenge收集器也是一个 多线程的新生代收集器,也使用 复制算法。但是它的特点与其他收集器不同,前几款收集器以及CMS收集器 的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是 达到一个可控制的 吞吐量(Throughput)

吞吐量(JVM衡量 GC性能的两个指标之一,另一个是 停顿时间):

  • 吞吐量是指应用程序线程用时占程序总用时(即 应用程序线程用时 + GC线程用时)的比例。 例如,吞吐量99/100意味着100秒的程序执行时间应用程序线程运行了99秒, 而在这一时间段内GC线程只运行了1秒。
  • 暂停时间是指GC时,STW的暂停时长。 例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内应用程序线程是完全停止的

Parallel Scavenge 收集器工作流程图:

在这里插入图片描述

使用 -XX:+UseParallelGC 参数指定新生代垃圾收集器为 Parallel Scavenge (此参数同时激活-XX:+UseParallelOldGC 老年代)
-XX:+MaxGCPauseMillis 参数是一个大于0 的整数,虚拟机会尽最大努力的保证GC时间不会超过这个值
-XX:+GCTimeRatio 默认值为99,表示最大GC耗时与总时长的占比应为1/(1+99)= 1%

优点 & 缺点

优点:

  • 高吞吐量 因此 Parallel Scavenge 虚拟机也被称为 “高吞吐量的收集器”

缺点:

  • 吞吐量高了,暂停时间就长了(就像一周打扫一次房间,和两天打扫一次房间的区别)

总结:

高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,不适合在用户交互的场景

五. Parallel Old 收集器

Parallel Old收集器 是Parallel Scavenge收集器的老年代版本,在JDK1.6时才开始提供,支持 多线程 并发,基于 “标记-整理”算法

如果对系统吞吐量要求比较高,JDK1.8后可有限考虑 Parallel Scavenge + Parallel Old 的搭配策略

值得一提的是,在它没有诞生之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择,就算Parallel Scavenge 是高吞吐量的垃圾收集器, 也会被 Serial Old 的单线程工作模式拖累,导致吞吐量上不去

在这里插入图片描述
使用 -XX:+UseParallelOldGC参数指定老年代垃圾收集器为Parallel Old,开启此参数会激活 -XX:+UseParallelGC

总结:

这款垃圾收集器的诞生,“吞吐量优先” 的收集器 终于有了比较搭配的组合,在注重吞吐量或者 CPU资源较为稀缺的场合下,都可以优先考虑 Parallel Scavenge + Parallel Old 的组合

六. CMS收集器

CMS(Concurrent Mark Sweep)收集器 是一款老年代垃圾收集器, 是一种以获取最短回收停顿时间为目标的收集器,尤其重视服务的响应速度 ,非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于 “标记-清除”算法实现的,新生代默认使用ParNew收集器,基于复制算法

它的回收过程为四个步骤进行垃圾回收:初始标记,并发标记,重新标记,并发清除。只有初始标记和重新标记阶段需要STW

在这里插入图片描述

  1. 初始标记(Initial-Mark)阶段 这个阶段中,程序中所有的工作线程都将会 “Stop-the-world”,这个阶段的主要任务仅仅只是 标记出GC Roots能直接关联到的对象,一旦标记完成之后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里的速度非常快

  2. 并发标记(Concurrent-Mark)阶段 从GC Roots的直接关联对象开始遍历整个对象图,这个过程耗时较长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行

  3. 重新标记(remark)阶段 这个阶段是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短,这个阶段用户线程会 STW

  4. 并发清除(concurrent sweep)阶段 清理掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的

使用 -XX:+UseConcMarkSweepGC 参数指定老年代垃圾收集器为CMS垃圾收集器,开启此参数会激活 -XX:+UseParNewGC

优点 & 缺点

优点:

  • 不愧是以最短回收停顿时间为目标的收集器,确实实现了低停顿时间

缺点:

CMS收集器存在着三大缺点

  • 内存碎片化 标记清除算法的通病,如果内存碎片化严重,那么为大对象分配空间时,明明有很多的空闲内存,却没有连续的内存空间可以分配,这样会触发Full GC。
    为了解决这种情况, CMS 提供了 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启) 用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程。虽然内存碎片没有了,但是停顿时间会变长
  • 占用了额外的CPU资源 在整个并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。 CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是 当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大
  • 无法处理浮动垃圾(Floating Garbage) 并发清除阶段用户线程会有不断的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉,这一部分垃圾就被称为 “浮动垃圾”,并且还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用,在JDK1.5的时候,触发CMS收集器执行的阈值为68%(可以通过-XX:+UseCMSInitiatingOccupancyOnly 来设置这个阈值 ),到了1.6 这个阈值提升至 92%。如果预留的内存空间无法容纳新对象的产生,就会出现“并发失败(Concurrent Mode Failure)”,那么虚拟将会冻结用户线程,并且临时使用 Serial Old 老年代垃圾收集器重新进行垃圾收集

七. G1 收集器

G1垃圾收集器(以下简称G1)也是以关注延迟为目标、服务器端应用的垃圾收集器,被HotSpot团队寄予取代CMS的使命,也是一个非常具有调优潜力的垃圾收集器,G1 可以通过参数-XX:MaxGCPauseMillis来设定最大暂停时间

G1并不像其他垃圾收集器那样分代收集,它的收集范围是整个堆空间,G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,但他仍保留了分代的概念

虽然G1仍然保留了分代的概念,但每个分区并不会固定的扮演某个代,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆大约划分为2048个分区

每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor和老年代。存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,可能会暂停所有应用线程
在这里插入图片描述

如上图所示,区域可以分配到Eden,survivor和老年代。此外,还有第四种类型,被称为巨型区域(Humongous Region)。Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC,G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收

每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片

避免全堆扫描——记忆集Remembered Set

为了避免全堆扫描的发生,虚拟机 为G1中每个Region维护了一个与之对应的Remembered Set(RSet)。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier(写屏障)暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过CardTable 把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏

由于RSet的记录要占用分区的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:

  1. 稀少:直接记录引用对象的卡片索引
  2. 细粒度:记录引用对象的分区索引
  3. 粗粒度:只记录引用情况,每个分区对应一个比特位

由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的

读到这里就衍生出了很多问题:

  1. G1 如果保证用户设置的最大暂停时间
    G1 会跟踪每个Region,并且在后台维护一个优先级列表,记录了每个Region回收的性价比,根据停顿时间,来回收性价比最高的Region
  2. 跨区引用对象如果解决
    每个通过记忆集来保存哪个Region指向了自己,记忆集的数据结构是一个哈希表,key就是别的Region的起始地址,value是一个集合,记录了引用对象在所属分区卡片的索引号,这样可以根据分区+卡片索引 的方式找到引用对象
  3. 并发阶段如何做到与用户线程互不打扰的
    G1为每个Region设计了两个指针,当垃圾回收时,这两个指针会划出一小部分区域,其中一个指向了这个区域的起始位置,另外一个指向了区域的结束位置,这个区域用来存储垃圾回收过程中的新对象。如果分配失败,G1会冻结用户线程,进行STW式垃圾回收

G1的回收过程大致可以分为这么几步

  1. 初始标记(Initial Marking )
    这个阶段是STW(Stop the World )的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象
  2. 并发标记
    从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。当并发标记完成后,开始最终标记(Final Marking )阶段
  3. 最终标记
    标记那些在并发标记阶段发生变化的对象,将被回收
  4. 筛选回收
    首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region

G1中提供了两种模式垃圾回收模式,Young GC和Mixed GC,两种都是Stop The World(STW)的

1. YoungGC年轻代收集

在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到另一部分的Survivor区以及Old区

YoungGC的回收过程如下:

  • 根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。
  • 处理Dirty card,更新RSet.
  • 扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
  • 拷贝扫描出的存活的对象到survivor2/old区
  • 处理引用队列,软引用,弱引用,虚引用
2. mixed gc

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,优先选择回收价值大的old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

注意:G1没有fullGC概念,需要fullGC时,调用serialOldGC进行全堆扫描(包括eden、survivor、o、perm)

总结:

G1的第一个重要特点是为用户的应用程序的提供一个低GC延时和大内存GC的解决方案。这意味着堆大小6GB或更大,稳定和可预测的暂停时间将低于0.5秒

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值