JVM 垃圾回收算法和常见的垃圾回收器

前言

前文已经介绍了Java虚拟机的内存区域,这一篇文章就重点来探讨一下关于JVM中垃圾回收的知识点,这一块的知识点也是面试中经常考察的地方,比如常见的垃圾回收算法、常见的垃圾回收器等等,废话不多说,直接进入正题。

前置知识

垃圾回收主要集中在Java堆中进行,为对象分配空间,回收垃圾对象空间

image-20210506211846427

在搞清楚垃圾的回收之前,要知道对象在堆内存中是如何进行分配的,对象的分配遵循以下三种原则:

  • 对象优先在Eden区分配

    堆空间被分为新生代和老年代,这样划分的目的主要是为了针对不同存活时间的对象分区域更好的进行垃圾回收。在一个对象首次被分配内存并且Eden区中有足够的空间容纳时,对象会首先在Eden区中进行内存分配.

  • 大对象直接进入老年代

    当一个对象需要较大的空间,Eden区没有这么大的空间可以分配时,对象会直接在老年代进行分配

  • 长期存活的对象进入老年代

    虚拟机使用分代收集算法来管理堆内存,那么分配在Eden区的对象,如果经过一次Minor GC 之后依然存活,并且Survivor有足够的空间,这个存活下来的对象,会被移动到两个Survivor区中的任意一个,并且年龄将变为1,当年龄不断地累计,到达一个阈值之后,这个对象就会晋升到老年代,参数-XX:MaxTenuringThreshold可以设置这个晋升阈值。

对象已死

在了解了前置知识之后,我们应当想到要进行垃圾回收,首先得判断哪些对象是垃圾?

识别垃圾主要依靠一下两种算法

  • 引用计数法(Reference Counting)

    对每个对象地引用进行计数,每当有一个地方引用该对象时计数器+1,引用失效则-1,对象头中存放着引用的计数,计数大于0的对象就被认为是存活的对象,小于0则意味着对象可以被回收。循环引用的问题虽然可以通过Recycler算法解决,但是在多线程环境下,引用计数的变更要进行昂贵的同步操作,性能低下,在Java虚拟机中并没有使用这种方式。

  • 可达性分析法(Tracing GC)

    从被称为GC Root的节点为起点,开始进行搜索,这些被引用的节点形成引用链,可以被搜索到的对象就判定为可达对象,如果该对象到GC Root没有任何引用链相连的话,此对象在后续将会被回收。下图中绿色的对象就是存活的对象,灰色的为可回收的对象。

image-20210509151738618

补充:

可作为GC Root对象的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

引用的分类

垃圾回收的一系列操作都和引用有关,无论是通过引用计数算法计算对象的引用数量,还是通过可达性分析算法哦按段对象是否可达,判定对象是否存活都和引用有关。jdk1.2之前对于引用的定义还比较传统,1.2之后引用被分为强引用、软引用、弱引用、虚引用4种。

  • 强引用

    使用范围最广,Java程序员使用的大部分是强引用,只要一个对象存在强引用的关系,垃圾收集器永远不会对其回收,即使是在内存不足的情况下,虚拟机宁愿抛出OOM错误,也不会将其回收。

  • 软引用

    具有软引用的对象属于有用,但非必须的对象,被软引用关联的对象,在系统内存不够的时候,就会被虚拟机所回收,可以使用SoftReference类来实现软引用。

  • 弱引用

    具有弱引用的对象也属于非必须的对象,但强度比软引用还要更低一些,弱引用对象只能存活到下一次垃圾收集发生为止,和软引用的区别在于,只要发生垃圾收集行为,不管内存是否足够,弱引用对象都会被回收。

  • 虚引用

    虚引用属于那种可以当它不存在的引用,它是最弱的一种引用关系,一个对象是否具有虚引用完全不会影响对象的生命周期,虚引用存在的目的只是为了在对象被垃圾收集器回收时收到一个系统通知,可以通过PhantomReference类来实现虚引用。

垃圾回收算法

在能够识别垃圾对象之后,怎样回收这些垃圾似乎又是一个问题,在Java虚拟机中解决这个问题的主要依靠垃圾算法,下面就详细阐述以下这几种常见的垃圾回收算法。

分代收集理论

在如今虚拟机中,基本都遵循着“分代收集”的理论进行设计,它属于他是建立在经验法则之上的,它建立在两个分代假说之上:

弱分代假说:大部分对象都是朝生夕死的。

强分代假说:经过多次垃圾回收过程依然存活的对象,存活的几率就会很大。

以上两个假说可以用一句话总结:强者恒强,弱者恒弱。

分代收集理论在Java虚拟机中的具体表现为:将堆空间划分为两个区域:新生代、老年代,针对不同的区域采用不同的垃圾收集算法,由于大部分对象一开始是在新生代进行空间分配的,在新生代的大部分对象都比较符合朝生夕死的规律,所以新生代被划分为 Eden区和两块大小相同的Survivor区,能够存活的对象在Survivor区中来回切换,直到对象的年龄到达晋升阈值进入老年代。

