Java虚拟机——垃圾收集算法

前言

几乎所有的对象都在堆(Heap)分配,我们只有在程序运行期时才知道要创建哪些对象,这部分内存的分配和回收都是动态的。垃圾收集器所关注的正是这部分内存。学习GC之前务必清楚地知道JVM运行时内存,可以先阅读文章 Java虚拟机——内存区域

什么样的对象是可回收对象
  • 引用计数器法:给每个对象设置一个引用计数器,对象被引用一次,计数器加一;引用失效时,计数器减一。任一时刻,引用计数器为0的对象就是可回收对象。但是引用计数器很难解决循环引用的问题,a,b两个对象如果相互引用,而没有被其他任何对象引用,理论上来说这两个对象均为可回收对象,但是因为他们的引用计数器均不为0,导致无法被回收。
  • 可达性分析:基本思路是从一系列名为“GC Roots”的对象出发开始向下搜索,搜索走过的路径称为引用链,当一个对象到“GC Roots”没有路径相连(不可达)时,该对象就是可回收对象。

“GC Roots”对象包括以下:

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
垃圾收集算法
  • 标记-清除(Mark-Sweep)算法:算法分为“标记”和“清除”两个阶段,首先标记出需要回收的对象,在标记完成后统一回收所以标记的对象。算法优点是简单,缺点有两个:效率低下和容易产生内存碎片。算法可视化过程大致如下:
    mark-swewp算法
  • 复制(Copying)算法:将可用内存分为大小相等的两块,每次只使用其中的一块。当其中的一块内存用完了,就将还存活的对象复制到另一块,然后再把可回收的对象都回收。这样每次都是对一半的区域进行垃圾清理,分配对象时不用考虑内存碎片的问题,只需要移动堆顶指针,按照顺序分配内存即可。优点是实现简单,运行高效。缺点是内存利用率太低,每次只使用一半的内存。一般年轻代会用此算法,因为年轻代中约有98%的对象都是“朝生夕死”的,所以也不需要按照1:1来划分两块区域。HotSpot虚拟机年轻代是分为Eden区和两个Survivor区,大小比例是8 : 1(是Eden区和其中一个Survivor区的比例,两个Survivor一样大)。每次只使用Eden区和其中一个Survivor区,回收时,将Eden区和Survivor区的存活对象复制到另一个Survivor区,然后清理掉Eden区和使用过的Survivor区,算法可视化过程大致如下:
    copying算法
  • 标记-整理(Mark-Compact)算法:算法分为“标记”和“整理”两个阶段,“标记”阶段和“标记-清理”算法一样。但是后续步骤不是直接清理标记对象,而是把存活对象向一端移动,然后直接清理端边界以外的内存。算法可视化过程大致如下:
    mark-compact算法
    事实上现在商用虚拟机采用的都是分代收集算法,根据对象的存活周期把堆(Heap)分为年轻代和老年代。每次收集,年轻代会回收绝大部分对象,所以采用复制算法,只需要消耗少量对象的复制成本。老年代中对象存活周期较长,所以采用标记-清除算法或者标记-整理算法。
垃圾收集器

垃圾收算法是理论指导,垃圾收集器则是实际应用。HotSpot虚拟机(JDK1.7)垃圾收集器大致有以下几款:
HotSpot-gc
连线表示两款垃圾收集器可以搭配使用,所处区域表示垃圾收集器工作的区域。

  • Serial收集器:是最基本,发展历史最悠久的收集器。这个收集器是一个单线程的收集器,在进行垃圾收集时,必须暂停所有工程的线程,直到它收集结束,也就是“Stop The World”。优点是简单、高效,单线程不会有线程切换的开销。-XX:+UseSerialGC参数指定使用该收集器。工作过程如下(Serial和Serial Old搭配使用):
    Serial/Serial Old收集器
  • ParNew收集器:是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余的行为均与Serial收集器一致。参数-XX:+UseParNewGC指定使用该收集器,工作过程如下(ParNew和Serial Old搭配使用):
    ParNew-Serial Old
  • Parallel Scavenge收集器:年轻代垃圾收集器,使用复制算法。目的是达到一个可控制的吞吐量,是吞吐量优先的垃圾收集器。参数-XX:+UseParallelGC指定使用该收集器,吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集时间),Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大停顿时间的-XX:MaxGCPauseMillis,以及直接设置吞吐量的参数-XX:GCTimeRatio。除此之外,还提供了参数-XX:+UseAdaptivePolicy实现GC自适应调节策略,即不需要手动设置年轻代大小、Eden区与Survivor区比例、晋升老年代年龄等参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。其工作过程如下(Parallel Scavenge和Parallel Old搭配使用):
    Parallel Scavenge-Parallel Old
  • Serial Old收集器:是Serial收集器的老年代版本,是一个单线程收集器,使用“标记-整理”算法,参数-XX:+UseSerialOldGC指定使用该收集器。
  • Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,参数-XX:+UseParallelOldGC指定使用该收集器。
  • CMS(Concurrent Mark Sweep)收集器:是一款以获取最短回收时间为目的的垃圾收集器,基于“标记-清除”算法,参数-XX:+UseConcMarkSweepGC指定使用该收集器,运行过程分为四步:初步标记、并发标记、重新标记、并发清除。下文会详细介绍CMS。
  • G1(Garbage First)收集器:与其他垃圾收集器相比,G1收集器具有并发与并行、分代收集、空间整合、可预测停顿等特点,参数-XX:+UseG1GC指定使用该收集器。
