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

原文连接:http://www.cubrid.org/blog/de...java

了解Java的垃圾回收(GC)原理能给咱们带来什么好处?对于软件工程师来讲,知足技术好奇心可算是一个,但重要的是理解GC能帮忙咱们更好的编写Java应用程序。程序员

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

本文是成为Java GC专家系列的第一篇。我将先对GC作一下基本的概述,在下一篇文章中,我将讲述如何分析GC状态以及经过[NHN]()的案例介绍GC调优相关的内容。segmentfault

本文的目的是以通俗的方式为你介绍GC概念。我但愿本文会对你有所帮助。事实上,个人同事们已经发表了一些在Twitter上很是受关注的优秀文章,你一样也能够拿来参考。安全

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

基于的分代理论的垃圾回收

在Java程序里不须要显式的分配和释放内存。有些人经过给对象赋值为null或调用System.gc()以指望显式的释放内存空间。给对象设置null虽没什么用,但问题不会太大;若是调用了System.gc()却可能会为系统性能带来严重的波动,即使调用System.gc()系统也未必当即响应去执行垃圾回收。(所幸的是,在NHN不曾看到有工程师这么作。)多线程

在使用Java时,程序员不须要在程序代码中显式的释放内存空间,垃圾回收器会帮你找到再也不须要的(垃圾)对象并把他们移出。垃圾回收器的建立基于如下两个假设(也许称之为推论或前提更合适):并发

  • 大多数对象的很快就会变得不可达布局

  • 只有极少数状况会出现旧对象持有新对象的引用性能

这两条假设被称为"弱分代假设"。为了证实此假设,在HotSpot VM中物理内存空间被划分为两部分:新生代(young generate)老年代(old generation)

新生代:大部分的新建立对象分配在新生代。由于大部分对象很快就会变得不可达,因此它们被分配在新生代,而后消失再也不。当对象重新生代移除时,咱们称之为"minor GC"。

老年代:存活在新生代中但未变为不可达的对象会被复制到老年代。通常来讲老年代的内存空间比新生代大,因此在老年代GC发生的频率较新生代低一些。当对象从老年代被移除时,咱们称之为"major GC"(或者full GC)。

看一下下图的示意:

GC区域和数据流向
图1:GC区域和数据流向

图中的permanent generation称为方法区,其中存储着类和接口的元信息以及interned的字符串信息。因此这一区域并非为老年代中存活下来的对象所定义的持久区。方法区中也会发生GC,这里的GC一样也被称为major GC

有些人可能认为:

若是老年代的对象须要持有新生代对象的引用怎么办?

为了处理这种场景,在老年代中设计了"索引表(card table)",是一个512字节的数据块。无论什么时候老年代须要持有新生代对象的引用时,都会记录到此表中。当新生代中须要执行GC时,经过搜索此表决定新生代的对象是否为GC的目标对象,从而下降遍历全部老年代对象进行检查的代价。该索引表使用写栅栏(write barrier)进行管理。wite barrier是一个容许高性能执行minor GC的设备。尽管它会引入必定的开销,却能带来整体GC时间的大幅下降。

索引表结构
图2:索引表结构

新生代的结构

为了深刻理解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把数据迁移到老年代的过程:

GC先后
图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区域上的数次转移而存活下来的长寿对象最后会被移到老年代。

老年代垃圾回收

当老年代数据满时,便会执行老年代垃圾回收。根据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的特性。

Serial GC(-XX:+UseSerialGC)

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

  1. 首先该算法须要在老年代中标记出存活着的对象

  2. 而后从前到后检查堆空间中存活的对象,并保持位置不变(把再也不存活的对象清理出堆空间,称为空间清理)

  3. 最后,把存活的对象移到堆空间的前面部分以保持已使用的堆空间的连续性,从而把堆空间分为两部分:有对象的和无对象的(称为空间压缩)

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

Parallel GC(-XX:+UseParallelGC)

