小白学JVM系列-新生代垃圾收集器

347 阅读7分钟

今天,我整理了下学习 JVM 时的心得,分享给大家,希望无论是对初学 JVM 还是准备面试的程序猿都能有些帮助。

大家好,我是吴明。 这一次我给大家讲下,垃圾收集器。

回顾

上一次,我们讲了一个理论、三个算法,我们还说到所有的垃圾收集器都来源于此。 那么接下来,我们开始具体讲垃圾收集器。

本文涉及到的名词:

  1. Serial:新生代收集器,单线程
  2. ParNew:新生代收集器,多线程
  3. ParallelScanvenge: 新生代收集器,多线程
  4. Throughout:吞吐量
  5. GC 线程:垃圾回收器的工作线程
  6. 用户线程:开发人员写的用户程序的执行线程
  7. Stop the world:暂停所有用户线程
  8. Safe Point:安全点,用户线程可以停止下来的地点

新生代和老年代的由来

大家先看一张图

初次看这张图的人可能会有些发懵。

别急,这样看。

这个图用颜色分成上下两部分。
上面浅色的部分,是新生代的收集器。
下面深色的部分,是老年代的收集器。

等等,吴明,为什么这么分,新生代和老年代的收集器还不一样吗?

是的,不一样。还记得上一次我们提到的一个理论吗? 现在用到它了,拿出来复习下:

  1. 大部分对象存活时间很短
  2. 存活时间越久的对象越不容易被回收。

这个理论告诉了我们什么,只是告诉了我们这两句话吗?别闹! 它告诉我们,对象与对象是不同的,它们可以按照存活时间分成两类:

  1. 第一类对象只占用内存一小段时间就被释放
  2. 第二类对象会占用内存很长时间才被释放

那么我是不是就可以做个优化啊?对。

  1. 第一类对象,我们回收的频率高一点
  2. 第二类对象,我们回收的频率低一点

自然而然的,我们针对这两类对象,用的收集器就不一样。

这就是一块内存分成新生代老年代的由来。

好了,回到这个图。
上半部分列出的新生代收集器有:Serial、ParNew、Parallel Scavenge。
下半部分列出的老年代收集器有:CMS、Serial Old、Paralle Old。
中间的连线说明如何配合使用新生代和老年代收集器。 如果我对新生代使用的是 Serial 收集器的话,对老年代我有哪些选择? 看连线。我可以选 CMS,也可以选 Serial Old。

注意,这里有个特殊的收集器是 Garbage First(简称 G1)。它既可以对新生代使用,又可以对老年代使用。G1是一个伟大的创新。

这几个收集器,我们会一个一个的讲。重点是 CMS、G1。

Serial

首先看 Serial,它是一种单线程的垃圾收集器。 关键字:单线程。 看图,大概是这样子。

一开始是用户线程在跑,需要垃圾收集时,所有用户线程会暂停,GC 线程会继续跑。

Stop the world

用户线程会发生 Stop the world。
Stop the world 是什么意思?就是说整个世界都停了。我们的程序世界“Hello World” 停止了。

在这里就是用户线程都停止了,应用会暂停。
当垃圾收集器都执行完了之后,用户线程又会继续执行。
看这个图,用户线程有多个,而 GC 线程只有一个,这个不是我偷懒,少画了,而是 GC 线程就只有一个。
这里有个安全点,在这个点会发生Stop the world。安全点的具体知识我在后面讲 CMS 时详细讲。

具体应用

那这种 Serial 用在哪里?用在客户端。内存比较小的情况下,效率非常高。为什么高?因为单线程模式,没有上下文切换开销。虽然有暂停,但是暂停时间非常的短,原因是内存小,扫描的时间也不会太久。

算法

还有一点很关键,垃圾收集器都来源于三个算法。Serial 是哪个算法?
记住 Serial 是标记-复制算法。
再请记住一句话,新生代都是标记-复制算法。

ParNew

ParNew 是多线程版本的 Serial 收集器。
代码几乎和 Serial 完全一致,差别 Serial 是单线程,ParNew 是多线程。

