垃圾回收

131 阅读8分钟

垃圾回收GC在java中也是相对备受关注的一项技术。在不同的jdk版本,不同的程序运行过程,选用不同的垃圾回收机制和内存分配机制,将直接影响到程序的性能。
当排查内存溢出,内存泄漏时有效的垃圾回收GC可使系统达到更好的高并发。
垃圾回收GC需要完成的无非三件事:

  1. 哪些需要回收
  2. 怎么回收
  3. 用什么回收

对象存活法则(哪些需要回收)

引用计数算法(Reference Counting)

判定方法:对存活的对象添加引用计数器,当有引用时,计数器+1,当引用失效时计数器-1,当计数器为0时,对象不再使用。
优点:实现简单,判定效率高。
缺点:难以解决对象之间相互循环引用问题。例如:当A引用了B,B引用A后,A=null,B=null。理论上为null后,计数器应该0,但存在相互引用,导致计数器不为0,最终无法通知收集器进行回收。
其应用有微软的COM技术,FlashPlayer的ActionScript3,Python语言和游戏脚本领域引用的Squirrel。

可达性分析算法(Reachabilility Analysis)

基本思想:定义GC Roots对象作为起始点,从这类节点往下搜索所走过的路程引用链(Reference Chain)。若对象不再引用链上(即:和GC Roots无关联)则判定为该对象可回收。

