漫画JVM(二)JVM之垃圾回收-经典按代垃圾收集器

公众号:爱码叔漫画软件设计(搜:爱码叔)
个人博客站点: icodebook
博主:爱码叔
专注于软件设计与架构、技术管理。擅长用通俗易懂的语言讲解技术。对技术管理工作有自己的一定见解。文章会第一时间首发在个站上,欢迎大家关注访问!

上一篇文章讲述了在 Java 世界中,对象如何产生和运行(直达电梯)。出生一定会伴随着消亡,这是一个亘古不变不变的自然规律。现实世界中,如果生物没有消亡,那么早晚地球要被撑爆。在程序世界中,如果对象不会消亡,那么等来的也将是 OOM 导致的 “世界” 毁灭。当前这轮文明被彻底消亡,重启后进入下一轮文明。(嗯?有点三体的味道)

那么这次咱们就来聊聊 Java 对象是怎么 “没” 的。本文将讲解如下内容。

1、垃圾收集算法介绍

2、按代收集——新生代

3、按代收集——老年代

按Region管理内存的垃圾收集器 G1 和 Z(也是按代的思想), 咱们以后再做分析。

1 垃圾收集算法介绍

1.1 如何整理书柜

咱们可以打个比方,垃圾收集就像整理书柜。我比较喜欢读书,虽然家中有个比较大的书柜,但依然经常被装满。我每隔一段时间就会整理一次书柜。我书柜的布局如下图:
在这里插入图片描述
“新购书籍” 区域摆放我最近购入的书籍,一般是新技术、新热点相关书籍。“常用书籍区域” 摆放的是我近期工作会用到的书籍。“经典书籍“ 区域摆放经典著作,我时不时会拿出来翻看,例如《设计模式》、《重构》等书。

大量新购入的书籍会被我摆放到“新购书籍” 区域。一段时间后,该区域将会摆满了书,没有空余空间。这个时候就要对书柜进行整理,整理的过程类似于 JVM 的垃圾收集。

“新购书籍” 区域的整理方式如下:

  1. 不会再次阅读的书籍, 我会挑出来,装箱后收到储藏室
  2. 近期还会用到的书籍,会被我摆放到 “常用书籍” 区域
  3. 我认为值得长期阅读的经典书籍,我会直接放入“经典书籍” 区域
  4. 留下来的书籍会被整理好,摆放整齐

在清理的第2,3步,很可能相应的区域也已经没有空间,那么也需要相应的清理。

对于 “常用书籍” 区域整理方式如下。

  1. 属于经典,值得长期阅读的书籍,会被我放入“经典书籍” 区域
  2. 近期的工作不再需要,也没有长期阅读价值的书, 会被我装箱后放到储藏室

对于“经典书籍” 区域整理方式如下。

  1. 已经很久没有看过的书籍, 会被我装箱后放到储藏室

以上对书柜的整理体现了两个思想。

  1. 按书籍被使用的周期对书柜进行区域划分
  2. 识别出不再需要的书籍,从书柜上清理掉,装箱封存
  3. 整理留下来的书籍,让书籍整齐的摆放在应该在的区域。

对书柜的一次整理,就好比进行了一次内存的垃圾收集。能够长期留在书柜中的一定是我长期需要的书籍。内存中的对象也是如此 ,如果熬过了一轮轮的垃圾收集, 那么必将成为 “经典”。
在这里插入图片描述
如果你理解了整理书柜的过程,那么你已经理解了基于分代的垃圾收集器 80% 的知识点。对于下面 JVM 垃圾收集的理解会轻松很多。

1.2 内存区域划分

垃圾收集在很长一段时间中都是基于分代的思想管理内存,将内存分为新生代和老年代。不过从 G1 开始,到后来出现的 Z,则是使用Region 的区域概念管理内存。

1.2.1 按代划分内存区域

在书柜的例子中,我的书柜被划分为三个区域。

  1. “新购书籍”区域(新生代-Eden)
  2. “常用书籍” 区域(新生代-Survivor)
  3. “经典书籍” 区域(老年代)

这种区域的划分就是一种分代的思想。相对应的内存区域标注在后面。

1.2.2 按Region划分内存区域

G1和Z垃圾收集器将内存划分为若干个Region。这是一种将大块内存分而治之的思想。虽然在名字上已经抛弃了新生代和老年代,但各个Region其实还在扮演的新生代和老年代,此时老年代和新生代不再固定,而是由若干个Region构成的动态集合。

1.3 垃圾收集算法

整理书柜的方式,其实就是整理书柜的算法。主要的垃圾收集算法有如下几种。我们分别来看。

1.3.1 标记清除

这个算法比较简单,分为标记和清除两个阶段。

  1. 在标记阶段,标记所有存活对象,这就好比整理书柜时,挑出留下的书籍。
  2. 清除阶段会将所有未被标记的对象清除。这就好比将不保留的书籍从书柜上拿下来,打包封箱后,可以封存、扔掉、卖废品。

在这里插入图片描述