我们来看图

不偷懒,我们依旧描述一下它的整体过程:
一开始是用户线程在跑,需要垃圾收集时,所有用户线程会暂停,多个 GC 线程开始工作。
GC的具体工作就是:

  1. 标记新生代的存活对象
  2. 清除新生代的非存活对象。

当垃圾收集器都执行完了之后,用户线程又会继续执行。

整个回收过程都是 Stop the world

ParNew 和 Serial对比

  1. 相同点:都是标记复制算法,都是新生代收集器,都是全程 Stop the world
  2. 不同点:Serial 是单个 GC 线程,ParNew 是多个 GC 线程。

关联

为什么之前有了 Serial,又出现了 ParNew 呢? 因为在 Java 早期,CPU 内核也是稀缺资源。单核的机器很普遍,而线程是 CPU 运行的最小单元。
不是有道面试题:进程和线程有什么区别吗? 进程是资源分配的最小单元,而线程是CPU执行的最小单元。 所以,在那个时代,Serial 完全满足需求。 后来 CPU 的核数普遍上来了,Serial 也需要做出改变,于是就有了 ParNew。

Parallel Scavenge

这是第三种垃圾收集器。Parallel 是并行的意思,Scavenge 一般指对新生代的扫描。单纯从语义来看,它和 ParNew 没什么区别,但是它却是 Jdk7 中新生代的默认垃圾收集器。
它有什么优势呢? 和 ParNew 相比,它引入了一个新的概念——吞吐量。吞吐量其实就是用户线程执行时间除以总时间。公式如下:
用户线程执行总时间/(用户线程执行总时间+GC线程执行总时间)
举个例子: 程序跑了100分钟,其中用户线程跑了99分钟,GC 线程跑了1分钟。 那程序的吞吐量就是99。

它的执行流程如下图,和 ParNew 一样的:

首先用户线程在执行,接着 Stop the world,GC线程开始跑。 ParNew 和 Paralle Scavenge 都是并行的垃圾收集器。

GC 的并发和并行

这里,有个问题需要搞清楚。什么是并行?什么是并发? 简单的说:

  1. 并行=多个 GC 线程一起执行
  2. 并发=用户线程GC 线程一起执行

如何控制吞吐量

Parallel Scavenge 提取了两个变量:

  1. 停顿时间
  2. 吞吐量大小

GC 线程根据这两个变量来控制线程的执行时间。

具体参数如下:

  1. -XX:MaxGCPauseMillis 垃圾收集停顿时间
  2. -XX:GCTimeRatio 吞吐量大小

吞吐量的思考:
这里我们需要思考一个问题,是不是停顿时间越短越好?看下面的例子:

  • 调整前:新生代占用空间1000M,停顿时间为100ms,回收后释放了900M。需要在1s后进行下次回收。
  • 调整:停顿时间设为20ms。
  • 调整后:新生代占用空间1000M,停顿时间20ms,回收后释放了200M,5ms后就需要进行下次回收。

停顿时间从100ms调整为20ms,单位时间回收的次数增多了,但是每次回收释放的空间减少了,很快空间又会不够。

那么是缩短停顿时间,保证回收频率,还是增大停顿时间,保准回收空间足够大?这是一个平衡问题。平衡点该如何去找呢?
JVM 给了你一个自适应参数:
+UseAdaptiveSizePolicy:自适应调解 GC 回收策略
这个参数意味着,很多相关参数,JVM 会自己帮你配置好,不再需要你操心。JVM 会自己寻找那个最适合的点。如果启用这个自适应的参数,在跟踪 GC log 时你会看到一个词 GC Ergonomics。

总结

这节课,一口气讲了三个新生代收集器,是不是一点不难啊!
别着急,挑战就要来了,下一次我们讲老年代收集器。其中的 CMS 是面试的重点,知识点和细节都非常多。 学好了CMS,后续的几个收集器也都会了。

复习下,本节课的内容。

最后

精彩持续不断,请大家关注微信公众号:无名岛上