第3章 垃圾收集器与内存分配策略

3.1 概述

垃圾收集(Garbage Collection GC)的历史远比Java更久远

3.2 对象已死?

  • 引用计数法
    给对象添加一个计数器,每有一地方引用它,计数器加1,当引用失效时计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。
    然而Java没有使用这个方法来管理内存,因为它很难解决对象之间的相互循环引用的问题

  • 根搜索算法
    通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜搜,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。
    Java中组为GC Roots的对象包括:虚拟机栈(栈帧中的本地变量表)、方法区中的类静态属性引用的对象、方法区的常量引用对象、本地方法栈中JNI(Navtive方法)的引用的对象

  • 再谈引用
    JDK1.2之后,引用被分为四种:强引用(strong)、软引用(soft)、弱引用(weak)、虚引用(phantom),强度依次递减。
    强引用:代码之中普遍存在,类似“Object obj = new Object()”,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
    软引用:用来描述一些还有用但非必须的对象,系统在将要发生内存溢出之前,将会把这些对象列进回收范围之内并进行二次回收
    弱引用:同样描述非必须的对象,被弱引用关联的对象只能生存在下一次垃圾回收之前
    虚引用:又称幽灵引用,无法通过虚引用来取得一个对象实例,它的唯一目的就是在这个对象被收集器回收之前收到一个系统通知

  • 生存还是死亡?
    如果一个对象在进行根搜索后没有与GC Roots相连的引用链,它会被第一次标记并进行一次筛选,筛选的条件是此对象是否有执行finalize()方法的必要。当对象没有覆盖finalize()方法,或是finalize()方法已经被虚拟机调用过了,虚拟机将这两种情况都视为“没有必要执行”。任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的的finalize()方法不会被再次执行。

  • 回收方法区
    永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
    类要同时满足3个条件才能算是“无用的类”:1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2.加载该类的ClassLoader已经被回收。3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

3.3 垃圾收集算法

  • 3.3.1 标记-清除算法
    标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。有两个缺点:1.标记和清除过程的效率都不高;2.标记清除之后会产生大量不连续的内存碎片。
  • 3.3.2 复制算法
    将内存那容量划分为大小相等的两块,每次使用其中一块。当这一块内存用完了,就将还存活者的对象复制到另一块上面,再把刚刚使用过的那一块内存清除掉。这样的确定时内存利用率不高。
    现代商用虚拟机大都采用这种算法回收新生代。将内存划分为一块较大的Eden和两块较小的Survivor,每次使用Eden和其中一块survivor。当回收时,将Eden和survive中还存活的对象一次性的拷贝到另一快survive,再清除掉这两块内存。通常Eden:Survive = 8:2,所有新生代可用的内存为90%,当survive不够时,需要依赖其他内存(老年代)。
  • 3.3.3 标记-整理算法
    标记过程和标记-清除算法相同,但是清除过程不是直接清除,而是让所有存活的对象向一段移动,然后直接清除掉端边界以外的内存。
  • 3.3.4 分代收集算法
    当前主流算法,根据对象的存活周期不同将内存划分成几块,根据各个年代的提点采用最适当的收集算法。

3.4 垃圾收集器

以HotSpot虚拟机1.6版本为例:
《深入理解Java虚拟机》学习笔记第三章
存在连线的两个收集器可以搭配使用。

  • 3.4.1 Serial收集器
    新生代单线程收集器,存在Stop The world过程,就是在进行垃圾收集时,暂停所有线程直到收集结束。简单而高效。
    《深入理解Java虚拟机》学习笔记第三章
  • 3.4.2 ParNew收集器
    Serial收集器的多线程版本,新生代收集器,可以以与CMS收集器配合使用。
    《深入理解Java虚拟机》学习笔记第三章
  • 3.4.3 Parallel Scavenge收集器
    Parallel Scavenge收集器和ParNew类似,也是一个新生代收集器,使用复制算法,是并行多线程收集器。
    其他收集器的目的是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码的时间/(运行用户代码的时间+垃圾回收的时间))。停顿时间越短越是和需要与用户交互的程序,高吞吐量则可以高效率的利用CPU,适合在后台运算而不需要太多交互任务。
  • 3.4.4 Serial Old收集器
    Serial Old是Serial收集器的老年代版本,也是一个单线程收集器,使用标记-整理算法。可以与Parallel Scavenge收集器搭配使用,也可以作为CMS收集器后备预案。
  • 3.4.5 Parallel Old收集器
    Parallel Old是Parallel Scavenge收集器老年代版本,使用多线程和整理标记算法。在注重吞吐量及CPU资源敏感的场合,可以考虑Parallel Scavenge加Parallel Old收集器。
  • 3.4.6 CMS收集器
    CMS收集器是一种以获得最短停顿时间为目标的收集器,基于标记-清除算法,主要分为四个步骤:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)。
    初始标记、重新标记都需要经过stop the world这个过程。初始标记仅仅标记一下GC Roots能直接关联的对象,速度很快。并发标记就是进行GC Roots Tracing的过程。重新标记是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,比初始长,远短于并发。
    《深入理解Java虚拟机》学习笔记第三章
    CMS缺点:
    1.CMS收集器对于CPU资源非敏感,并发阶段因为占用CPU资源会导致应用程序变慢。
    2.CMS无法处理浮动垃圾,可能出现“concurrent mode failure”导致另一侧Full GC产生。所谓浮动垃圾就是在并发清理阶段产生的新的垃圾。要是CMS运行期间预留的内存无法满足程序的需要,就会出现“concurrent mode failure”,虚拟机将临时启用Serial Old收集器收集老年代的垃圾
    3.CMS基于标记-清除算法,会产生大量碎片空间。
  • 3.4.7 G1收集器
    相对于CMS收集器,G1收集器有两个显著的该井:1.基于标记-整理算法,不会产生碎片空间;2.可以精确的控制停顿,可以让用户明确指定一个长度为M毫秒的时间片内,消耗在垃圾收集上的时间不超过N毫秒。
    G1收集器可以实现基本不牺牲吞吐量的前提下完成低停顿的内存回收。之前的垃圾收集器的作用范围是整个新生代或老年代,而G1极力避免全区的垃圾收集。G1将整个Java堆(包括新生代和老年代)划分为多个大小固定的独立区域,并跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。

相关文章: