以下两篇文章是笔者前面所翻译的关于垃圾回收的文章:
CMS(Concurrent Mark Sweep)垃圾收集器
Hotspot中的parallel垃圾收集器旨在尽量减少应用程序花费在垃圾收集上的时间,这被称为吞吐量(throughput)。对于所有的应用来说,这并不是很适当的折中方案,有些所谓的延迟需求(latency requirement)要求系统停顿时间最短。
CMS(Concurrent Mark Sweep)收集器被设计成比parallel收集器延迟更低的收集器。这种设计的最关键的一部分就是在应用程序运行的同时尝试去做垃圾回收的部分工作。这就意味着当收集器需要暂停应用的执行时它并不会暂停太长的时间。
在这一点上你可能会想“并行(parallel)回收和并发(Concurrent)回收不是很类似吗?”,并行GC(GC Parallel)的环境指的是“同时使用多个线程去执行GC”,并发GC(Concurrent GC)指的是“在运行应用程序的同时运行GC进行垃圾回收”。
新生代回收(Young Generational Collection)
在CMS中新生代回收器称为ParNew,ParNew实际上所使用的算法和并行收集器(parallel collectors)中的并行回收(Parallel Scavenge)算法一致,这个在前面我们已经描述过了。
在hotspot代码库中对于并行回收(Parallel Scavenge)来说ParNew仍然是一个不同的收集器,因为它的执行需要和CMS的其他部分进行交互,并且它实现了一个不同的内部API来完成并行回收(Parallel Scavenge)。并行回收(Parallel Scavenge)对与那个老年代收集器(tenured collectors)一起工作做了假设——特别是ParOld和SerialOld。这也就意味着新生代(young generational)的收集器也属于“stop the world”,在进行垃圾回收是也需要暂停应用。
老年代回收(Tenured Collection)
与ParOld收集器一样,CMS的老年代(tenured)收集器使用了标记和清除(mark and sweep)算法,可达的对象会被标记而不可达的对象会被删除。在内存管理中删除实际上是一个很奇怪的术语。收集器实际上并不是在消隐内存的意义上去删除对象,它实际上是将与对象相关联的内存返回到内存系统可分配的空间——空闲列表上。虽然术语上我们称之为并发标记和清除(concurrent mark and sweep)收集器,实际上并不是每个阶段都与应用程序并行运行,在所有的阶段中其中两个阶段也是属于“stop the world(需要暂停应用)”,其中有四个阶段与应用程序并行运行。
GC如何被触发?
在ParOld中是当老年(tenured )堆空间不足时垃圾回收被触发,这种做法可行是因为ParOld只是简单的暂停应用程序然后开始回收。为了让应用程序在老年代空间进行回收时也能继续运行,CMS需要在老年代空间还剩余足够空间的时候开始进行垃圾回收。
因此CMS的启动基于老年代空间有多大——这种思想就是剩余空闲空间的数量为执行GC提供了了一个最佳时机。这就是所谓的initiating occupancy fraction(启动占用率),并以堆的多大程度来加以描述。因此如果initiating occupancy fraction(启动占用率)为70%就相当于在堆空间被用完之前给了堆空间的30%的来运行GC。
CMS阶段
一旦GC被触发,CMS算法有一系列有序的阶段所组成。
初始标记(Initial Mark):暂停应用线程,标记所有可直接从“root”对象访问的对象为可达对象。这个阶段stop the word。
并发标记(Concurrent Mark):应用线程重启。这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记所有可达对象。
并发预清理(Concurrent Preclean):本阶段查找并发标记期间更新或者升级的对象或者在并发标记期间被分配的新对象。它更新标记位以表示这些对象是可达的还是不可达的。此阶段可能会重复运行直到Eden空间达到指定的使用率(specified occupancy ratio)。
重新标记(Remark):因为一些对象可能在预清理阶段已经被更新,但是仍然是有必要stop the word去处理其他的一些对象。这个阶段从root对象开始向下追溯,它也处理引用对象,例如软引用和弱引用(soft and weak references)。此阶段stop the word。
并发清理(Concurrent Sweep):这看起来就像普通对象指针(Ordinary Object Pointer )表引用所有堆中的对象,并找到不可达的对象。然后重新添加被分配给这些不可达对象的内存到堆中的空闲列表。这个可以分配对象的空间列表。
并发重置(Concurrent Reset):重置CMS收集器的数据结构,等待下一次垃圾回收。
理论上在预清理(Preclean)阶段对象就已经被标记,但是在下一个阶段——重新标记(Remark)阶段——对象依然会被检索,但是重新标记(Remark)阶段是stop the world的,因此预清理(preclean)阶段的存在主要是为了尝试并发的完成一部分重新标记工作来减少重新标记阶段的暂停时间。原来CMS被加入HotSpot的时候这个阶段是不存在的。此阶段是在Java 1.5的时候被添加来解决新生代(young generation)垃圾回收导致的停顿场景并且很快被重新标记所关注。重新标记会导致一个停顿,这些停顿组合在一起就会导致一个更痛苦的停顿。这也是为什么重新标记是需要在Eden空间占用率达到一定的门槛之后才会被触发的原因——目标是把重新标记阶段安排在新生代停顿之间。
重新标记阶段也需要停顿,但是预清理阶段不需要,这就意味着预清理阶段减少了GC所花费的停顿时间。
并发模式失效(Concurrent Mode Failures)
有时CMS无法满足应用程序的需求且需要运行一次stop the world的Full GC。这被称为并发模式失效(concurrent mode failure),这通常所导致的结果将会是一个很长的停顿。在老年代没有足够的空间升级对象时并发模式失效(concurrent mode failure)就会发生,有两种情况会导致并发模式失效(concurrent mode failure)。
一个被提升的对象太大而不能够找到合适的连续内存空间。
老年代没有足够的空间支持被提升的可达对象的比率。
这些情况会发生是因为并发回收清理空间的速度跟不上对象被提升的速率,或者因为CMS的持续使用造成了堆内存的内存碎片,并且没有足够大的独立空间能够足以分配被提升的对象。为了能够正确的“整理”老年代堆空间,一次Full GC就很有必要。
永久代(Permgen)
CMS默认情况下不能回收永久代空间(permgen spaces),如果要让CMS回收永久代空间(permgen spaces)的话,需要XXX:+CMSClassUnloadingEnabled标记启动。在使用CMS时,如果永久代空间(permgen spaces)被填满时没有设置这个标记,那么将会触发一次Full GC。此外,永久代空间(permgen space)可以通过类加载器之类的东西将引用保存到正常的堆中,也就是说直到回收永久代时,可能会造成常规的堆内存溢出。在Java 7中,类文件的字符串常量被分配到常规堆(regular heap),而不是永久带空间,从而减少永久代空间的消耗,同时也从永久代空间添加一些引用到常规堆。
浮动垃圾(Floating Garbage)
CMS回收的最后可能有一些对象没有被删除——这被称为浮动垃圾。这种情况的发生是在初始标记的时候对象已经变成不可达对象(de-referenced/解除引用)。并发预清理和重新标记阶段通过查询已创建、已突变或已提升的对象确保所有可达对象被标记。如果一个对象在初始标记和重新标记阶段引用已经被解除(已经不可达),那么它就需要完全回滚整个对象图以找到所有不可达对象。这显然是非常昂贵的开销,但是重新标记阶段必须要保持很短的时间,因为它是一个停顿阶段。
对于CMS用户来说,这是一个没有必要的问题,因为下一次CMS收集器运行的时候会清理掉这些垃圾。
总结
并发标记与清理(Concurrent Mark and Sweep)通过在应用程序运行的同时同步执行一些GC工作来减少我们在并行回收器(parallel collector)中所看到的停顿时间。但是它并没有完全移除停顿,因为它的算法的一部分也需要停顿应用执行。
原文:https://www.javacodegeeks.com/2013/06/garbage-collection-in-java-3.html