GC Roots对象:

  1. 虚拟机栈(栈桢中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈JNI(Native)引用的对象

判断生死的finalize

在可达性分析算法中会有两次标记过程,当对象不可达(不和GC Roots相连接)便进行第一次标记,并判断该对象是否重写finalize方法。若重写finalize方法或已经被虚拟机调用了,且没有在finalize方法自我拯救(例如:关联其他可达对象或调用this)则视为该对象已经被抛弃,做了二次标记后,等待回收。但若自我拯救后则为GC Roots可达。
其中需要注意的是finalize的自我拯救机会只有一次。一个对象的finalize()方法最多只能被系统自动调用一次。

垃圾收集算法(怎么回收)

当对象不可达后,不同的平台的虚拟机会采用不同的算法来收拾不可达对象。

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

该算法如同他的名字一样简单粗暴。先对需要回收的对象进行标记。标记后便进行回收。
不足

  1. 效率不高。标记和清除过程的效率并不高。
  2. 内存空间连续性不高。由于清除了大量不连续的碎片,内存空间变得不连续,若在程序运行时,需要进行分配大对象时,需要找到足够的内存空间,从而需要再进行一次垃圾回收。

复制算法(copying)

将内存按容量划分成大小相同的两块,当其中一块内存用完后,就将还存活着的对象复制到另一块上面,然后将已使用的内存空间全部清理。
优点:

  1. 不需要考虑碎片问题
  2. 只要移动堆指针,按顺序分配内存,实现简单,效率高效。

不足:

  1. 有一半的内存闲置,过于浪费。

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

“标记-整理”算法中的标记和”标记-清除”算法中的标记方法一样,但标记后并不直接对可回收对象进行清理,而是让所有存活的对象移向一端,然后清理掉需要回收的内存。
这样可以很好的让内存空间达到连续。

分代收集算法(Generational Collection)

将对象存活周期划分成不同的几块。一般分为老年代和新生代。根据各个年代采用适当的收集算法。
新生代:每次会回收大量对象,只有少量存活,因此采用复制算法。只需复制少量的存活对象。
老年代:存活率高,没有额外空间进行分配担保,采用“标记-整理”或“标记-清除”算法。

垃圾收集器(用什么回收)

在不同的厂家、不同版本的虚拟机所提供的垃圾收集器不一样。用户可根据应用特点和要求通过参数组合出适当的垃圾收集器。
值得说明的是,目前所提供的垃圾回收器中并非没有一款是可以适用于任何应用场景。更多的需要通过参数组合来得到合适的收集器。
使用组合:

年轻代(young generation):Serial、ParNew、Parallel Scavenge
老年代(Tenured generation):CMS、Serial Old(MSC)、Parallel Old
介于老年代和年轻代之间:G1

新生代

Serial收集器(串行收集器)

Serial收集器为单线程收集器,在年轻代中采用“复制算法”进行回收,老年代中采用“标记-整理算法”方式进行回收。由于是单线程,在回收过程中会暂停当前应用的其他线程工作。对于用户的体验并非友好。
但在于单CPU环境的Client模式的虚拟机来说,只要停顿时间控制在短时间内,回收次数不频繁的话,Serial收集器是个不错的选择。

ParNew收集器

ParNew为serial收集器的多线程版本,在参数控制、收集算法、对象分配规则、回收策略基本和Serial收集器一样。
在单CPU情况下ParNew收集器回收效果不如Serial收集器,甚至还因存在线程的切换的开销使得ParNew收集器效果比Serial收集器效率低下。
但随着CPU的增加ParNew的功效也随着提升。

Parallel Scavenge收集器

并行清理。相比其他收集器而言Parallel Scavenge收集器更关注吞吐量。当垃圾收集时用的时间占代码运行时时间+垃圾收集时间比例越小,则吞吐量越大。
吞吐量=代码运行时间/(代码运行时间+垃圾收集时间)
可通过参数-XX:MaxGCPauseMillis和-XX:GCTimeRatio参数进行控制。

老年代

Serial Old收集器

为Serial收集器在老年代版本的收集器,采用的是“标记-整理算法”。其特性和Serial差不多。

Parallel Old收集器

为Parallel Old收集器的老年代版本使用多线程和“标记-整理算法”。一般若新生代采用了Parallel Scavenge收集器则老年代一般会Serial Old或Parallel Scavenge进行收集。

CMS(Concurrent Mark Sweep)

以获取最短回收停顿时间为目标的收集器。主要应用在B/S系统上。系统停顿时间达到最短。
CMS共分为四个步骤:

  1. 初始标记(CMS initial mark):仅仅标志GC Roots能直接关联的对象,速度相对比较快
  2. 并发标记(CMS concurrent mark):为GC Roots Tracing过程。
  3. 重新标记(CMS remark):修正并发标记时因程序运行导致变动的标记记录,停顿时间相比初始标记长,并发标记短。
  4. 并发清除(CMS concurrent sweep):对标记的对象进行清理。

G1收集器

相比其他收集器,G1具有的特性如下

  • 并发与并行:充分利用多CPU、多核来缩短停顿时间。
  • 分代收集:不需要和其他收集器进行配合,可以独立完成新生代和老年代的收集。
  • 空间整合:基于“标记-整理算法”和“复制算法”之上,可更好的收集规整可用内存。分配大对象时不会因为联系内存空间不足而提前触发一次GC
  • 可预测停顿:可指定停顿时间片段

G1运行步骤

  1. 初始标记(Initial Marking):标记GC Roots能直接关联到的对象,时间相对比较短
  2. 并发标记(Concurrent Marking):从GC Root开始对堆进行可达性分析,找出存活对象,耗时相对比较长,但可以和程序并发进行
  3. 最终标记(Final Marking):修正并发标记期间,程序运行继续运行导致标记发生变化的标记记录,该过程采用并行方式,但程序不运行。
  4. 筛选回收(Live Data Counting and Evacuation):根据用户期望的GC停顿时间进行回收,可与程序并发交替执行。

内存分配与回收策略

  1. 对象优先在Eden区进行分配,若Eden区空间不足,则触发一次Minor GC。
  2. 大对象(需要大量连续空间的对象,例如:长字符串或字符)直接进入老年代。
  3. 长期存活对象进入老年代。对象在Eden区中经过一次Minor GC仍然存活则进入Survivor区,每次进行GC且没被GC掉则年龄增加1,当达到一定年龄(默认15可通过-XX:MaxTenuringThreshold进行设置)后则晋升为老年代
  4. 动态对象年龄判断。若Survivor空间相同年龄所有对象的总和大于Survivor空间的一半,则年龄大于或等于该对象的直接进入老年代。
  5. 空间分配担保。在Minor GC前检查老年代最大可用连续空间,若大于新生代所有对象总空间则条件成立,可进行Minor GC。若条件不成立则GC有风险。
    虚拟机查看HandlePromotionFailure设置是否担保,若运行,则继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,若大于则进行Minor GC。若小于或HandlePromotionFailure不担保则进行一次Full GC。