首页 > 代码库 > Java GC(2)

Java GC(2)

前面介绍了新生代和老年代,接下来我们分析新生代的具体构成。

新生代是用来保存那些新创建的对象的,它可以分为三个分布:一个伊甸园空间(Eden)两个幸存者空间(Survivor)

每个空间的执行顺序如下:

  1. 绝大多数刚刚被创建的对象会存放在伊甸园空间。
  2. 在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。
  3. 此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。
  4. 当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。
  5. 在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。

上面的过程就是通过频繁的minor GC把数据移动到老年代的过程。同一时刻,两个survivor只被使用一个,另外一个是用来进行复制GC时使用的。

在上述的过程中,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointerTLABs(Thread-Local Allocation Buffers)。

Bump-the-pointer技术跟踪在伊甸园空间创建的最后一个对象。这个对象会被放在伊甸园空间的顶部。如果之后再需要创建对象,只需要检查伊甸园空间是否有足够的剩余空间。如果有足够的空间,对象就会被创建在伊甸园空间,并且被放置在顶部。这样以来,每次创建新的对象时,只需要检查最后被创建的对象。这将极大地加快内存分配速度。但是,如果我们在多线程的情况下,事情将截然不同。如果想要以线程安全的方式以多线程在伊甸园空间存储对象,不可避免的需要加锁,而这将极大地的影响性能。

TLABs 是HotSpot虚拟机针对这一问题的解决方案。该方案为每一个线程在伊甸园空间分配一块独享的空间,这样每个线程只访问他们自己的TLAB空间,再与bump-the-pointer技术结合可以在不加锁的情况下分配内存。

这两种技术不用刻意去记住,只是有一个了解。需要记住的是,新创建的对象是被保存在伊甸园空间,那些长期存活的对象会经由幸存者空间转存到老年代空间。

接下来我们分析gc中的重头戏,老年代的gc处理机制。老年代空间的gc事件基本上是在空间已满时发生,执行的过程根据gc类型不同而不同,因此,我们需要了解各种gc类型。在jdk7中gc类型有5种:

  • Serial GC
  • Parallel GC
  • Parallel Old GC (Parallel Compacting GC)
  • Concurrent Mark and Sweep GC (CMS)
  • Garbage First(G1) GC 

其中第一种Serial GC不应该被用于服务器中,因为这是串行的gc方式(在单核cpu时代就出现的最早的gc类型),会频繁的发生Stop the world的情形,因此会严重影响服务器的性能。

下面我们详细分析每一种gc类型的过程:

1. Serial GC (-XX:+UseSerialGC)

新生代空间的gc方式我们在前面已经介绍过了,在老年代空间中的gc方式称之为mark-sweep-compact的算法。

  1. 算法的第一步是标记老年代中依然存活对象。(标记)
  2. 第二步,从头开始检查堆内存空间,并且只留下依然幸存的对象。(清理)
  3. 从头开始,顺序地填满堆内存空间,并且将对内存空间分成两部分:一个保存着对象,另一个空着。(压缩)

2. Parallel GC (-XX:+UseParallelGC)

Serial GC 与 Parallel GC的区别

从上图中,可以明显的看出Serial GC和Parallel GC的区别,Serial GC只使用一个线程执行gc,而Parallel GC使用多个线程,因此Parallel GC更高效。这种gc在内存充足以及多核的情况下会很有用,它也被称为throughput GC

3. Parallel Old GC(-XX:+UseParallelOldGC)

Parallel Old GC是在JDK5之后出现的,与Parallel GC相比,唯一的区别在于针对老年代的gc算法。Parallel Old GC分为三步:标记-汇总-压缩(mark – summary – compaction)。汇总(summary)步骤与清理(sweep)的不同之处在于,其将依然幸存的对象分发到gc预先处理好的不同区域,算法相对清理来说略微复杂一些。

可以使用参数 -XX:ParallelGCThreads=n 来指定并行的线程数。

4. CMS GC (-XX:+UseConcMarkSweepGC)

Serial GC 与 CMS GC的区别

就像上图中看到的那样, CMS GC比我之前解释的各种算法都要复杂很多。第一步初始化标记(initial mark) 比较简单。这一步骤只是查找那些距离类加载器最近的幸存对象。因此,停顿的时间非常短暂。在之后的并行标记( concurrent mark )步骤,所有被幸存对象引用的对象会被确认是否已经被追踪和校验。这一步的不同之处在于,在标记的过程中,其他的线程依然在执行。在重新标记(remark)步骤,会再次检查那些在并行标记步骤中增加或者删除的与幸存对象引用的对象。最后,在并行交换( concurrent sweep )步骤,转交垃圾回收过程处理。垃圾回收工作会在其他线程的执行过程中展开。一旦采取了这种gc类型,由gc导致的暂停时间会极其短暂。CMS GC也被称为低延迟GC。它经常被用在那些对于响应时间要求十分苛刻的应用之上。

当然,这种gc类型在拥有stop-the-world时间很短的优点的同时,也有如下缺点:

  •  它会比其他gc类型占用更多的内存和CPU
  •  默认情况下不支持压缩步骤

在使用CMS GC类型之前你需要慎重考虑。如果因为内存碎片过多而导致压缩任务不得不执行,那么stop-the-world的时间要比其他任何gc类型都长,因此使用CMS前你需要考虑压缩任务的发生频率以及执行时间。

5. G1 GC

最后,我们来学习垃圾回收优先(G1)GC类型。

 G1 GC的结构

如果想要理解G1,首先你要忘记你所学过的新生代和老年代的概念。正如你在上图所看到的,每个对象被分配到不同的格子,随后gc执行。当一个区域装满之后,对象被分配到另一个区域,并执行gc这中间不再有从新生代移动到老年代的三个步骤。这个类型是为了替代CMS GC而被创建的,因为CMS GC在长时间持续运作时会产生很多问题。

G1最大的好处是性能,他比我们在上面讨论过的任何一种gc都要快。但是在JDK 6中,他还只是一个早期试用版本。在JDK7之后才由官方正式发布。就我个人看来,NHN在将JDK 7正式投入商用之前需要很长的一段测试期(至少一年)。因此你可能需要再等一段时间。并且,我也听过几次使用了JDK 6中的G1而导致Java虚拟机宕机的事件。因此在正式的项目中还是不建议使用此gc方式,等它足够稳定再说吧。

 未完待续。。。

Java GC(2)