JVM 垃圾回收算法&垃圾收集器

对象是否可回收判断方式

引用计数法

对象每被引用一次,其引用计数就+1,不再引用时就-1,这样虽然简单高效,但是无法解决互相引用的问题,比如A持有B,B持有A A a=new A(); B b =new B(); a.instance=b; b.instance=a; a=null;b=null; 这样引用计数都不为0,也就无法回收

可达性分析

确定一批GC Roots引用,然后以这些引用为出发点一级级的查找可以引用到的对象,但凡不能被引用到的对象,就属于废对象,搜索经过的路径称为引用链

哪些对象可以作为GC Roots呢?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

垃圾回收算法

标记-清除算法(Mark-Sweep)


这是最基础的收集算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法(Copying)


为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。

但是这种算法的效率相当高,所以,现在的商业虚拟机都采用这种收集算法来回收新生代。为什么新生代可以使用复制算法呢?

IBM 有专门研究表明,新生代中的对象 98% 都是朝生夕死,所以就不需要按照1:1的比例来划分内存空间。这里鉴于此,新生代采用了如下的划分策略。

现在把新生代再划分为三部分,一块较大的 Eden(伊甸园) 和两块较小的 Survivor(幸存者) 区域。

当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot 虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。

这样清理完成后,原来的 Survivor 就空了,并一直保持为空,直到下次 Minor GC 时,它再作为存活对象的盛放地。两个 Survivor 就这样轮流当做 GC 过程中新生代存活对象的中转站。

但是,如果使用复制算法的内存区域有大量的存活对象时,复制算法就会变得捉襟见肘,这时需要更大的 Survivor 区用于盛放那些存活对象,甚至可能需要 1:1的比例。所以针对堆内存区域的老年代,就有了下面的算法。

标记-整理算法


标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这种方法避免了碎片的产生,同时也不需要一块额外的内存空间,对于老年代会比较合适。

但是相比复制算法,虽然该算法占用的内存空间少,但是耗费的垃圾回收时间会比复制算法久,所以上面也说了

我们应该尽量避免或者减少 Full GC 的发生。

这两种算法用精炼的语言描述就是

  • 复制算法:用空间换时间

  • 标记-整理算法:用时间换空间

一句话 鱼与熊掌不可兼得,但是针对新生代和老年代,他们都是最佳的选择。

分代回收算法

一个应用启动,操作系统会给他分配一个初始的内存大小,由上可知,这部分内存大部分应该属于堆内存,JVM 为了更好地利用管理这部分内存,对该区域做了划分。一部分成为新生代,另一部分称为老年代。

一开始对象的创建都发生在新生代,随着对象的不断创建,如果新生代没有空间创建新对象,将会发生 GC ,这时的 GC 称之为 Minor GC,位于新生代的对象每经过一次 Minor GC 后,如果这个对象没有被回收,则为自己的标记数加1,这个标记数用于标识这个对象经历了多少次的 Minor GC,对于 Sun 的 Hotspot 虚拟机,如果这个次数超过 15 ,该对象才会被移动到老年代。

随着时间的推移,如果老年代也没有足够的空间容纳对象,老年代也会试着发起 GC,这时的 GC 被称为 Full GC

相比 Minor GC,Full GC 发生的次数比较少,但是每发生一次 Full GC,整个堆内存区域都需要执行一次垃圾回收,这对程序性能造成的影响比 Minor GC 大很多。所以我们应该尽量避免或者减少 Full GC 的发生

同时,在堆内存区域,发生最多的 GC 情形就是新生代的 Minor GC 了,因为所有的对象会优先去新生代开辟空间,所以这块的内存变化会很快,只有内存不够用,就会发生 GC,但是一般的 Minor GC 执行比 Full GC 快很多。为什么呢?因为新生代和老年代的垃圾回收算法不一样。

这个算法并没有什么新鲜的东西,只是根据java对象的生存规律(大部分对象都是朝生夕死)将堆分为新生代和老年代,对象都在新生代中创建,大部分也会在新生代中被回收,而留下的大年龄对象都被转移到老年代中,对于新生代,由于存活率很低,所以适合采用复制法,而老年代对象存活率高,采用标记清除法或者标记移动法进行回收

小结

