Java GC专家系列1:理解Java垃圾回收

澳门新葡亰3522平台游戏 6

回到垃圾回收上,在开始学习GC之前你应该知道一个词:stop-the-world。不管选择哪种GC算法,stop-the-world都是不可避免的。Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。GC调优通常就是为了改善stop-the-world的时间。

Parallel Old GC(-XX:+UseParallelOldGC)

JDK 5 以后开始支持 Parallel Old GC。与并行 GC 相比,唯一的区别是这个 GC
算法是为老年代设计的。它的执行一共有三个步骤:标记-汇总-压缩。汇总这一步为
GC 已经执行过的区域单独标记存活的对象,这一步和 标记-交换-压缩
算法中的交换步骤是不一样的。这需要通过更复杂的步骤来完成。

本文是成为Java GC专家的案例介绍GC调优相关的内容。

垃圾回收的来源

Java 不会在代码中手动指定一块内存再释放它。有的开发者会将相关对象置为
null 或者使用 System.gc() 方法手动释放内存。设置为 null
不是什么大问题,但是调用 System.gc()
方法会剧烈的影响系统的性能,所以不应该使用。(幸好,我还没有看到 NHN
的开发者有使用这个方法。)

在 Java
中,由于开发者不需要在代码中手动释放内存,垃圾搜集器会查找不需要的对象(垃圾)并释放它们。垃圾搜集器基于以下两条假设创建(称它们为推测或者先决条件也许更准确)

  • 大多数对象很快变成不可达。
  • 只存在少量从老的对象到新对象的引用

这些假设称为“弱分代假设”(weak generational
hypothesis)
,为了强化这一假设,HotSpot
虚拟机在物理上分为两个部分-新生代(young generation)
老年代(old generation)

新生代:大多数新创建的对象都存放在这里。因为大多数对象很快就会变得不可达,很多对象都在新生代创建,然后就消失。当一个对象从这个区域消失的时候,我们就说发生了一次“小的
GC”(minor GC)

老年代:那些在新生代存活下来,并没有变成不可达的对象被复制到这里。它通常要比新生代大。由于容量更大,GC
发生的次数就没有新生代频繁。当对象从老年代消失时,我们就说发生了一次“大
GC”(major GC)
(或者是 “全 GC”(full GC))。

我们一起来看一下这幅图:

澳门新葡亰3522平台游戏 1

图1:GC 区域和 数据流程

上图中的持久代(permanent generation)通常也称为“方法区(method
area)”
,它用于存储类或者字符常量。所以这个区域不是用于永久存储从老年代存活下来的对象。这个区域也可能会发生
GC。这个区域发生的 GC 也算作大 GC。

有人可能会想:

如果一个处于老年代的对象需要引用一个处于新生代的对象会怎么样?

为了解决这个问题,在老年代有一个称为“card
table”
的东西,是一个512字节大小的块。当老年代中的对象要引用一个新生代的对象时,它就会被记录在这个
table 中。当新生代执行 GC 的时候,只需要搜索这个 table
来确定它是否属于需要 GC 的对象,而不用检查老年代所有引用的对象。card
table 通过 write barrier 管理。write barrier 给小 GC
性能上带来极大的提升。尽管会有一点额外的开销,但是 GC 的总体时间减少了。

澳门新葡亰3522平台游戏 2

图2:Card Table 的结构

新生代的结构

为了深入理解GC,我们先从新生代开始学起。所有的对象在初始创建时都会被分配在新生代中。新生代又可分为三个部分:

  • 一个Eden
  • 两个Survivor

在三个区域中有两个是Survivor区。对象在三个区域中的存活过程如下:

  1. 大多数新生对象都被分配在Eden区。
  2. 第一次GC过后Eden中还存活的对象被移到其中一个Survivor区。
  3. 再次GC过程中,Eden中还存活的对象会被移到之前已移入对象的Survivor区。
  4. 一旦该Survivor区域无空间可用时,还存活的对象会从当前Survivor区移到另一个空的Survivor区。而当前Survivor区就会再次置为空状态。
  5. 经过数次在两个Survivor区域移动后还存活的对象最后会被移动到老年代。

如上所述,两个Survivor区域在任何时候必定有一个保持空白。如果同时有数据存在于两个Survivor区或者两个区域的的使用量都是0,则意味着你的系统可能出现了运行错误。

下图向你展示了经过minor GC把数据迁移到老年代的过程:

澳门新葡亰3522平台游戏 3
图3: GC前后

