垃圾回收的步骤?垃圾回收算法有哪些?各自的优劣?HotSpot 垃圾回收器

目录

垃圾回收的步骤

标记(Mark)

清除(Sweep)

解决碎片

朴素算法

垃圾回收算法有哪些?各自的优劣?

分代

年轻代怎么分区?

怎么进入老年代?

跨代引用怎么处理?卡片标记(card marking)

HotSpot 垃圾回收器

年轻代垃圾回收器

Serial垃圾回收器

ParNew垃圾回收器

Parallel Scavenge 垃圾收集器

老年代垃圾回收器

Serial Old 垃圾收集器

Parallel Old

CMS 垃圾收集器

配置参数

STW


JVM 中有哪些垃圾回收算法?它们各自有什么优劣?

CMS 垃圾回收器是怎么工作的?有哪些阶段?

服务卡顿的元凶到底是谁?

 

按照语义上的意思,垃圾回收,首先就需要找到这些垃圾,然后回收掉。但是 GC 过程正好相反,它是先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后删除。所以垃圾回收只与活跃的对象有关,和堆的大小无关。

垃圾回收的步骤

标记(Mark)

使用可达性分析找到活跃对象,并标记。如图:

清除(Sweep)

将未被标记的对象回收掉。如图:

图中的清除方式有问题,会产生碎片。

解决碎片

复制(Copy)

提供一个对等的内存空间,将存活的对象复制到该空间中,然后回收原内存空间。浪费了一半的空间来做这事。一般遇到扩缩容或者碎片整理问题时,复制算法都是非常有效的。比如:HashMap 的扩容也是使用同样的思路,Redis 的 rehash 也是类似的。

整理(Compact)

移动所有存货对象,按照内存地址依次排序,回收末端地址之后的内存。效率上低于复制算法。

朴素算法

  • 标记-清除(Mark-Sweep):效率一般,缺点会造成碎片问题。
  • 复制算法(Copy):效率最高,缺点是造成一定的空间浪费。
  • 标记-整理(Mark-Compact):效率最差。

我们就需要一些算法来改善朴素算法。

垃圾回收算法有哪些?各自的优劣?

分代

算法的提出往往基于一定的假设,GC算法基于的是弱代假设(weak generation hypothesis),即对象大部分死得快,其它活得时间长。

把死得快对象占用的区域,叫年轻代(Young generation),活得时间长的对象所占的区域,称为老年代(Old generation、Tenured Generation)。注意分代指的是空间不是对象。

年轻代怎么分区?

因为年轻代死得快,因此使用效率较高的复制算法。这里又有一个假设,大部分对象都会在第一次GC被回收。基于这一个假设可以改善空间浪费的问题,将年轻代空间分为三份,比例为8 : 1 : 1,这样就可以将50%的空间浪费降至10%,通过过程就知道为什么是10%。

如年轻代分为:伊甸园空间(Eden)、两个幸存者空间(Survivor)。具体过程如下:

  • 当Eden区满了之后,触发GC。

  • 第一次GC会存活的对象复制到其中一个幸存者空间(如图中from),并清空原空间;

  • Eden区再次满了之后,触发第二次GC,Eden区和from区的存活对象,会被复制到to区;

  • 如此往复,只要保证一个幸存者空间空置(10%)就行。

两个关键点:Eden满触发GC、一个幸存者空间空置。把握这两个关键点,就掌握年轻代。空间分配的比例可以通过 -XX:SurvivorRatio配置,默认为8。

TLAB

TLAB 的全称是 Thread Local Allocation Buffer,JVM 默认给每个线程开辟一个 buffer 区域,用来加速对象分配。这个 buffer 就放在 Eden 区中。

这个道理和 Java 语言中的 ThreadLocal 类似,避免了对公共区的操作,以及一些锁竞争。

对象的分配优先在 TLAB上 分配,但 TLAB 通常都很小,所以对象相对比较大的时候,会在 Eden 区的共享区域进行分配。TLAB 是一种优化技术,类似的优化还有对象的栈上分配(这可以引出逃逸分析的话题,默认开启)。

怎么进入老年代?

老年代一般使用“标记-清除”、“标记-整理”算法,因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,还不如采取就地收集的方式。

进入老年代的途径有4种:

1、提升(Promotion)

每发生一次GC,存活的对象年龄加1,年龄达到阈值时,进入老年代。通过参数-XX:+MaxTenuringThreshold进行配置,它占用4bit,所以最大值是15。

2、分配担保

看一下年轻代的图,每次存活的对象,都会放入其中一个幸存区,这个区域默认的比例是 10%。但是我们无法保证每次存活的对象都小于 10%,当 Survivor 空间不够,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。

3、大对象直接在老年代上分配

超出某个大小的对象将直接在老年代分配。这个值是通过参数 -XX:PretenureSizeThreshold 进行配置的。默认为 0,意思是全部首选 Eden 区进行分配。