简单梳理一下文中讲到的一些知识点

  • 为了更好的管理堆内存,该区域分为新生代和老年代。

  • 新生代发生垃圾回收要比老年代频繁。

  • 新生代发生的垃圾回收成为 Minor GC;老年代发生的 GC 成为 Full GC。

  • 新生代使用复制算法进行垃圾回收;老年代使用标记-整理算法

  • 为了更高效管理新生代的内存,按照复制算法,结合 IBM 的研究论证,新生代分为三块,一块比较大的 Eden 区和两块比较小的 Survivor 区,比例为 8:1:1

垃圾收集器

基本概念

串行、并行和并发

计算机系统的信息交换有两种方式:并行数据传输方式和串行数据传输方式。

  • 串行: 计算机中的串行是用 Serial 表示。A 和 B 两个任务运行在一个 CPU 线程上,在 A 任务执行完之前不可以执行 B。即,在整个程序的运行过程中,仅存在一个运行上下文,即一个调用栈一个堆。程序会按顺序执行每个指令。

  • 并行: 并行性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同 CPU 上同时执行。比如,A 和 B 两个任务可以同时运行在不同的 CPU 线程上,效率较高,但受限于 CPU 线程数,如果任务数量超过了 CPU 线程数,那么每个线程上的任务仍然是顺序执行的。

  • 并发: 并发指多个线程在宏观(相对于较长的时间区间而言)上表现为同时执行,而实际上是轮流穿插着执行,并发的实质是一个物理 CPU 在若干道程序之间多路复用,其目的是提高有限物理资源的运行效率。 并发与并行串行并不是互斥的概念,如果是在一个CPU线程上启用并发,那么自然就还是串行的,而如果在多个线程上启用并发,那么程序的执行就可以是既并发又并行的。

在这里插入图片描述

JVM 垃圾收集中的串行、并行和并发

在 JVM 垃圾收集器中也涉及到如上的三个概念。

  • 串行(Serial): 使用单线程进行垃圾回收的回收器。

  • 并行(Parallel): 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

  • 并发(Concurrent): 指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。

在了解了这些概念之后,我们开始具体介绍常用的垃圾收集器。

串行垃圾回收器

如上所述,串行回收器是指使用单线程进行垃圾回收的回收器,每次回收时串行回收器只有一个工作线程,对于并发能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的表现。串行回收器可以在新生代和老年代使用,根据作用的堆空间不同,分为新生代串行回收器和老年代串行回收器。

Serial

Serial收集器是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即 STW(Stop The World,服务暂停)。到现在为止,它依然是虚拟机运行在 client 模式下的默认新生代收集器。

参数控制:-XX:+UseSerialGC 使用串行收集器。

Serial Old

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

UseSerialGC:开启此参数使用 Serial & Serial Old 搜集器(client 模式默认值)。

并行垃圾回收器

并行回收器是在串行回收器的基础上做了改进,它可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机来说,可以有效的缩短垃圾回收所需的实际时间。

ParNew

ParNew 收集器是一个工作在新生代的垃圾收集器,它只是简单的将串行收集器多线程化,它的回收策略和算法和串行回收器一样。新生代并行,老年代串行;新生代复制算法、老年代标记-整理。

参数控制:-XX:+UseParNewGC 使用 ParNew 收集器;-XX:ParallelGCThreads 限制线程数量

除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

Parallel

Parallel 是采用复制算法的多线程新生代垃圾回收器,Parallel 收集器更关注系统的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;

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

可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-整理

参数控制:

  • -XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间

  • -XX:GCTimeRatio: 设置吞吐量的大小(默认是99)

  • -XX:+UseAdaptiveSeizPolicy: 打开自适应模式,当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

Parallel Old

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,采用多线程和标记-整理算法,也是比较关注吞吐量。在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel 加 Parallel Old 收集器。

参数控制:-XX:+UseParallelOldGC 使用 Parallel Old 收集器;-XX:ParallelGCThreads 限制线程数量。

CMS 垃圾回收器

CMS(Concurrent Mark Sweep)并发标记请除,它使用的是标记-清除法,工作在老年代,主要关注系统的停顿时间。

CMS 并不是独占的回收器,也就是说,CMS 回收的过程中应用程序仍然在不停的工作,又会有新的垃圾不断的产生,所以在使用CMS的过程中应该确保应用程序的内存足够可用,CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值(默认为68)的时候开始回收,也就是说当老年代的空间使用率达到68%的时候会执行CMS。如果内存使用率增长很快,在CMS执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失败,虚拟机将启动老年代 Serial 进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC的停顿时间可能较长,所以阀值的设置要根据实际情况设置。

  • 初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快;

  • 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

  • 并发清除: 开启用户线程,同时GC线程开始对未标记的区域做清扫。