在HotSpot
VM中,使用了两项技术来实现更快的内存分配:”指针碰撞(bump-the-pointer)“和”TLABs(Thread-Local
Allocation Buffers)
“。

Bump-the-pointer技术会跟踪在Eden上新创建的对象。由于新对象被分配在Eden空间的最上面,所以后续如果有新对象创建,只需要判断新创建对象的大小是否满足剩余的Eden空间。如果新对象满足要求,则其会被分配到Eden空间,同样位于Eden的最上面。所以当有新对象创建时,只需要判断此新对象的大小即可,因此具有更快的内存分配速度。然而,在多线程环境下,将会有别样的状况。为了满足多个线程在Eden空间上创建对象时的线程安全,不可避免的会引入锁,因此随着锁竞争的开销,创建对象的性能也大打折扣。在HotSpot中正是通过TLABs解决了多线程问题。TLABs允许每个线程在Eden上有自己的小片空间,线程只能访问其自己的TLAB区域,因此bump-the-pointer能通过TLAB在不加锁的情况下完成快速的内存分配。

本小节快速浏览了新生代上的GC知识。上面讲的两项技术无需刻意记忆,只需要明白对象开始是创建在Eden区,然后经过在Survivor区域上的数次转移而存活下来的长寿对象最后会被移到老年代。

G1 GC

最后,我们一起来看一下垃圾优先(G1)GC。

澳门新葡亰3522平台游戏 4

图6:G1 GC 的布局

如果你想理解 G1
GC,忘掉你所知道的新生代和老年代的所有一切。如上图所示,每一个对象被分配到每个网格中,然后会执行
GC。一旦一个区域被填满,对象就会被分配到另一个区域,然后执行一次
GC。在G1 GC
中,将数据从新生代的3个区域移动到老年区的所有步骤都不存在。G1 GC
的创建时用于替换 CMS GC,因为从长远看后者会引发很多问题。

G1 GC 最大的优点是性能。它比我们前面讨论过的任何 GC
类型都要快。但是在 JDK 6中,这是一个所谓的早期版本所有只能用于测试。JDK
7的官方版本中已经包含这一类型 GC。以我个人的意见,我们在将 JDK 7应用到
NHN
的实际服务之前需要很长的时间的测试(至少一年),所以你可能需要等待一段时间。同时我听说了几次在
JDK 中使用 G1 GC 后JVM出现崩溃。所以请继续等待直到它更稳定。

本文译自:Understanding Java Garbage
Collection

老年代垃圾回收

当老年代数据满时,便会执行老年代垃圾回收。根据GC算法的不同其执行过程也会有所区别,所以当你了解了每种GC的特点后再来理解老年代的垃圾回收就会容易很多。

在JDK 7中,内置了5种GC类型:

  1. Serial GC
  2. Parallel GC
  3. Parallel Old GC(Parallel Compacting GC)
  4. Concurrent Mark & Sweep GC (or “CMS”)
  5. Garbage First (G1) GC

其中Serial
GC务必不要在生产环境的服务器上使用
,这种GC是为单核CPU上的桌面应用设计的。使用Serial
GC会明显的损耗应用的性能。

下面分别介绍每种GC的特性。

Parallel GC(-XX:+UseParallelGC)

澳门新葡亰3522平台游戏 5

图4:Serial GC 和 Parallel GC 之间的差别

从这张图片上很容易发现Serial GC 和 Parallel GC 之间的差异。Serial GC
只是用一个线程执行 GC,parallel GC 使用多个线程执行
GC,所以更快。当内存足够并且 CPU 内核够多时这种 GC
非常有用。它也被称作”吞吐量 GC(throughput GC)。“

Parallel Old GC(-XX:+UseParallelOldGC)

Parallel Old GC在JDK 5中被引入,与Parallel
GC相比唯一的区别在于Parallel的GC算法是为老年代设计的。它的执行过程分为三步:标记(mark)–总结(summary)–压缩(compaction)。其中summary步骤会会分别为存活的对象在已执行过GC的空间上标出位置,因此与mark-sweep-compact算法中的sweep步骤有所区别,并需要一些复杂步骤才能完成。

老年代的 GC

老年代在数据存满时会执行 GC。各种 GC
的执行过程因类型而异,所以如果你知道不同类型的 GC, 理解起来会容易一些。

在 JDK 7中,一共有5中类型的 GC。

  • 1、Serial GC
  • 2、Parallel GC
  • 3、Parallel Old GC(Parallel Compacting GC)
  • 4、ConCurrent Mark & Sweep GC (CMS)
  • 5、Garbage First(G1)GC

