JVM的Garbage Collection(GC)

垃圾回收把程序员从繁重的内存管理的任务中解放出来,提高生产效率。我以前做C++的时候就对Java的这个特性非常感兴趣,今天结合书的参考资料,深入地研究一下GC中的原理和工作机制。(以Hotspot JVM为例)

关于对象的“生死”

当我们在Heap上分配了内存给对象实例后,JVM怎么判断某个对象已经无用可以进行回收了呢?答案是:JVM会采用根搜索算法找出所用不可达的对象,对它们进行标记。(JVM没有使用C++里智能指针常用的引用计数法。)

标记一般会做两次,第一次标记是找出所有和GC Roots不相连的对象,然后判断些对象是否有必要执行finalize()(如果对象没有覆盖finalize()或者finalize()已经被JVM调用过,则不会执行。finalize()只会被执行一次。)。如果判定为有必要执行finalize(),那对象会被放置在一个名为F-Queue的队列中。在稍后由一条JVM自动建立的、优先级很低的Finalizer线程中执行(这就是为什么常常看到finalize()执行和gc()不在一起)。原因是为了避免finalize()方法中有执行缓慢的操作。然后JVM会对F-Queue进行一次小规模的第二次标记。如果对象不能在finalize()中逃出升天,那就是真要的“死了”(被JVM回收掉)。

方法区上的回收

堆上对象生死清楚之后,看看方法区。方法区只要收集两部分内存:无用的常量和无用的类。无用常量的判断很容易和对象很像,无用的类麻烦了些。类要满足3个条件才能算是“无用的”。

  • 该类所有的实例都已经被回收。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的Class类没有在任何地方被引用 ,无法在任何地方通过反射访问该类的方法。
我们可以用一些JVM参数监控类的回收与加载(-Xnoclassgc, -verbose:class, -XX:+TraceClassLoading, -XX:+TraceClassUnLoading)等等。

垃圾回收的算法

知道了回收的对象,也就确定了垃圾的内存,下来看几种具体回收的算法。

1. 标记-清除算法(Mark-Sweep)

此算法就是先对垃圾内存进行标记,然后再清除掉。这个算法有两个缺点,一个是效率,一个是清除之后会生产很多内存碎片。

2. 复制算法(Copying)

为了解决效率问题,复制算法出现了。它将内存分为大小相等的两块,每次只使得其中一块。当这块内存用完了就将活着的对象复制到另一块上。然后再把已使用过的内存一次清理掉。这样就不用考虑内存碎片的问题了。

能选用这个算法有一个统计的前提。IBM的研究表明,新生代的对象98%是朝生夕死(但这不是说新生代的对象也就是说可以准备两块内存,但一块大一块小,比如10:1或8:1。平时使用大的,一段时间后,大内存块中大部分内存死去,把活的复制到小内存块上,实现了高效的回收。那块小内存就像是一块分配担保的空间。

3. 标记-整理算法(Mark-Compact)

复制算法有一个问题就是会浪费内存,且当如果接近100%的内存(如老年代)都存活时,复制算法就无法发挥它的优势。 于是在Mark-Sweep的之上,提出了另一种算法。在标记之后,让所有活的对象都向内存块的一端移动,再清理掉边界的内存。这样就不会有碎片了。

4. 分代收集算法(Generational Collection)

所谓“代”就是根据对象存活的周期划分出来的。Java堆分为新生代和老年代(听说C#分4代)。根据不同代的特点,采用不同的算法进行GC就是分代收集了。

  • 对于新生代,每次会有大批对象死去,少量存活,就选用复制算法。
  • 对于老年代,对象存活概率大,没有额外空间做担保(也没有必要),必须使用“标记-清理”或“标记-整理”算法进行回收。

垃圾回收器(Garbage Collector)

HotSpot中实做了很多不同的Garbage Collector,作用在不同的“代”上。

新生代上的垃圾回收器

1. Serial收集器

据说是最基本、历史最悠久的收集器,它是一个单线程的收集器,更可怕的是,它在执行时,会终止掉所有的用户线程,又叫“Stop the world!“。虽然每次世界只停顿短短的几ms.

回收算法采用的是复制算法

2. ParNew收集器

它是Serial收集器的多线程版本(在新生代上),其余的行为、规则和策略都与Serial完全相同。

3. Parallel Scavenge收集器

它和ParNew一样,使用复制算法的并行多纯种收集器。但它有一个特别之处。它的目标是达到一个可控制的吞吐量(Throughput)。吞吐量就是CPU用于用户代码的时间与CPU总消耗时间的比值。可见高吞吐量可以最高效的利用CPU时间,尽快的完成程序的运算任务。Parallel Scavenge收集器有两个参数用于精确控制吞吐量,分别是-XX:MaxGCPauseMillis和-XX:GCTimeRatio.

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内在回收花费的时间不超过设定值。GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。

GCTimeRatio参数的值是一个大于0小于100的参数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。

4. CMS (Concurrent Mark Sweep) 收集器

它的目标是获取最短回收停顿时间。它是一使用的Mark-Sweep算法,分为4个步骤,包括:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

 

它的优点就是并发收集、低停顿。但它也存在一些缺点:

  • CMS对CPU资源非常敏感。
  • CMS收集器无法处理浮动垃圾。
  • 使用Mark-Sweep算法,效率低。当碎片过多时,就会触发Full-GC。

总结

本文是对JVM垃圾回收的一个概括性的总结。

发表评论

电子邮件地址不会被公开。 必填项已用*标注