java 垃圾回收的来源(Java垃圾回收详解)
java 垃圾回收的来源(Java垃圾回收详解)新生代:大多数新创建的对象都存放在这里。因为大多数对象很快就会变得不可达,很多对象都在新生代创建,然后就消失。当一个对象从这个区域消失的时候,我们就说发生了一次“小的 GC”(minor GC)。这些假设称为“弱分代假设”(weak generational hypothesis),为了强化这一假设,HotSpot 虚拟机在物理上分为两个部分-新生代(young generation) 和 老年代(old generation)。Java 不会在代码中手动指定一块内存再释放它。有的开发者会将相关对象置为 null 或者使用 System.gc() 方法手动释放内存。设置为 null 不是什么大问题,但是调用 System.gc() 方法会剧烈的影响系统的性能,所以不应该使用。(幸好,我还没有看到 NHN 的开发者有使用这个方法。)大多数对象很快变成不可达。只存在少量从老的对象到新对象的引用
知道 Java 的垃圾回收(GC)怎么工作有什么好处?作为一个软件工程师,满足智力上的好奇心可能是一个理由,但是同时理解 GC 怎么工作可以帮助你写出更好的 Java 应用。
这是我自己非常个人、主观的看法,但是我相信一个精通 GC 的人很可能是一个更好的 Java 开发者。如果你对 GC 的过程感兴趣,那说明你已经有了开发一定规模应用的经验。如果你曾仔细思考选择正确的 GC 算法,说明你已经完全理解了你所开发的应用的功能。当然,这可能不是评判一个优秀开发者的通用标准。然而,当我说要想成为一名出色的 Java 开发者必须理解 GC 时,我想很少会有人反对。
这是我“成为 Java GC 专家” 系列文章的第一篇。本文将介绍 GC,在下一篇文章中,我将讨论分析 GC 的状态以及 NHN 的 GC 调节示例。
在了解 GC 之前你需要知道一个术语。这个术语就是“全局暂停事件”(stop-the-world)。不管你选择什么 GC 算法,全局暂停事件都会发生。全局暂停事件意味着JVM将停止当前应用的运行来执行 GC。当全局暂停事件发生时,除了 GC 所需要的线程外,所有的线程都会停止执行任务。被中断的任务只有当 GC 任务完成以后才会恢复。GC 调节通常意味着减少全局暂停事件的次数。
垃圾回收的来源Java 不会在代码中手动指定一块内存再释放它。有的开发者会将相关对象置为 null 或者使用 System.gc() 方法手动释放内存。设置为 null 不是什么大问题,但是调用 System.gc() 方法会剧烈的影响系统的性能,所以不应该使用。(幸好,我还没有看到 NHN 的开发者有使用这个方法。)
大多数对象很快变成不可达。
只存在少量从老的对象到新对象的引用
这些假设称为“弱分代假设”(weak generational hypothesis),为了强化这一假设,HotSpot 虚拟机在物理上分为两个部分-新生代(young generation) 和 老年代(old generation)。
新生代:大多数新创建的对象都存放在这里。因为大多数对象很快就会变得不可达,很多对象都在新生代创建,然后就消失。当一个对象从这个区域消失的时候,我们就说发生了一次“小的 GC”(minor GC)。
老年代:那些在新生代存活下来,并没有变成不可达的对象被复制到这里。它通常要比新生代大。由于容量更大,GC 发生的次数就没有新生代频繁。当对象从老年代消失时,我们就说发生了一次“大 GC”(major GC)(或者是 "全 GC"(full GC))。
我们一起来看一下这幅图:
图1:GC 区域和 数据流程
上图中的持久代(permanent generation)通常也称为“方法区(method area)”,它用于存储类或者字符常量。所以这个区域不是用于永久存储从老年代存活下来的对象。这个区域也可能会发生 GC。这个区域发生的 GC 也算作大 GC。
有人可能会想:
如果一个处于老年代的对象需要引用一个处于新生代的对象会怎么样?
为了解决这个问题,在老年代有一个称为"card table"的东西,是一个512字节大小的块。当老年代中的对象要引用一个新生代的对象时,它就会被记录在这个 table 中。当新生代执行 GC 的时候,只需要搜索这个 table 来确定它是否属于需要 GC 的对象,而不用检查老年代所有引用的对象。card table 通过 write barrier 管理。write barrier 给小 GC 性能上带来极大的提升。尽管会有一点额外的开销,但是 GC 的总体时间减少了。
图2:Card Table 的结构
新生代的组成
为了理解 GC, 我们先了解一下新生代,也就是对象第一次被创建的地方。新生代被分成3个区域。
-
一个 Eden 区
-
两个 存活(Survivor) 区
总共3个区域,其中两个是存活区。每一个区域的执行顺序是这样的:
-
1、大部分新创建的对象都处于 Eden 区
-
2、在 Eden 区域执行第一次 GC 以后,存活下来的对象被移动到其中一个存活区。
-
3、在 Eden 区域再次执行 GC 以后,存活下来的对象继续堆积已经有对象的那个存活区。
-
4、一旦一个存活区被存满,存活对象就会被移动到另一个存活区。然后被存满的那一个存活区数据就会被清掉(修改为无数据状态)。
-
5、如此反复一定次数之后,还处于存活状态的对象被移动到老年区。
如果你仔细检查这些步骤,存活区域总是有一个是空的。如果两个存活区域同时都有数据,或者同时都为空,这意味着你的系统存在问题。
通过小 GC 将数据堆积到老年代的过程可以参考下图:
图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 区域创建,然后长期存活的对象通过存活区移动到老年代。
老年代的 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。
Serial GC(-XX: UseSerialGC)
上一段中我们介绍的新生代的 GC 使用的是这种类型。老年代的 GC 使用叫做 "标记-清除-压缩(mark-sweep-compact)"的算法。
-
1、这个算法的第一步是标记老年代中的存活对象
-
2、然后、从头开始检查堆,将存活的对象放到后面(交换)
-
3、最后一步,用存活对象从头开始填充堆,这样这些存活对象连续堆放,并且将对分为两部分:一部分有对象另一部分没有对象(压缩)
Serial GC 适合小型内存和有少量CPU 内核的环境。
Parallel GC(-XX: UseParallelGC)
图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)
JDK 5 以后开始支持 Parallel Old GC。与并行 GC 相比,唯一的区别是这个 GC 算法是为老年代设计的。它的执行一共有三个步骤:标记-汇总-压缩。汇总这一步为 GC 已经执行过的区域单独标记存活的对象,这一步和 标记-交换-压缩 算法中的交换步骤是不一样的。这需要通过更复杂的步骤来完成。
CMS GC(-XX:UseConcMarkSweepGC)
图5:串行 GC 和 CMS GC
如你所见,CMS GC 比我们前面所介绍的任何 GC 都要复杂的多。刚开始的 初始标记步骤很简单。离类加载器最近的对象中的存活对象被搜索出来。所以,暂停时间很短。在并发标记步骤中,刚才已经确认的存活对象所引用的对象被跟踪并检查。这一步的差别在于它在处理的同时其他线程同时也在处理。在重新标记阶段,新添加的对象或者在并发标记阶段被停止引用的对象会被检查。最后,并发清除阶段,垃圾回收过程被执行。垃圾回收在其他线程还在进行的时候就执行。因为这一类型的 GC 是以这样的方式执行,GC 的暂停时间很短。CMS GC也被称作低延时 GC,所以当响应时间对所有的应用都很关键的时候使用这种 GC。
CMS GC 拥有较短的全局暂停时间这一优点,同时也有以下缺点。-
它比其他类型的 GC 使用更多的内存和 CPU
-
默认没有提供压缩算法。
在使用这种 GC 之前需要认真检查。同时,如果多个内存碎片需要压缩,全局暂停时间的时间会比任何其他类型的 GC 都要长。所以你需要确认压缩任务执行的频率和时间。
G1 GC
最后,我们一起来看一下垃圾优先(G1)GC。
图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出现崩溃。所以请继续等待直到它更稳定。