0%

Java垃圾回收器

自动内存管理的优缺点

优点

在C/C++等语言中,内存管理是程序员的责任,程序员需要在内存的分配和释放上花费大量的时间,手动内存管理过程中容易出现以下问题:

  1. 悬垂指针问题(Dangling references,tries to access the original object, but the space has been reallocated to a new object, the result is unpredictable and not what was intended)
  2. 内存泄露问题(Memory/Space Leaks, memory is allocated and no longer referenced but not released)
    包括Java在内的现代面向对象语言中都提供了自动内存管理机制,垃圾回收器保证正在使用的对象不会被回收从而解决悬垂指针问题;通过自动释放不再使用对象占用的内存空间来解决内存泄露问题。内存管理机制有助于程序员将精力集中在业务实现上并且可以写出更加可靠的代码。

缺点

自动内存管理机制的缺点在于,垃圾回收器本身需要CPU时间和其它资源,从一定程度上降低了性能。

垃圾回收器的职责

在JVM运行时数据区中,线程私有的虚拟机栈,本地方法栈和程序计数器随着线程的启动而创建,线程结束后资源就被回收了,所以一般不需要对线程私有的数据进行垃圾回收。JVM主要对各线程共享的方法区和堆进行垃圾回收,方法区(也叫永久代)中的垃圾主要包括废弃的常量和无用的类,由于永久代垃圾回收的效率较低,一般在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader是场景下使用,垃圾回收器的工作主要包括:

  1. 分配内存
  2. 保证所有被引用的对象保存在内存中
  3. 回收从正在执行的代码中引用的对象出发不可达的对象占用的空间

垃圾标记算法

引用计数法(Reference Counting)

给对象中添加一个引用计数器,当这个对象被引用时,引用计数加一;不再引用它时,计数器减一。当对象的计数器值为0时,该对象不再被其它对象引用。
缺点:难以解决对象的循环引用问题

可达性分析法