经过这两个阶段,内存中可以被清理的对象已经被清除,内存空间得到了释放。虽然释放内存空间的目的已经达到,但是问题也很明显——产生大量不连续的内存碎片。这就像清理书柜,如果只是把不保留的书拿掉,那么书柜上的空余空间并不连续。这会导致大的对象无法放入内存中,再次触发垃圾收集。

1.3.2 标记整理

针对标记清除造成大量不连续内存碎片的问题,标记整理算法提供了解决方案。它与标记清除的不同在于清除阶段的处理。它并没有直接清除掉可回收对象,而是将需要保留的对象向内存空间一端紧凑对齐,避免碎片空间产生。对齐完成后,再将存活对象边界之外的内存全部清理。

这个过程有点像我在整理书柜时,将留下的书全部排列到书柜一端。然后把后面所有的书打包装箱。

在这里插入图片描述

标记整理算法很好的解决了内存碎片问题。但是由于要移动对象,有着更高的成本。移动对象意味着所有对被移动对象引用的地方都需要更新引用值。这就好比整理完书柜后,书已经不在原来的位置,我按照形成的印象位置去找某本书,会发现并不在那里。程序更是如此,如果移动了对象在内存中的位置,必须更新所有引用者持有的引用值,否则程序一定会出错。因此,程序需要暂停,更新对象引用。此时,整个程序世界停止运转,这就是著名的 “Stop The World”。

1.3.3 标记复制

标记清除算法还存在一个不足,未被标记的对象,需要逐一清理。如果存活的对象少,需要清理的对象多,这种方式的效率并不高。标记复制算法使用 “空间换时间” 的办法,解决这个问题。标记复制算法的核心思想是,提供一块额外的内存区域,保持干净未被使用。当垃圾收集发生时,先把存活的对象紧凑地放入这块内存区域,然后将原内存区域一次性清理干净。

在这里插入图片描述

标记复制算法适用于存活对象占比小的场景,提升回收效率,并且不会产生内存碎片。但它的缺点是需要额外的内存空间用于对象复制。在极端情况下,内存的使用率只能达到50%。

1.3.4 小结

每种算法都有自身独特的优点和缺点,有其适用的场景。因此,针对对象在内存中不同的驻留周期,需要选择适合的算法进行垃圾收集。

2 按代回收-新生代

Java 程序中的方法被频繁调用,同时伴随着频繁的调用结束。这意味着大量的对象被创建出来,但是随着方法执行完成,这些对象又迅速成为了垃圾对象。有研究表明,新生代98%的对象熬不过一轮垃圾收集。

当一个对象被创建,就像新买回来的一本书,会先进入 “新生代-Eden” 区域。Eden 的意思是伊甸园,意为保存出生不久的对象。

当一轮垃圾收集发生时,Eden 区域绝大多数的对象会被清理掉。这就像新书读完后,只有少部分会被留在书柜上。这部分存活对象会进入新生代的 Survivor 区域。由于存活下来的对象非常少,所以Suvivor区域会比Eden小很多。另外,新生代的垃圾收集算法基于标记复制,所以需要两块 Survivor 区域,这两块 Suvivor 轮流用做复制区域。Eden、Survivor 0、Survivor 1 的默认比例为8:1:1。

在这里插入图片描述

每次 GC 发生时,Eden区域和正在使用的 Survivor 区域中存活的对象,会被放入另外一块 Survivor 区域,当前的Eden 和 Survivor 区域被清空。

在这里插入图片描述
新生代的垃圾收集器主要有如下几种。

1、Serial 垃圾收集器

最早的垃圾收集器。Serial是一个单线程的垃圾收集器,采用标记复制算法。当它进行垃圾收集时,必须要 “Stop The World”。对于内存资源有限制、或者单核处理器,也有其用武之地。

2、ParNew 垃圾收集器

ParNew时Serial的多线程版本。他和Serial的运行机制几乎完全一样,只是使用了多条线程。它的辉煌是因为CMS垃圾收集器的出现。CMS是一款老年代垃圾收集器,它的厉害之处在于可以让垃圾收集线程和用户线程同时工作,从而降低 “Stop The World” 的发生频率。CMS只能和新生代垃圾收集器 Serial 或者 ParNew 搭配使用,因此大部分相对复杂的场景,用户都会选择ParNew+CMS。在JDK9之后,官方只保留了 ParNew+CMS 的垃圾收集器组合。其余的组合全部取消。

3、Parallel Scavenge 垃圾收集器

Parallel Scavenge 同样基于标记复制算法,也是多线程工作。它和ParNew的区别之处在于,它关注点在于吞吐量。吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间)。它的特点是在用户设置了关注的吞吐量后,会根据监控数据,自动调整参数,例如 Eden 和 Survivor 的比例、晋升老年代的对象大小等,以尽量达到设置的目标。

3 按代回收-老年代

新生代-Eden 区域的对象在熬过一轮垃圾收集后,该对象的年龄+1,将会进入Survivor区域。如果对象在 Survivor 区域又熬过了数轮垃圾收集,年龄达到一定阈值,说明这是一个生命周期较长的对象,将会被移入另外一块内存区域——老年代。老年代和新生代默认大小比例为 2:1。

