深入理解Java虚拟机(四)——HotSpot垃圾收集器详解

垃圾收集器

新生代收集器

1.Serial收集器

特点:

  • 单线程工作,收集的时候就会停止其他所有工作线程,用户不可知不可控,会使得用户界面出现停顿。
  • 简单高效,是所有收集器中额外内存消耗最少的。
  • 没有线程交互的开销,单线程收集效率高。
  • 对于客户端模式下的虚拟机是一个很好的选择。
  • 采用标记复制算法。
2.ParNew收集器

是Serial收集器的多线程版本。采用多条GC线程并行地清理垃圾。任然需要在清理过程中停止一切用户线程。

特点:

  • 多线程执行,适合多处理器环境,单处理器效率不如Serial。
  • 采用标记复制算法。
  • 追求降低垃圾收集时用户线程的停顿时间。

如何降低用户线程停顿:

  • 在多CPU环境中使用多条GC线程,从而垃圾回收的时间减少,从而用户线程停顿的时间也减少;
  • 实现GC线程与用户线程并发执行。所谓并发,就是用户线程与GC线程交替执行,从而每次停顿的时间会减少,用户感受到的停顿感降低,但线程之间不断切换意味着需要额外的开销,从而垃圾回收和用户线程的总时间将会延长。
3.Parallel Scavenge收集器

与ParNew非常相似,是多线程新生代的收集器,采用标记复制算法。ParNew收集器追求降低用户线程的停顿时间,因此适合交互式应用,但是它的目标是达到一个可控的吞吐量,适合没有交互的后台应用。

所谓吞吐量是处理器用于运行用户代码的时间与处理器总耗时的比值。

Parallel Scavenge提供的参数:

  • 设置“吞吐量”
    通过参数-XX:GCTimeRadio设置垃圾回收时间占总CPU时间的百分比,直接设置吞吐量大小。

  • 设置“停顿时间”
    通过参数-XX:MaxGCPauseMillis设置垃圾处理过程最久停顿时间。收集器尽力保证垃圾回收的时间不超过这个值,时间下降的同时,吞吐量也会下降。

  • 启用自适应调节策略
    通过命令-XX:+UseAdaptiveSizePolicy就能开启自适应策略。我们只要设置好堆的大小和MaxGCPauseMillis或GCTimeRadio,收集器会自动调整新生代的大小、Eden和Survior的比例、对象进入老年代的年龄,以最大程度上接近我们设置的MaxGCPauseMillis或GCTimeRadio。

老年代收集器

1.Serial Old收集器

是serial收集器的老年代版本,同样是单线程收集。区别是老年代的使用标记整理算法,而新生代是使用标记复制算法。

2.Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,支持多线程并发收集。它们会搭配使用,针对在注重吞吐量或者处理器资源较为稀缺的场合。
区别是它是基于标记整理算法的。

3.CMS 收集器

CMS是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。它在工作时,可以使用户线程和GC线程并行执行,但是存在用户线程和GC线程切换的额外开销,回收的总时间会被延长。

回收过程:

  1. 初始标记:只是标记下GC Roots能直接关联到的对象,需要停顿用户线程,但是很快。
  2. 并发标记:从GC Roots的直接关联对象开始遍历整个堆的过程,这个过程比较慢,但是不需要停顿用户线程。
  3. 重新标记:为了修正并发标记期间,用户线程导致的标记产生变动的那部分记录,比初始标记时间稍微长一些,需要停顿用户线程。
  4. 并发清除:利用清除线程,删除被标记一句死亡的对象,过程很慢,不需要停顿用户线程。
    在这里插入图片描述

存在缺点:

  • 吞吐量低:因为收集过程中用户线程和GC线程并行执行,会有线程切换的额外开销。
  • 无法处理浮动垃圾,导致频繁的Full GC产生
    由于垃圾清理过程中,并发标记和并发清理阶段,用户线程还在继续运行,就会继续产生新的死亡对象,而这部分没有被标记,就需要都能到下次垃圾收集标记,这部分垃圾就称为浮动垃圾
    如果CMS在垃圾清理过程中,用户线程需要在老年代中分配内存时发现空间不足时,就需要再次发起Full GC,而此时CMS正在进行清除工作,因此此时只能由Serial Old临时对老年代进行一次Full GC。
  • 标记-清除算法产生内存碎片
    应对方法:
    1. 开启-XX:+UseCMSCompactAtFullCollection
      开启该参数后,每次FullGC完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块儿。但每次都整理效率 不高,因此提供了以下参数。
    2. 设置参数-XX:CMSFullGCsBeforeCompaction
      本参数告诉CMS,经过了N次Full GC过后再进行一次内存整理。

通用垃圾收集器G1

是目前最优秀的垃圾收集器。

特点:
  1. 追求停顿时间
  2. 多线程
  3. 面向服务端应用
  4. 混合使用标记整理和标记复制孙发,不会产生内存碎片
  5. 堆整个堆进行垃圾回收
  6. 可预测停顿时间
G1的内存模型

开创基于Region的内存布局,不存在固定的新生代和老年代。将Java堆划分为很多大小相等的独立区域,每个Region都可以根据需要扮演新生代或者老年代空间。还存在Humongous区域用来存储大对象。