通过一系列正在被使用的对象“GC root”作为起点,从这些节点开始搜索,如果一个对象没有被搜索到,则证明此对象不再被使用。可作为GC Root是的对象包括:

  • 虚拟机栈中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中JNI(Native方法引用的对象)

    JDK1.2之后引用分为四种:

  • 强引用(Strong Reference):显式创建的对象,类似`”Object obj = new Object()”``

  • 软引用(Soft Reference):软引用用来描述一些还有用但并非必需的对象。软引用指向的对象在系统将要发生内存溢出异常之前进行回收,如果回收后还是没有足够的内存,才会抛出内存溢出异常。(由SoftReference类实现)

  • 弱引用(Weak Reference):弱引用指向的对象只能生存到下一次回收之前。(由WeakReference类实现)

  • 虚引用(Phantom Reference):一个对象的虚引用不会影响其生存周期,也无法通过虚引用来取得对象的实例。为对象设置虚引用的唯一目的是能在这个对象被收集时收到一个系统通知。

ps:即使可达性分析中不可达的对象,回收之前也至少经过两次标记过程。

垃圾收集算法

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

该算法分为标记和清除两个阶段:

  • 标记,标记出所有需要回收的对象
  • 回收,标记完成之后同一回收所有被标记的对象

缺点:

  1. 效率问题, 标记和清除两个过程效率都不高
  2. 空间问题,标记清除后会留下大量不连续的内存碎片,碎片过多时可能导致以后需要分配较大对象时,无法找到足够的连续内存而不得不执行一次垃圾回收

复制(Copy)算法

将可用内存划分为大小相等的两块,每次只使用其中的一块,当其中一块用完了,就将其中还存活的对象复制到另一块内存上,然后将使用过的这块清空。

现代商业虚拟机都采用这种算法来回收新生代,由于新生代中98%的对象的生命周期都很短,新生代回收并不需要1:1的比例划分空间。Hotspot将新生代内存划分为一个Eden区和两个Survivor区,Eden区与Survivor区的大小比例为8:1. 回收时,将Eden区和一个Survivor区中存活的对象复制到另一个Survivor区,然后清理掉刚才用过的Eden和Surviror区。

复制算法不用考虑内存碎片问题,简单高效;缺点是划分比例低于1:1时需要额外的空间进行分配担保,分配比例等于1:1时,每次内存只能使用一半。

标记-整理算法

复制算法适用于对象存活率低的场景,当对象存活率较高时,需要复制大量的对象,复制算法的效率会降低。老年代中对象的存活率较高,一般不能直接使用复制算法。

标记-整理算法可用于回收老年代对象,与标记-清除算法类似,标记-整理算法不是直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清理掉边界以外的内存。

分代收集(Generational Collection)算法

商业虚拟机都采用分代收集算法,其核心思想是:根据对象存活周期的不同将内存划分为几块,一般将Java堆分为新生代和老年代,这样就可以根据各年代的特点采用最适用的收集算法。在新生代中,每次垃圾回收都有大量的对象死去,只有少量对象存活。选用复制算法可以只复制较少的存活对象完成垃圾收集。而老年代中对象存活率高/没有额外的空间进行分配担保,必须使用”标记-清理“或者”标记-整理“算法进行收集。

HotSpot垃圾回收实现

直接使用枚举根节点算法会导致时间消耗过多(扫描方法区和虚拟机栈很耗时)和GC停顿问题(Stop The World,枚举根节点期间,为了保持一致性,整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中引用关系还在变化的情况,改点不满足的话分析结果的准确性就无法得到保障)。

OopMap

在Hotspot垃圾标记的实现中,使用了OopMap数据结构,在类加载时就把对象内各偏移位置的数据结构记录下来,JIT编译过程中也会在特定位置记录栈和寄存器中哪些位置是引用。

安全点

在OopMap协助下,HotSpot可以快速准确地完成GC Roots枚举,由于可以引起引用关系变化的情况很多,为每一条指令生成OopMap需要大量的空间。所以HotSpot只会在特定的位置(安全点,Safepoint)记录这些信息,在安全点枚举根节点是安全的。当GC触发一次垃圾回收,它只是简单的设置一个标志; 当前执行函数会轮询这个标志, 当发现这个标志置位的时候,他们就会阻塞。那些轮询的点都是安全点。 多数情况下, JIT负责在合适的位置插入轮询程序。 安全点不能太少,否则GC一次需要等待太长时间,也不能过于频繁以致于过分增大运行时的负荷。
最好的结果是刚好有足够的轮询点满足需求:

  1. 一类强制的轮询点是内存分配点。分配可以触发一次垃圾收集,因此分配必须是安全点
  2. 长时间的执行总是和方法调用或者循环有关。因此,调用点和循环回边点也是期望的轮询点  
    这些就是Harmony中的轮询点: 分配点,调用点和循环回边点,多数情况下运行时负载小于1%。

安全区域

由于有一些情况不能及时响应GC触发事件,例如Sleep状态或者Blocked状态,在这些状态中引用关系不会发生变化,所以线程执行到这些区域时,将设置Ready flag表示可以进行GC。当线程离开安全区域时会检查GC是否完成,如果还未完成将会像安全点一样阻塞。

垃圾回收器的选择

一个好的垃圾回收器必须保证所有存活的对象都不能被回收,垃圾必须在几个垃圾回收周期内被回收;好的垃圾回收器必须高效,在程序运行期间不能引入长时间暂停。同时,垃圾回收器还负责整理碎片以保证大的对象能够读入。实际的垃圾回收器在时间,空间和回收频率之间取得平衡。

Serial收集器

Serial收集器是Hotspot虚拟机Client模式下新生代的默认收集器,它的特点是简单高效,在单个CPU环境下,Serial由于没有线程交互的开销,可以获得很高的单线程收集效率。停顿时间可以控制在几十至一百毫秒左右。

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,是Server模式下虚拟机中的首选新生代收集器。

Parallel Scavenge(吞吐量优先的收集器)

Parallel Scavenge也是一个使用复制算法的新生代收集器,它是一个并行的多线程收集器,它的目标是达到一个可控制的吞吐量(ThroughPut, 即CPU用于运行用户代码的时间与CPU消耗总时间的比值)。Parallel Scavenge收集器有一个-XX:+UseAdaptiveSizePolicy参数,打开这个参数后虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整新生代大小(-Xmm)、Eden和Survivor区比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等参数。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,在注重吞吐量以及CPU资源敏感的场合,可以考虑Parallel Scavenge加Parallel Old收集器。

CMS(Concurrent Mark Sweep)收集器

CMS收集器是一种以获取最短回收停顿时间为目标的并发收集器,适用于响应速度重要的场合。CMS基于标记-清除算法实现,整个收集过程包括4步:

  1. 初始标记(CMS inital mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

初始标记和重新标记需要(Stop the world, STW),初始标记只是标记一下GC roots能直接关联到的对象,速度较快;并发标记阶段就是进行GC Roots Tracing的过程;重新标记是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般比初始标记阶段稍长,但远比并发标记时间时间短。

优点:并发收集、低停顿

缺点:产生大量内存碎片,并发阶段降低吞吐量

promotion failed

在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的。

解决方法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
也就是CMS在进行5次Full GC(标记清除)之后进行一次标记整理算法,从而可以控制老年代的碎片在一定的数量以内,甚至可以配置CMS在每次Full GC的时候都进行内存的整理。

concurrent mode failure

在执行CMS的过程中有业务对象需要在老年代直接分配,例如大对象,但是老年代没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。

解决方法:调低触发CMS GC执行的阀值,CMS GC触发主要由CMSInitiatingOccupancyFraction值决定,默认情况是当旧生代已用空间为68%时,即触发CMS GC,在出现concurrent mode failure的情况下,可考虑调小这个值,提前CMS GC的触发,以保证旧生代有足够的空间。

G1收集器

G1之前的收集器收集的范围都是整个新生代或者整个老年代,而 G1 内存布局不同,它将 Java 堆划分为大小相等的 Region,虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离。通过引入 Region 的概念,将原来的一整块内存空间划分成多个小空间,使得每个小空间可以单独进行垃圾回收。这种划分带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先队列,每次根据允许的收集时间,优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作可划分为以下几个步骤:

  1. 初始标记
  2. 并发标记
  3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set中。这个阶段需要停顿线程,但可并行执行。
  4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段也可以做到与用户程序一起并发执行,但因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

与 CMS 相比,G1 有以下特点:

  1. 空间整合:G1 收集器使用标记整理算法,不会产生空间碎片
  2. 可预测停顿:G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者在长度为 N 毫秒的时间片段内,消耗在垃圾回收的时间不超过 S 毫秒

参考文献

  1. CMS之promotion failed&concurrent mode failure
  2. CMS收集器几个参数详解 -XX:CMSInitiatingOccupancyFraction, CMSFullGCsBeforeCompaction
  3. JAVA堆外内存的简介和使用
  4. 关于 JVM 堆外内存的一切
  5. Netty中的零拷贝

http://www.cnblogs.com/ridox/p/3646381.html
周志明《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版