Serial GC与Parallel GC的区别
图4:Serial GC与Parallel GC的区别

图中能够容易的看出serial GC与parallel GC的区别。Serial GC使用单一线程执行GC,而parallel GC则使用多个线程并发执行,所以parallel GC 较serial GC具备更快的速度。Parallel GC适用于多核CPU且使用了较大内存空间的场景。Parallel 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步骤有所区别,并须要一些复杂步骤才能完成。

CMS GC(-XX:+UseConcMarkSweepGC)

Serial GC与CMS GC
图5:Serial GC与CMS GC

从图上可看出并发标记-清理(Concurrent Mark-Sweep) GC比之后上其余GC都要复杂。开始时的初始标记(initial mark)比较简单,只有靠近类加载器的存活对象会被标记,所以停顿时间(stop-the-world)比较短暂。在并发标记(concurrent mark)阶段,由刚被确认和标记过的存活对象所关联的对象将被会跟踪和检测存活状态。此步骤的不一样之处在于有多个线程并行处理此过程。在重标记(remark)阶段,由并发标记所关联的新增或停止的对象瘵被会检测。在最后的并发清理(concurrent sweep)阶段,垃圾回收过程被真正执行。在垃圾回收执行过程当中,其余线程依然在执行。得益于CMS GC的执行方式,在GC期间系统中断时间很是短暂。CMS GC也被称为低延迟GC,适用于全部应用对响应时间要求比较严格的场景

CMS GC虽然具备中断时间断的优点,其缺点也比较明显:

  • 与其余GC相比,CMS GC要求更多的内存空间和CPU资源

  • CMS GC默认不提供内存压缩

使用CMS GC以前须要对系统作全面的分析。另外为了不过多的内存碎片而须要执行压缩任务时,CMS GC会比任何其余GC带来更多的stop-the-world时间,因此你须要分析和判断压缩任务执行的频率及其耗时状况。

G1 GC

最后咱们学习有关G1垃圾回收的介绍。

G1 GC的布局
图6:G1 GC的布局

若是你想清晰的理解GC,请先忘记上面介绍的有关新生代和老年代的知识。如上图所示,每一个对象在建立时会分析到一个格子中,后续的GC也是在格子中完成的。每当一个区域分配满对象后,新建立的对象就会分配到另一个区域,并开始执行GC。在这种GC中不会出现其余GC中的对象在新生代和老生代三区域中移动的现象。G1是为了取代在长期使用中暴露出大量问题且饱受抱怨的CMS GC。

G1最大的改进在于其性能表现,它比以上任何一种GC都更快速。它在JDK6中以早期版本的形式释放出来以用于测试,它真正的发布是在JDK7中。我我的认为在NHN真正在生产环境使用JDK7至少还须要1年的测试时间,因此还须要等待一段时间。而且我据说在JDK6中使用G1偶尔会出现JVM崩溃现象。因此稳定版尚需时日。

接下来的文章中会讲解GC调优,但我想先提一个问题。若是应用中全部对象的类型和大小都是同样的,WAS上使用的GC能够设置相同的GC选项。若是在WAS上建立的对象的大小和生命周期各不相同的对象,配置的GC选项也各不相同。换名话说,不能由于一个服务使用了GC选项"A",其余的不一样服务使用相同的选项"A"也能获取最好的表现。因此为了找到WAS线程的最佳值,每一个WAS实例须要经过持续的调优和监控以便找到最优的配置和GC优项。这不仅是来自个人我的经验,而是来自于JavaOne 2010上工程师们对于Oracle JVM讨论后的一致见解。

本节咱们只简单介绍Java中的GC基础。下一章节,我将会讨论关于如何监控GC状态以及如何作性能调优。


本文参考了2011年12月出版的《Java 性能》和Oracle网站上提供的白皮书《Java HotspotTM 虚拟机内存管理》。

做者:Sangmin Lee, 性能实验室高级工程师,NHN公司

相关文章
相关标签/搜索