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

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

虚拟机如何判断对象是否存活?

1.引用计数算法

  给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

  考虑一种情形:对象objA和objB都有字段instance,赋值令objA.instance=objB和objB.instance=objA;除此之外,这两个对象再无任何引用,实际上这两个对象以及不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。如果这个对象特别大,则会造成严重的内存泄露。

2.可达性分析算法

  基本思想:通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的。

GC Roots的对象包括下面几种:

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

垃圾收集算法

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

  最基础的收集算法,分为”标记”和”清除”两个阶段:首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

  主要不足有两个:一是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片大多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。

2.复制算法(Copying)

  为了解决效率问题,一种称为”复制“的收集算法出现,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指正按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一般,未免太高了一点。

  现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司研究表明,新生代的对象98%都是”朝生夕死“的,所以并不需要1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间。HotSpot默认Eden和Survivor的大小比例是8:1.如果Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)

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

  复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。根据老年代的特点,提出此种算法,标记过程仍然与”标记-清除“算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。

4.分代收集算(Generational Collection)

  当前商业虚拟机的垃圾收集都采用”分代收集“算法。一般是把java堆分成新生代和老年代。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用”标记——清理“或者”标记——整理“算法来进行回收。


垃圾收集器

  HotSpot虚拟机的垃圾收集器如图所示。

Serial收集器

  这是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条手机线程去完成垃圾手机工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。“Stop the world”,由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的。它是虚拟机运行在Client模式下的默认新生代收集器。

  优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

ParNew收集器

  ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

Parallel Scavenge收集器

  Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

  Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

Parallel Old收集器

  Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS收集器

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrenr mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

  其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。

  由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

  CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点:

  CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。

  CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。

  最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。

G1收集器(Garbage-First)

  G1是一款面向服务器应用垃圾收集器,与其他GC收集器想必,G1具备以下特点:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一半时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的“标记-清理”算法不同,G1从整体上看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现,无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,小号在垃圾收集上的时间不能超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

  在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remebered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中,就是检查是否老年代中的读写引用了新生代中的对象)。如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Rememered Set即可保证不对全队扫描也不会有遗漏。

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

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选标记

垃圾收集器参数总结

参数 描述
-XX:+UseSerialGC Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收
-XX:+UseParNewGC 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收
-XX:+UseConcMarkSweepGC 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。
-XX:+UseParallelGC Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收
-XX:+UseParallelOldGC 使用Parallel Scavenge + Parallel Old的收集器组合进行回收
-XX:SurvivorRatio 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1
-XX:PretenureSizeThreshold 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
-XX:MaxTenuringThreshold 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代
-XX:UseAdaptiveSizePolicy 动态调整java堆中各个区域的大小以及进入老年代的年龄
-XX:+HandlePromotionFailure 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留
-XX:ParallelGCThreads 设置并行GC进行内存回收的线程数
-XX:GCTimeRatio GC 时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效
-XX:MaxGCPauseMillis 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效
-XX:CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSCompactAtFullCollection 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效
-XX:+CMSFullGCBeforeCompaction 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用
-XX:+UseFastAccessorMethods 原始类型优化
-XX:+DisableExplicitGC 是否关闭手动System.gc
-XX:+CMSParallelRemarkEnabled 降低标记停顿
-XX:LargePageSizeInBytes 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m


Client、Server模式默认GC

服务端or客户端 | 新生代GC方式 | 老年代和持久代GC方式
— | —
Client | Serial 串行GC | Serial Old 串行GC
Server | Parallel Scavenge | 并行回收GC Parallel Old 并行GC

Sun/oracle JDK GC组合方式

参数 新生代GC方式 老年代和持久代GC方式
-XX:+UseSerialGC Serial 串行GC Serial Old 串行GC
-XX:+UseParallelGC Parallel Scavenge 并行回收GC Serial Old 并行GC
-XX:+UseConcMarkSweepGC ParNew 并行GC CMS 并发GC 当出现“Concurrent Mode Failure”时,采用Serial Old 串行GC
-XX:+UseParNewGC ParNew 并行GC Serial Old 串行GC
-XX:+UseParallelOldGC Parallel Scavenge 并行回收GC Parallel Old 并行GC
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC Serial 串行GC CMS 并发GC 当出现“Concurrent Mode Failure”时采用Serial Old 串行GC

参考资料

  1. 《深入理解Java虚拟机》周志明著
  2. Java虚拟机学习 - 垃圾收集器

欢迎支持笔者的作品《深入理解Kafka: 核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客(ID: hiddenkafka)。
本文作者: 朱小厮

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×