JVM垃圾收集机制

线程私有区域(程序计数器、虚拟机栈、本地方法栈)随线程创建而创建,随线程退出而销毁,栈中的栈帧随着方法的调用和退出有条不紊的进栈和出栈,每个栈帧分配多少内存基本上在类结构确定的时候就已知,因此,这些区域的内存分配和回收都具备确定性。因为方法结束或者线程结束时,对应的栈帧和栈内存会随之回收。

线程共享区域(堆、方法区)则不具备确定性,一个接口多个实现类需要的内存不同,一个方法的多个分支所需要的内存也不同,只有在运行时才能准确知道需要创建哪些对象,这部分内存的分配和回收都是动态的,GC所关注的是这部分内存。

判断对象存活的方法

  1. 引用计数算法(Reference Counting):给对象添加引用计数器,有地方引用,计数器加1,引用失效,计数器减1,计数器为0表示对象不存活。缺点:无法解决对象之间相互循环引用的问题

  2. 根搜索算法(GC Roots Tracing):Java和C#均采用此算法。通过GC Roots对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链(图论表示法,GC Roots到这个对象不可达)时,表示此对象不存活。GC Roots包括:

    • 虚拟机栈(栈帧中的本地变量表)中的引用的对象
    • 方法区中的类静态属性引用的对象
    • 方法区中的常量引用的对象
    • 本地方法栈中JNI(即Native方法)引用的对象

    对象发现没有和GC Roots相连接的引用链,会被第一次标记并进行筛选,如果此对象有必要执行finalize()方法(覆盖实现了finalize()方法且从未被系统调用过),会被放置在一个F-Queue的队列中,稍后由JVM自动建立的低优先级的Finalizer线程去执行。如果对象在finalize()方法中成功与GC Roots引用链建立关联(比如把this复制给某个类变量),则在稍后GC对F-Queue进行的第二次小规模标记中将其移除出“将要回收”的集合,否则该对象正式被回收。对于finalize()方法,即使多次调用,系统对任何对象的finalize()方法只会执行一次。应该尽量避免使用finalize()方法,因为运行代价高昂,不确定性大,无法保证各个对象的调用顺序。使用try-finally或者其他方式能做得更好、更及时。

对象引用的强度

  • 强引用(Strong Reference):new出来的直接引用,只要强引用存在,GC永远不会回收被引用的对象
  • 软引用(Soft Reference):还有些用,但非必需的对象。在系统将要发生内存溢出异常之前,会将这些对象列进回收范围并进行第二次GC,如果还是没有足够的内存,才会抛出内存溢出异常。
  • 弱引用(Weak Reference):比软引用更弱,被引用的对象只能生存到下一次GC发生之前,发生GC时,无论当前内存是否足够,都会被回收。
  • 虚引用(Phantom Reference):最弱的引用方式。虚引用的存在完全不会对其生存期构成影响,也无法通过虚引用取得一个对象实例。为对象设置虚引用唯一目的是希望能在对象被GC时收到系统通知。

回收方法区

方法区(永久代)主要回收两部分

  • 废弃常量:类似回收堆中的对象,对于常量池中的字面量,没有任何reference引用这个字面量,在回收时会被清出常量池,常量池中的类接口、方法、字段的符号引用与之类似
  • 无用的类:在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类卸载功能,以保证永久代不会溢出。但是,无用的类判定标准相对苛刻,需要同时满足以下条件:
    • 该类所有实例都已经被回收,也就是堆中不存在该类的任何实例
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法

1. 标记-清除(mark-sweep)算法

分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。缺点:一、标记和回收的效率都不高,二、标记清除之后会产生大量不连续的内存碎片。

2. 复制(copying)算法

将内存划分为大小相等的两块,每次使用其中一块,当着一块内存用完,将还存活的对象复制到另外一块上面,然后再把已经使用的那一块一次清空,不会出现碎片问题。缺点:一、可用内存仅为原来的一半,二、对于像老年代对象存活率较高的情况时,需要复制大量存活对象,效率较低。

3. 标记-整理(mark-compact)算法

首先标记所有需要回收对象,把所有存活对象向一端移动,按顺序“压缩”到一端,然后直接清理掉边界以外的内存,从而避免“标记-清除”算法的碎片问题以及“复制”算法的空间问题。缺点:整理步骤需要原址重新排列(压缩)存活对象,理论上效率低于复制算法(不冗余空间的整理操作要做swap,而冗余只需要做move) ,因此一般用于老年代的Major GC。

4. 分代收集(generational-collection)算法

