目录
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。