CMS收集器

CMS(Concurrent Mark Sweep)垃圾收集器是一款以获取最短停顿时间为目的的垃圾收集器,从名字上可以看出基于“标记-清除”算法实现。整个过程分为四个步骤:
CMS-gc

  • 初始标记(CMS initial Mark):该过程仅仅标记“Gc Roots”直接关联的对象,速度很快
  • 并发标记(CMS concurrent Mark):该过程就是Gc Roots搜索的过程
  • 重新标记(CMS remark):该过程是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动那一部分对象的标记记录,消耗时间比初始标记长,但是远小于并发标记
  • 并发清除(CMS concurrent sweep)

CMS收集器的主要优点是并发收集和低停顿;主要缺点有三个:

  • 对CPU资源敏感。CMS收集器默认启动的回收线程数是(CPU数量 + 3)/ 4,当CPU数量大于4时,并发回收时垃圾线程不少于25%的CPU资源,但是当CPU数量小于4个时,CMS对用户程序的影响就会变得很大。
  • CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生,在并发清除阶段,由于工作线程还在运行,所以必定会产生新的垃圾。但是新的垃圾是出现在标记之后,所以只有下一次gc的时候才能清除,这部分垃圾就是“浮动垃圾”。正是由于浮动垃圾的存在,所以CMS不能等到老年代几乎快满了才进行垃圾回收,而是预留一些空间。CMS收集器在老年代使用了92%(JDK1.6,参数-XX:CMSInitiatingOccupancyFraction指定)后就会启动。要是CMS运行期间预留的空间无法满足程序需要,就会出现“Concurrent Mode Failure”失败,这时虚拟机将启用后备方案:临时使用Serial Old收集器来重新进行老年代垃圾回收,这样停顿时间就很长了。
  • 容易产生内存碎片:因为CMS基于“标记-清除”算法。内存碎片过多的话,容易给大对象的分配带来很大的麻烦,往往老年代还有大量的空间,但是无法找到连续的空间来分配大对象,所以不得不提前触发一次Full GC。为了解决这个问题,虚拟机提供了一个参数-XX:+UseCMSCompactAtFullCollection,用于CMS顶不住要进行Full GC时,开启内存碎片的合并整理过程。内存碎片的问题解决了,但是停顿时间变长了。虚拟机还提供了另一个参数-XX:CMSFullGCsBeforeCompaction,这个参数表示执行多少次不压缩的Full GC后,接着执行一次压缩的Full GC(默认值为0,表示每次Full GC时都压缩)。
完整的GC过程

新创建的对象在Eden区,当Eden区空间不足时,触发Minor GC(年轻代GC),会把Eden区和已使用的Survivor区中的存活对象复制到另一个Survivor区,然后清空Eden区和之前使用的Survivor区。当对象熬过15次Minor GC(-XX:MaxTenuringThreshold参数指定该值,默认15)后依然存活,会转移到老年代;如果Survivor区放不下该对象,会直接放入老年代;新生成的大对象(-XX:+PretenuerSizeThreshold指定阈值,超过该值就是大对象,该参数只对Serial和ParNew两款收集器有效)直接进入老年代;如果在Survivor区相同年龄所有对象大小总和大于Survivor区的一半,则大于等于该年龄的对象进入老年代而无需等待-XX:MaxTenuringThreshold指定的参数。
Full GC是指老年代GC,但是老年代GC往往伴随着年轻代GC的发生,Full GC触发条件:

  • 老年代空间不足
  • 永久代空间不足(JDK7及以下)
  • CMS收集器出现promotion failed或者concurrent mode failure。前者是在Minor GC过程中,Survivor空间不足,对象进入老年代,而老年代空间也不足时会触发Full GC;后者是CMS收集器收集时,老年代预留空间不足,会触发Full GC。
  • Minor GC进入到老年代的平均大小大于老年代剩余空间
  • 程序调用System.gc(),提醒收集器进行Full GC,不过仅仅是提醒,具体的执行还是由JVM决定。
常用参数
  • -XX:+PrintGCDetails:打印GC日志
  • -Xss每个线程的虚拟机栈大小,一般256K
  • -Xms:堆的初始值
  • -Xmx:堆的最大值,一般设置和Xms一致,因为heap扩容时,会发生内存抖动,影响程序稳定性
  • -XX:NewSize:年轻代初始空间大小。
  • -XX:MaxNewSize:年轻代最大空间大小。
  • -Xmn:年轻代大小
  • -XX:SurvivorRatio:年轻代中Eden和Survivor的比值,默认8:1(Eden占8/10,两个Survivor各占1/10)
  • -XX:NewRatio:老年代和年轻代内存大小比例,默认2:1
  • -XX:MaxTenuringThreshold:年轻代对象进入老年代,熬过Minor GC次数的阈值
  • -XX:PermSize:持久代初始化大小,默认64M(JDK7及以下)
  • -XX:MaxPermSize:持久代初始化大小最大值(JDK7及以下)
参考

《深入理解Java虚拟机》

发布了52 篇原创文章 · 获赞 107 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Baisitao_/article/details/96019888