4、动态对象年龄判定

它会使用一些动态的计算方法。比如,如果幸存区中相同年龄对象大小的和,大于幸存区的一半,大于或等于 age 的对象将会直接进入老年代。

通过下图可以看一下一个对象的分配逻辑。

跨代引用怎么处理?卡片标记(card marking)

对象的引用关系是一个巨大的网状。有的对象可能在 Eden 区,有的可能在老年代,那么这种跨代的引用是如何处理的呢?由于 GC 是单独发生的,如果一个老年代的对象引用了它,如何确保能够让年轻代的对象存活呢?对于是、否的判断,我们通常都会用 Bitmap(位图)和布隆过滤器来加快搜索的速度。

位图:https://blog.csdn.net/ETalien_/article/details/90752420 用模确定存放区间、用余数确定存放位置,用按位与运算确定是否存在。

布隆过滤器:https://www.jianshu.com/p/2104d11ee0a2 不同哈希函数生成多个hash值,能够判断一定不存在、可能存在。

JVM 也是用了类似的方法。其实,老年代是被分成众多的卡页(card page)的(一般数量是 2 的次幂)。卡表(Card Table)就是用于标记卡页状态的一个集合,每个卡表项对应一个卡页。

如果年轻代有对象分配,而且老年代有对象指向这个新对象, 那么这个老年代对象所对应内存的卡页,就会标识为 dirty,卡表只需要非常小的存储空间就可以保留这些状态。

垃圾回收时,就可以先读这个卡表,进行快速判断。

HotSpot 垃圾回收器

年轻代垃圾回收器

Serial垃圾回收器

处理GC的只有一条线程,在回收时停止一切线程。

因为简单,所以高效,通常应用于客户端应用上。因为客户端应用不会频繁创建很多对象,用户也不会感觉出明显的卡顿。相反,它使用的资源更少,也更轻量级。

ParNew垃圾回收器

Serial的多线程版本,由多条GC并行的运行。运行期间需要停止用户线程

ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。

Parallel Scavenge 垃圾收集器

另一个多线程版本的垃圾回收器。它与 ParNew 的主要区别是:

  • Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,适合没有交互的后台计算。弱交互强计算。

  • ParNew:追求降低用户停顿时间,适合交互式应用。强交互弱计算。

老年代垃圾回收器

Serial Old 垃圾收集器

与年轻代的 Serial 垃圾收集器对应,都是单线程版本,同样适合客户端使用。

年轻代的 Serial,使用复制算法。

老年代的 Old Serial,使用标记-整理算法。

Parallel Old

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。

CMS 垃圾收集器

CMS(Concurrent Mark Sweep)收集器是以获取最短 GC 停顿时间为目标的收集器,它在垃圾收集时使得用户线程和 GC 线程能够并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。我们会在后面的课时详细介绍它。

长期来看,CMS 垃圾回收器,是要被 G1 等垃圾回收器替换掉的。在 Java8 之后,使用它将会抛出一个警告。Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.

配置参数

除了上面几个垃圾回收器,我们还有 G1、ZGC 等更加高级的垃圾回收器,它们都有专门的配置参数来使其生效。通过 -XX:+PrintCommandLineFlags 参数,可以查看当前 Java 版本默认使用的垃圾回收器。

 

以下是一些配置参数:

  • -XX:+UseSerialGC 年轻代和老年代都用串行收集器
  • -XX:+UseParNewGC 年轻代使用 ParNew,老年代使用 Serial Old
  • -XX:+UseParallelGC 年轻代使用 ParallerGC,老年代使用 Serial Old
  • -XX:+UseParallelOldGC 新生代和老年代都使用并行收集器
  • -XX:+UseConcMarkSweepGC,表示年轻代使用 ParNew,老年代的用 CMS
  • -XX:+UseG1GC 使用 G1垃圾回收器
  • -XX:+UseZGC 使用 ZGC 垃圾回收器

尤其注意 -XX:+UseParNewGC 这个参数,已经在 Java9 中就被抛弃了。很多程序(比如 ES)会报这个错误,不要感到奇怪。

线上使用最多的垃圾回收器,就有 CMS 和 G1,以及 Java8 默认的 Parallel Scavenge。

  • CMS 的设置参数:-XX:+UseConcMarkSweepGC。
  • Java8 的默认参数:-XX:+UseParallelGC。
  • Java13 的默认参数:-XX:+UseG1GC。

STW

如果在垃圾回收的时候(不管是标记还是整理复制),又有新的对象进入怎么办?为了保证程序不会乱套,最好的办法就是暂停用户的一切线程。也就是在这段时间,你是不能 new 对象的,只能等待。表现在 JVM 上就是短暂的卡顿,什么都干不了。这个头疼的现象,就叫作 Stop the world。简称 STW。

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值