老年代中的对象来源有如下几种。

  1. 新生代中的对象熬过N轮垃圾收集后进入老年代

N的默认值为15 ,通过 -XX:MaxTenuringThreshold 参数可以进行调节,但是不能超过15,这是因为对象头中用4 bit 记录对象的年龄,只能记录到 15。

  1. 大对象直接进入老年代

大对象如果存活时间较长,会在新生代中经历多次复制。这样做的效率极低,复制开销极大。因此,比较好的做法是让大对象直接进入老年代,大对象的判定值可以通过参数 -XX:PretenureSizeThreshold 进行设置。

  1. 动态判断对象年龄

新生代中的对象年龄并不一定必须达到固定的 N,才会进入老年代。假如存在一个年龄 M,如果占据 Survivor 区域中一半空间大小的对象的年龄都小于 M,那么大于、等于年龄 M 的对象直接进入老年代。换句话讲,当Survivor 区域空间使用超过一半时,必定存在满足条件的年龄M。这个规则可以确保 Survivor 区域有半数以上的空间可用。

老年代的垃圾收集器主要有如下几种。

1、Serial Old 垃圾收集器

Serial 垃圾收集器的老年代版本,同样是单线程,但是老年代适合使用标记整理算法。在 JDK 5 之前,它可以搭配Parallel Scavenge 收集器使用。另外,他是 CMS 垃圾收集器失败后的备选收集器。

2、Parallel Old 垃圾收集器

Parallel Scavenge 垃圾收集器的老年版本,支持多线程,同样使用了标记整理算法。JDK 6后,它才出现,搭配Parallel Scavenge 使用。

3、CMS 垃圾收集器

上面讲到的老年代收集器均使用标记整理算法,而 CMS 垃圾收集器基于标记清除算法,以追求更少的停顿为目标。CMS工作分为如下四个阶段

  1. 初始标记阶段(需要stop the world,时间很短)
  2. 并发标记阶段(与用户线程并行)
  3. 重新标记阶段(需要stop the world,稍长,但远远低于并发标记阶段)
  4. 并发清除(与用户线程并行)

可以看到 CMS 将标记阶段分为了三步。第一步初始标记阶段,仅标记 GC Root 直接关联的对象,因此停顿很短。第二步与用户线程并发执行,遍历对象图,标记所有可回收对象。这一步最为耗时,但是可以与用户线程并发执行。第三 步用于修正在第二步并发执行期间产生的变动,正常情况下,这一步时间虽然稍长,但还是远远小于第二步。

标记阶段拆分为三步后,最为耗时的第二步不会 “Stop The World”,大大降低了停顿时间。

最后,由于采用了标记清除算法,并不需要移动对象,所以在清除阶段也不会因为移动对象而停顿。

CMS 虽然有着低停顿的优势,但是同样也有如下几个问题。

  1. 由于和用户线程并发执行,在垃圾收集期间,会影响到用户线程的执行,导致系统变慢。尤其是对于不足 4 核的CPU。这是因为 CMS 默认回收线程数是(CPU 核数+3)/4。
  2. 在并行处理期间,垃圾并没有被彻底清理完,而是伴随着新垃圾的产生。这部分在清理期间产生的垃圾称为浮动垃圾。在并发清理垃圾的时候,需要为浮动垃圾留有足够多的内存空间,否则会面临OOM的风险。这会导致老年代不能接近用满时才进行回收。但即使如此,还是可能在并发清除的过程中,内存无法容纳新产生的对象。这会导致 “并发失败”,JVM 会启用备案 Serial Old 进行回收。老年代使用比例 的参数为-XX:CMSInitiatingOccupancyFraction。这个比例过高会造成留给浮动垃圾的空间过小,导致频繁启用 Serial Old 回收,性能下降。比例过低则导致老年代使用率低,回收频繁。
  3. CMS基于标记清除算法,不移动对象,达到了不停顿的目的。但是,标记清除算法会产生内存碎片。CMS有两种方式解决这个问题。第一个是被动方式,当老年代的连续内存空间无法容纳一个大对象时,提前触发一次包括碎片整理的 Full GC。第二种是主动的方式,当经历过 N 次不进行碎片整理的 Full GC 后,触发一次进行碎片整理的 Full GC。

4 按代垃圾收集总结

根据对象生命周期的长短,JVM 为对象划分了不同的存储区域,这样 JVM 可以根据对象的特点采用针对性的回收算法。新生代中的对象有着朝生夕灭的特点,JVM在进行垃圾收集时,存活的对象数量少,复制的工作量较小,因此主要采用标记复制的算法进行回收。老年代中的对象生命周期长,反复进行复制的成本高,因此主要采用标记整理算法。但是标记整理需要 Stop The World,会导致用户线程停顿。CMS收集器,采用标记清除的办法,能够大幅度减少停顿。但是由于标记清除会产生内存碎片,需要定期进行碎片整理。

下图是按代回收的垃圾收集器总结,连线代表可以搭配使用的垃圾收集器。

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值