对各个Region区域的垃圾进行估值,然后优先处理收益最大的那些Region,保证收集器尽可能高的收集效率。

利用记忆集解决跨Region引用的问题,只需要扫描变脏的Region区域,就能避免对这个堆进行扫描。

G1运作过程:
  1. 初始标记:只是标记GC Roots直接关联的对象,需要停顿用户线程,很快。
  2. 并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆,找出需要回收的对象,耗时长,但是可以与用户线程并发执行。
  3. 最终标记:标记出并发标记过程中用户新产生的垃圾,需要暂停用户线程,很快。
  4. 筛选回收:移动存活对象到空的Region中,清空整个Region,多条收集线程并行,需要暂停用户线程。·
    在这里插入图片描述
G1与GMS比较

G1优点:以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集这些创新性设计带来的红利。整体采用标记整理,局部采用标记复制,不会产生内存空间碎片,空间利用率高。
G1缺点:垃圾收集时产生的内存占用和程序运行时的额外执行负载高于GMS。空间换时间。

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
  • CMS收集器以最小的停顿时间为目标的收集器;G1收集器可预测垃圾回收的停顿时间;
  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
  • CMS无法解决浮动垃圾问题,G1则通过最终标记处理浮动垃圾。

具体实现细节知识点

根节点枚举

所谓根节点枚举就是在垃圾收集过程中,利用可达性算法标记有效对象前,寻找GC Roots的集合。

固定可作为GC Roots的节点主要在全局性的引用(如常量)与执行上下文(例如栈帧中的本地变量表)中。

存在问题:在根节点枚举的时候必须暂停用户线程,存在Stop the world的困扰。否则分析结果的准确性无法保证。

主流的Java虚拟机采用的都是准确式垃圾收集,所以在用户线程停顿之后需要一个不漏的检查完所有的执行上下文和全局变量中的引用位置。在HotSpot的解决方案中,利用称为OopMap的数据结构,在类加载完成时,将对象的数据计算出来,在即时编译的时候,在特定的位置记录下栈和寄存器中那些位置时引用,这样垃圾收集器在扫描的时候就利用这些信息进行获取引用,就不需要从方法区一个不漏地开始查找。

安全点

存在问题:利用OopMap,HotSpot可以快速地完成GC Roots的枚举,但是如果将使OopMap内存变化的指令都生成相应的数据,就会导致耗费大量的额外内存空间。

解决方案:生成OopMap时,只需要再特定的位置记录对象信息,这些位置被称为安全点。通过安全点,在用户线程中不是代码指令流的任意位置都能停顿下来进行来回收,而是必须到到安全点后才能暂停。

安全点的注意问题:

  • 安全顶点位置选取:安全点不能太少,否则会使得收集器等待时间过长,也不能太多,这样频繁运行增大内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点
  • 如果在垃圾收集时,让所有线程都跑到最近的安全点暂停:
    1. 抢断式中断:不需要线程主动配合,发生垃圾收集的时候系统就把所有线程中断,如果发现某个线程不在安全点上,则恢复这条线程的执行,让它跑到安全点上。几乎没有虚拟机采用这个方式。
    2. 主动式中断:垃圾收集时,系统不对线程直接操作,而是设置线程上的一个标志位,各个线程在执行的时候,每当跑到安全点时就去判断这个标志位,如果发现这个标志被修改,就主动暂停运行。

安全区域

存在问题:安全点保证程序在运行的时候在短时间内都能停顿下来,进入垃圾收集过程的信息收集。但是,如果线程本来就被阻塞或者睡眠了,就无法进入安全点挂起自己。

解决方案:必须引入安全区域来解决,是指能够确保在某一段代码片中,引用关系不会发生变化,因此在这个区域的任意地方开始垃圾收集都是安全的。可以把安全区域看多被拉伸的安全点。

当用户线程进入安全区域,就会标识自己,这样虚拟机在发生垃圾回收的时候就不会管这些在安全区的线程。当线程离开安全区,虚拟机就会要求其提交根节点枚举,没有完成就继续留着。这样,线程被阻塞在安全区域,就不会有影响,因为其引用没有发生改变。

记忆集与卡表

存在问题:老年代中的对象可能会引用新生代中的对象,这样在Minor GC的时候,就需要扫描整个老年代,非常耗时。

解决方案:通过记忆集记录从非收集区指向收集区的内存区域集合。这样,需要收集新生代的时候,只需要扫描被记忆集标识过的老年代,就能检测出所有的跨代引用,而不需要扫描整个老年代。

记忆集中的记录粒度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

其中卡精度所指的是利用卡表的方式实现记忆集。这是最常用的实现方式。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。卡表最简单的形式可以只是一个字节数组,数组的每个元素对于一个内存块,这个块被称为卡页。当卡页中的字段存在跨代引用,那么这个页就被变脏,垃圾收集的时候,只需要将脏的卡页加入GCRoots中一并扫描。

写屏障

存在问题:如何维护卡表元素。

解决方案:Hotspot通过写屏障来维护卡表状态。这个写屏障有点像spring的AOP切面,当发生引用对象的赋值,就会产生一个around环形通知,在通知中完成卡表的状态更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值