主要优点: 并发收集、低停顿。

但是它有下面三个明显的缺点:

  • 对CPU资源敏感

  • 无法处理浮动垃圾

  • 使用的 标记-清除 算法会导致收集结束时会有大量空间碎片产生

标记清除法产生的内存碎片问题,CMS 提供提供了一些优化设置,可以设置完成 CMS 之后进行一次碎片整理,也可以设置进行多少次 CMS 回收后进行碎片整理。

参数控制:

  • -XX:+UserConcMarkSweepGC: 使用 CMS 垃圾清理器

  • -XX:CMSInitatingPermOccupancyFraction: 设置阀值

  • -XX:ConcGCThreads: 限制线程数量

  • -XX:+UseCMSCompactAtFullCollection: 设置完成 CMS 之后进行一次碎片整理

  • -XX:CMSFullGCsBeforeCompaction: 设置进行多少次 CMS 回收后进行碎片整理

G1(Garbage First)

G1(Garbage First) 垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在 JDK7 就已加入 JVM 的收集器大家庭中,成为 HotSpot 重点发展的垃圾回收技术。

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。包括:Eden、Survivor、Old 和 Humongous。

其中,Humongous 是特殊的 Old 类型,回收空闲巨型分区,专门放置大型对象。这样的划分方式意味着不需要一个连续的内存空间管理对象。G1 将空间分为多个区域,优先回收垃圾最多的区域。一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?

当然不是,每个 Region 都有一个 Remembered Set(已记忆集合),用于记录本区域中所有对象引用的对象所在的区域,从而在进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对所有堆内存的遍历。

同 CMS 垃圾回收器一样,G1 也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用 G1 来代替选择 CMS。G1 最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至 CMS 的众多缺陷。

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

  • 初始标记: 初始标记阶段仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 的值,让下一个阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这一阶段需要停顿线程,但是耗时很短。

  • 并发标记: 并发标记阶段是从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

  • 最终标记: 而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remenbered Set Logs 里面,最终标记阶段需要把Remembered Set Logs 的数据合并到 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这一阶段需要停顿线程,但是可并行执行。

  • 筛选回收: 最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。

在这里插入图片描述
G1 能充分利用多 CPU、多核环境下的硬件优势,使用多 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的GC动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。

此外,与其他收集器一样,分代概念在G1中依然得以保留。虽然 G1 可以不需其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。

空间整合: 与 CMS 的 标记-清理 算法不同,G1 从整体看来是基于 标记-整理 算法实现的收集器,从局部(两个 Region 之间)上看是基于 复制 算法实现,无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。

可预测的停顿: 这是 G1 相对于 CMS 的另外一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器特征了。

参数控制:-XX:+UseG1GC

小结

本文介绍了常见的7种不同分代的收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

  • 新生代收集器: Serial、ParNew、Parallel Scavenge

  • 老年代收集器: Serial Old、Parallel Old、CMS

  • 整堆收集器: G1

根据收集的区域(年轻代或年老代)和收集器自身的特性,可以有如下组合:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel/Serial Old、Parallel/Parallel Old、G1。

ZGC 来了。ZGC 是 JDK11 中要发布的最新垃圾收集器。完全没有分代的概念,官方给出 ZGC 的优点是无碎片,时间可控,超大堆。读者可以尝试了解和使用一下 ZGC 。

垃圾收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式。单线程、Client模式下默认新生代收集器
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案。Serial的老年代版本、单线程、Client模式下使用
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合。Serial的多线程版本、Server模式下默认收集器、默认线程数=CPU数量
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务。多线程、目标关注吞吐量
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务。Parallel Scavenge的老年代版本、多线程、关注吞吐量
CMS 并发 老年代 标记-清除 响应速度优先 并发低停顿、关注最短停顿时间。集中在互联网站或B/S系统服务端上的Java应用
G1 并发 新生代+老年代 标记-整理+复制算法 响应速度优先 面向服务端应用,将来替换CMS
  • 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 停顿时间短则响应速度好提升用户体验;高吞吐量则CPU利用率高,适合后台运算

Reference

发布了152 篇原创文章 · 获赞 27 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/u010979642/article/details/103862862