标记 — 清除算法

标记—清除算法主要分为“标记”和“清除”两个阶段。首先标记所有需要回收的对象,标记完成之后,统一回收被标记的对象。

它的缺点也很明显:会造成大量的内存碎片,需要进行大量的标记动作,效率较低。

image-20210510211648086

标记—复制算法

原理:将内存分为两块大小相同的区域,每次只使用其中的一块,当其中的一块内存使用完了,就将存活的对象移动到另一块区域,然后将已经使用过的内存一次清理干净。这种算法地优势就在于解决了标记清除算法所带来的大量内存碎片的问题,在新生代中就有对这种算法的应用。缺点也显而易见:同一时刻只有一半的空间得到利用,对空间的浪费较多。
image.png

标记—整理算法

原理:先对垃圾对象进行标记,然后将存活的对象向内存的一端进行移动,然后清理掉边界之外的内存区域,它与标记清除算法的差异在于它中间进行了一次移动,他的优点在于在对内存空间高效利用的同时尽量减少了内存碎片。

image-20210510214353653

常见的垃圾收集器

有了以上的垃圾回收算法,垃圾回收器就是对垃圾回收算法的具体实践。不同的垃圾回收器有着各自的优缺点,适用的场景都不一样,所以需要我们程序员根据不同的场景来选择不同的垃圾收集器。

Serial收集器

Serial收集器时历史最长的收集器,它是基于单线程工作的收集器,Serial收集器在工作时,必须暂停其他所有的工作线程,直到它收集结束,从这就能看出它在进行垃圾回收的时候,会造成停顿,会给用户的体验造成一定影响。但是即便如此,Serial收集器在单线程方面有着很高的收集效率,很适合在客户端模式下运行的虚拟机。

image-20210512210546357

ParNew收集器

ParNew收集器可以理解为是Serial收集器的多线程版本,在垃圾收集阶段它是使用多条线程来进行垃圾收集的,其他行为都和Serial收集器完全一样,在JDK9之后ParNew只能与CMS互相搭配使用

image-20210512212043345

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一款基于标记—复制算法实现的新生代收集器。它和ParNew收集器很类似,但Parallel Scavenge 收集器的关注点在于达到一个可控制的吞吐量,这里说的吞吐量是指:处理器用于运行用户代码的时间于处理器总消耗时间的比值,Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。

Serial Old收集器

Serial Old收集器Serial 收集器的老年代版本,也是一个单线程版本,主要是供虚拟机在客户端模式下使用。在服务端模式下它有两种用途:1.在JDK5以及以前的版本中与Parallel Scavenge 收集器搭配使用,2.作为CMS收集器发生失败时的后备方案。

image-20210512210546357

Parallel Old收集器

Parallel Old是Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记—整理算法实现。

image-20210512214615247

CMS收集器

CMS(Concurrent Mark Sweep)收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。CMS收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW的阶段主要是:初始标记 和 重新标记)

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

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

  3. 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,并且也会导致“Stop-the-World”的发生,但也远比并发标记阶段的时间短。

  4. 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

image-20210513212032156

CMS 的优点与缺点:

优点

​ 并发收集
​ 低延迟
缺点

​ 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
CMS收集器无法处理浮动垃圾。

G1收集器

G1(Garbage-First)垃圾回收器是在Java7 之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以G1收集器也被Oracle官方称为“全功能收集器”。G1 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器.

使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过

XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。

虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。

一个Region有可能属于Eden,Survivor或者Old/Tenured内存区域。但是一个Region只可能属于一个角色。
image-20210513214135121

特点:

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1 收集器的运作大致分为以下几个步骤:

image-20210513212434359

  • 初始标记:标记一下GCRoots能够直接关联到的对象
  • 并发标记:从GCRoot开始向堆中对象进行可达性分析,找出要回收的对象,耗时较长
  • 最终标记:对用户线程做做一个短暂的暂停
  • 筛选回收:更新Region的统计数据,根据用户所期望的停顿时间来制定回收计划,将需要回收的那一部分Region里面的存活对象复制到空的Region中,然后再清理整个旧的Region空间。

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

ZGC收集器

ZGC是在JDK 11加入的一款具有实验性质的低延迟垃圾收集器。它是尽可能减少对吞吐量的影响,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在10毫秒以内的低延迟。是一款基于Region内存布局的,不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记—整理算法的,以低延迟为首要目标的一款垃圾收集器。目前在生产环境中使用居多的还是JDK8,所以使用的也不是很多,感兴趣的可以看一下美团技术团队的这篇文章:新一代垃圾回收器ZGC的探索与实践

参考:《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第三版》

-------------end------------

之后会继续更新Java虚拟机的相关文章,来都来了点一波关注再走。微信公众搜索Java驭风

qrcode_for_gh_f251a2f45b45_258

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值