所有这些 GC 当中,serial GC 不可以在服务端使用。这种 GC 在只有一个
CPU 的桌面系统中才会创建。使用 serial GC 会明显的降低应用的性能。

现在我们一起来学习每一种 GC。

上面是我个人的主观的看法,但我相信熟练掌握GC是成为优秀Java程序员的必备技能。如果你对GC执行过程感兴趣,也许你只是有一定的开发应用的经验;如果你仔细考虑过如何选择合适的GC算法,说明你对你所开发的程序有了全面的了解。当然这对一个优秀的程序员来说未必是一个通用的标准,但很少人会反对我关于”理解GC是作为优秀Java程序员的必备技能”的看法。

新生代的组成

为了理解 GC,
我们先了解一下新生代,也就是对象第一次被创建的地方。新生代被分成3个区域。

  • 一个 Eden
  • 两个 存活(Survivor)

总共3个区域,其中两个是存活区。每一个区域的执行顺序是这样的:

  • 1、大部分新创建的对象都处于 Eden 区
  • 2、在 Eden 区域执行第一次 GC
    以后,存活下来的对象被移动到其中一个存活区。
  • 3、在 Eden 区域再次执行 GC
    以后,存活下来的对象继续堆积已经有对象的那个存活区。
  • 4、一旦一个存活区被存满,存活对象就会被移动到另一个存活区。然后被存满的那一个存活区数据就会被清掉(修改为无数据状态)。
  • 5、如此反复一定次数之后,还处于存活状态的对象被移动到老年区。

如果你仔细检查这些步骤,存活区域总是有一个是空的。如果两个存活区域同时都有数据,或者同时都为空,这意味着你的系统存在问题

通过小 GC 将数据堆积到老年代的过程可以参考下图:

澳门新葡亰3522平台游戏 6

图3:GC 前后

注意在 HotSpot
虚拟机中,有两种技术用于快速内存分配。一个成为“bump-the-pointer”,另一个称为“TLABs(Thread-Local
Allocation Buffers)”

Bump-the-pointer 技术跟踪 Eden 区域最后分配的对象。那个对象将处于
Eden 区域的顶部。如果有新的对象需要创建,只需要检查对象的大小是否适合
Eden 区域。如果合适,新的对象将被放在 Eden
区域,并且新的对象处于顶部。所以,当创建新的对象时,只需要检查上一次创建的对象,这样可以做到较快的内存分配。但是,如果是在多线程环境那将是另外一个场景。为了保证
Eden
区域多线程使用的对象是线程安全的,将不可避免的使用锁,这会导致性能的下降。HotSpot
虚拟机使用 TLABs 来解决这个问题。使用 TLABs 允许每一个线程在 Eden
区域有自己的一小块分区。由于每一个线程只能访问它们自己的 TLAB,即使是
bump-the-pointer 技术也可以不使用锁就分配内存。

到现在我们快速的概述了新生代的
GC。你不必完全记住我刚才所提到的两种技术。你不知道它们也没什么大不了。但是请记住:对象是在
Eden 区域创建,然后长期存活的对象通过存活区移动到老年代。

Serial GC(-XX:+UseSerialGC)

在前面介绍的年轻代垃圾回收中使用了这种类型的GC。在老年代,则使用了一种称之为”mark-sweep-compact“的算法。

  1. 首先该算法需要在老年代中标记出存活着的对象
  2. 然后从前到后检查堆空间中存活的对象,并保持位置不变(把不再存活的对象清理出堆空间,称为空间清理)
  3. 最后,把存活的对象移到堆空间的前面部分以保持已使用的堆空间的连续性,从而把堆空间分为两部分:有对象的和无对象的(称为空间压缩)

Serial GC适用于CPU核数较少且使用的内存空间较小的场景。

Serial GC(-XX:+UseSerialGC)

上一段中我们介绍的新生代的 GC 使用的是这种类型。老年代的 GC 使用叫做
“标记-清除-压缩(mark-sweep-compact)”的算法。

  • 澳门新葡亰3522平台游戏,1、这个算法的第一步是标记老年代中的存活对象
  • 2、然后、从头开始检查堆,将存活的对象放到后面(交换)
  • 3、最后一步,用存活对象从头开始填充堆,这样这些存活对象连续堆放,并且将对分为两部分:一部分有对象另一部分没有对象(压缩)

Serial GC 适合小型内存和有少量CPU 内核的环境。