根据对象存活周期的不同将内存划分为新生代、老年代、方法区(永久代),对各个区域采用最适当的收集算法。由于新生代98%的对象是朝生夕死的,只有少量存活,因此选用复制算法,而且并不需要按照1:1来划分,而是Eden:from:to=8:1:1,每次使用Eden和from,也就是每次可用内存空间为新生代空间的90%(80%+10%),如果回收时多于10%存活,则需要依赖其他内存(老年代)来进行分配担保(Handle Promotion);而老年代因为对象存活率高、没有额外空间进行分配担保,就必须用”标记-清除“或”标记-整理“算法来回收。

垃圾收集器

基于Sun HotSpot JVM的垃圾收集器如下,如果两个收集器之间存在连线,表示可以搭配使用。

1. Serial收集器(串行收集器)

用于新生代的单线程收集器,收集时需要暂停所有工作线程(STW,Stop the world)。优点在于:简单高效,单个CPU时没有线程交互的开销,堆较小时停顿时间不长,client模式首选新生代收集器。常与Serial Old 收集器搭配使用 。

2. ParNew收集器(Parallel New收集器,新生代并行收集器)

Serial收集器多线程版本,使用了多条线程进行垃圾收集,其余行为与Serial收集器一样,单CPU环境下不会比Serial收集器效果更好,由于存在线程交互的开销,只有随着CPU数量的增加,效率的优势才能发挥出来。server模式首选新生代收集器。常与Serial Old 收集器搭配使用 。

3. Parallel Scavenge收集器

与ParNew收集器一样是多线程收集器,其特点在于关注点与别的GC收集器不同:一般的GC收集器关注于缩短工作线程暂停的时间(STW),而该收集器关注于吞吐量,因此也被称为吞吐量优先收集器。高吞吐量与停顿时间短相比主要强调更高效率利用CPU时间,任务更快完成主要适合后台运算而不需要太多交互的任务。

吞吐量 = 用户运行代码时间 /  (用户运行代码时间 + 垃圾回收时间)

有一个自适应参数开关(-XX:+UseAdaptiveSizePolicy)打开之后,就不需要手工指定新生代大小(-Xmn)、Eden与Survivor比例(-XX:SurvivorRatio)等细节参数,JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量,自适应调节策略也是它和ParNew收集器的一个重要区别。

由于Parallel Scavenge收集器及后面提到的G1收集器没有使用传统的GC收集器代码框架,而另外独立实现,其余收集器则公用了部分的框架代码,因此无法与CMS配合使用,常和Parallel Old 收集器配合使用(JDK1.5及以前版本没有Parallel Old之前与Serial Old一起使用)

4. Serial Old收集器

Serial收集器的老年代版本,作为CMS收集器在发生Concurrent Mode Failure时的后备预案。

5. Parallel Old收集器

Parallel Scavenge收集器的老年代版本,从JDK1.6开始提供。在注重吞吐量及CPU资源敏感的场合,优先考虑Parallel Scavenge加Parallel Old收集器组合。

6. CMS收集器(Concurrent Mark Sweep)

以获取最短回收停顿时间(SWT)为目标的收集器,应用于互联网或B/S系统的服务端,注重服务的响应速度,尽可能缩短停顿时间,以给用户带来快速的响应体验。CMS收集器基于标记清除算法实现,主要分为4个步骤:

  1. 初始标记,需要stop the world,标记GC Root能直接关联到的对象,速度快
  2. 并发标记,对GC Root执行可达性算法,可以与用户线程一起工作
  3. 重新标记,需要stop the world,修复并发标记时因用户线程运行而产生的标记变化,所需时间比初始标记长,但远比并发标记短
  4. 并发清除,可以与用户线程一起工作

由于整个过程中耗时最长的并发标记和并发清除均可以与用户线程一起工作,总体上,可以认为CMS的响应是即时的。但是,也有如下缺点:

  1. 其对于CPU资源很敏感。在并发阶段,虽然CMS收集器不会暂停用户线程,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量降低。其默认启动的回收线程数是(cpu数量+3)/4,当cpu数较少的时候,会分掉大部分的cpu去执行收集器线程
  2. 无法处理浮动垃圾,浮动垃圾即在并发清除阶段因为是并发执行,还会产生垃圾,这一部分垃圾即为浮动垃圾,要等下次收集。正是由于在垃圾收集阶段用户线程还可能持续产生垃圾,因此需要预留足够的内存空间给用户线程使用,因此CMS不能像其他收集器那样等到老年代完全被填满了再进行收集,需要预留部分空间提供并发收集时用户线程使用。如果预留的内存无法满足需要,就会出现“Concurrent Mode Failure”,从而启用Serial Old收集器重新进行收集,从而导致收集时间更长
  3. CMS收集器使用的是标记清除算法,GC后会产生碎片。通过-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩的Full GC之后,执行一次带压缩的清除碎片

G1收集器(Garbage First)

相比CMS收集器,G1收集器主要有两处改进:

  1. 使用标记整理算法,确保GC后不会产生内存碎片
  2. 可以精确控制停顿,允许指定消耗在垃圾回收上的时间

G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。