说明
- ==本文摘自【MemoryManagement-Whitepaper-1-150020.pdf】并转译,本文并不是完整的转译,部分地方有删减;==
- ==本人水平有限,如有不正确的地方烦请指出,感激不尽。==
概述
在J2SE5.0u6的Java HotSpot虚拟机包含了四种垃圾回收器,所有回收器都是分代使用。本节描述了回收器的分代和类型,并且还讨论了对象经常快速而有效的分配原因,在后续也提供了每种回收器的详细信息。
HotSpot分代
在Java HotSpot虚拟机中内存分为三代:年轻代、老年代和永久代。大多数的对象刚创建时都被分配在年轻代,老年代包含的对象都是在年轻代经过多次(垃圾)回收而幸存下来的对象,一些大的对象也有可能直接分配到老年代中,永久代持有的对象是是JVM方便垃圾回收器查找管理,如描述类和方法的对象,以及类和方法本身。
年轻代分为Eden和两个小的幸存区,如下图所示
大多数对象刚创建时都分配在Eden中(没说全部对象创建时都分配在Eden是因为有些大对象是直接分配在老年代),幸存区的对象都至少经历过一次年轻代的回收幸存下来,因此,在被认为’足够年龄’进入到老年代之前,可能已经死亡。在任何时刻,其中的一个幸存区拥有这些对象,同时另外一个幸存区为空,一直没使用,直到下次回收时才恢复。
垃圾回收类型
当年轻代被充满的时候,就会产生年轻代回收(有时候也叫做微小回收[minor collection]),当老年代或持久代被充满的时候,通常就会产生著名的满回收(有时候也叫主要回收[major collection]),意思就是说所有代都会进行回收。一般来说,年轻代会最先被回收,使用的回收算法也是专门对应年轻代的回收算法,因为它通常是在年轻代中能高效识别垃圾的算法,接下来要说的就是运行在老年代和持久代中根据不同回收器而不同的老年代回收算法,还有压缩,如果压缩产生的时候,每个代都分开执行压缩。
有时候如果年轻代优先回收时,从年轻代推入老年代的对象会导致老年代太满而无法容纳所有对象,在这种情况下,就可以使用CMS回收器,年轻代的回收算法不再运行,反之,老年代的回收算法应用在整个堆(CMS老年代回收算法比较特殊,因为它不能回收年轻代)。
快速分配
在接下来垃圾回收器的描述中,你可以看到,很多情况下,对象需要分配大块连续的内存,要从这些块中有效分配,可以使用bump-the-pointer技术,这就是说前一个对象被分配的内存结尾会一直被跟踪,当有一个新的分配请求时,我们所需要做的就是检查这个对象能不能放在这个代中的剩余部分,如果可以,就像上面一样更新指针并初始化对象。
在多线程应用中,分配操作需要是线程安全,如果使用全局锁来确保这个操作,那么分配到代中就会成为瓶颈并降低性能,为此,HotSpot JVM 推出了一项技术叫Thread-Local Allocation Buffers(TLABs),这项技术可以根据线程本身的缓冲区来提升多线程分配的吞吐量,因为每次分配都只有一个线程在TLAB,可以使用bump-the-pointer技术快速的完成分配而不必请求任何锁。只有很少的情况下,一个线程才会充满整个TLAB并且还需要一个新的TLAB,这时候就需要同步。还有几种技术也是因为使用TLAB技术才得以消耗很小的空间来实现。例如,使用TLABs技术的回收器只消耗Eden少于1%的空间。TLABs的应用和线性回收的bump-the-pointer的应用使得每一次回收都很高效,而要实现这个效果只需要10条左右的本地指令。
串行回收器
使用串行回收器,年轻代和老年代的回收都完全使用串行,都在stop-the-world模式下,意思就是说当回收进行时应用的运行就会被挂起。
年轻代使用串行回收器
如上图所示,年轻代使用的回收器就是串行回收器。Eden中的存活对象被复制到空的幸存区中(在图中标识为’To’的区域),当然太大的对象也不会复制到这里而是直接复制到老年代中。幸存区(图中的From区)的存活对象还相对年轻,还会被复制大另外一个幸存区(图中的To),直到到了指定年龄才会被复制到老年代。==注意:如果To空间已经被充满,那么Eden或From中的存活对象不管多少,将终身不会被复制到To中。在Eden或From中,复制存活对象过后剩下来的所有对象,没有特殊情况的话,都不是存活对象,并且它们也没有剩余的必要(在图中用’X’的对象就不是存活对象)。==
在年轻代回收完成之后,Eden和以前被占用的幸存区空间都已为空,并且以前为空的幸存区空间才包含存活对象,这就是幸存区的交换规则,详细见下图:
老年代使用串行回收器
如果老年代和持久代中要使用串行回收器,可以使用标记清理压缩(mark-sweep-compact)回收算法。在标记阶段,回收器会识别哪些对象还存活,在清理阶段,清理会遍历整个代来识别垃圾。然后回收器会进行滑动压缩,把存活对象推到老年代空间的开始处(持久代也是同样做法),使得所有空闲空间都连续在对面,如下图。
压缩使用bump-the-pointer技术使得后续分配到老年代或持久代的请求可以快速完成。
使用串行回收器的时机
串行回收器是大多数运行在client模式下的应用程序的选择,它们不要求低停顿时间,在今天硬件情况下,串行回收器和64MB的堆可以高效的管理大多数的普通应用,并且全部回收所导致的停顿时间也小于一秒。
串行回收器的选项
在J2SE 5.0发行版中,串行回收器在非server模式的机器中是默认选择,在其他机器中,可以使用’-XX:+UseSerialGC’命令选项来使用串行回收器。
并行回收器
在今时今日,很多java应用运行的机器都有很多内存和多颗CPU,并行回收器也称为吞吐量回收器,为了更高效的利用CPU而开发,相当于一颗CPU在运行垃圾回收工作的时候而其他CPU没有在荒废。
年轻代使用并行回收器
并行回收器是利用串行回收器的年轻代回收算法的并行版本,它依然有停止世界(stop-the-world)和复制回收器,但在年轻代中是使用多颗CPU并行执行这些操作,减少垃圾回收的开销和提高应用程序的吞吐量,下图展示了在年轻代中使用串行和并行的区别:
老年代使用并行回收器
老年代垃圾回收使用并行回收器的做法和串行回收器的串行标记清理压缩回收算法是使用相同的序列来完成。
使用并行回收器的时机
应用程序可以在多核CPU的机器中使用并行回收器获益很多,它可以不受短暂的停顿时间的限制,但也有潜在的风险,如果老年代的回收经常发生的话就会产生很大的停顿时间。举一些并行回收器经常占用应用程序的例子,如批量处理、计算、工资已经科学计算等。
这时候你可能会选择并行压缩回收器来代替并行回收器,因为它的回收操作是作用在所有代中,而不单单是年轻代。
并行回收器的选项
在J2SE 5.0发行版中,在server模式的机器中就是使用并行回收器作为默认的回收器,在其他机器中,可以使用’-XX::+UserParallelGC’命令行选项来使用并行回收器。
并行压缩回收器
并行压缩回收器在J2SE5.0u6中第一次添加,它和并行回收器的区别在于老年代的垃圾回收使用了新算法,==注意:以后并行压缩回收器必定会取代并行回收器==
年轻代使用并行压缩回收器
年轻代中使用并行压缩回收器的垃圾回收和使用并行回收器的垃圾回收应用的是同一种算法。
老年代使用并行压缩回收器
使用并行压缩回收器,老年代和持久代的回收也会有停止世界(stop-the-world)的影响,大多数的滑动压缩也是使用并行方式。回收器会经历三个阶段。首先,每个代在逻辑上都会分为固定大小的区域,在标记阶段,直接从应用程序代码访问的初始对象被划分到垃圾收集线程中,然后所有存活对象被并行标记。如果一个对象被识别为存活,那么它的数据所在区域的大小和对象的位置将更新。
分区域操作的总结阶段,不包含讨论对象,因为经过前一个回收的压缩,左侧每一代的某些部分已经很密集,包含着很多的存活对象,再从这些密集区域恢复的空间不值得再次压缩它们,所以总结阶段的第一件事就是检查这些区域的密度,从左侧最密集的开始,直到遇到有恢复空间的点,并且恢复这些空间值得再次压缩该区域,该点左边的区域称为密集前缀,没有对象可以再移入这些区域;该点右侧的区域可以再次压缩,以消除所有死亡空间。总结阶段计算并存储每个压缩区域中存活对象的第一个字节。
==注意:总结阶段目前的实现是串行化,而实现并行化也是有可能的,但对于标记和压缩阶段的并行化来说,性能反而没那么重要。==
在压缩阶段,垃圾回收线程使用即时的数据来标识哪些区域需要被充满,并且线程会自主的复制数据到该区域中,这会导致堆的一端数据很密集,而另一端则很空白。
使用并行压缩回收器的时机
和并行回收器一样,并行压缩回收器在多颗CPU的机器上可以让应用程序获得更好的受益。另外,老年代回收的并行操作可以减少停顿时间(pause time),这使得并行压缩回收器比并行回收器更能减少停顿时间(pause time)。并行压缩回收器并不适合让应用程序运行在大型共享机器(如SunRays)上,在这些类型的机器上并没有让一个应用程序独立占用所有CPU资源。在这样的机器中,我们需要考虑减少垃圾回收的线程数(通过命令行选项’-XX: ParallelGCThreads=n ‘来设置)或选择其他回收器。
并行压缩回收器的选项
如果你想用并行压缩回收器,你可以使用’-XX:+UseParallelOldGC’命令行选项来设置。
并行标记清理回收器
对于很多应用程序来说,快速响应时间比端对端的吞吐量更重要,那么它所需要的就是年轻代的回收不可以有很长的停顿时间(pause time)。然而,老年代的回收,虽然很少发生,但依然还是有较长的停顿时间,特别是在大堆中执行回收的时候。要解决这个问题,HotSpot JVM提供了一种叫并行标记清理(CMS)的回收器,也可以称为低潜伏回收器(low-latency collector)。
年轻代使用CMS回收器
年轻代使用CMS回收器的方式和并行回收器一样。
老年代使用CMS回收器
很多老年代的回收使用CMS回收器时,都是和应用程序一起并行执行。在CMS回收器开始回收时会伴有短暂的停顿(short pause),称为初始标记,它会从应用程序的代码中直接识别存活对象集,然后,在并行标记阶段,回收器会从这个集合中标识所有存活对象,因为在标记阶段,应用程序正在运行并会更新字段的引用,所以在并行标识阶段后并不能保证所有的存活对象都还没存活,要处理这个情况,应用程序还需要再来一次短暂的停顿(pause),这个阶段称为重新标记(remark),这个阶段可以最终标识在并行标识阶段被更改的所有对象,因为重新标识的停顿区间比初始标识的停顿区间要大得多,因此可以使用并行的多线程来提升它的效率。
在重新标识阶段结尾,堆中的所有存活对象都确保已被标识,因此随后的并行清理阶段将会清理所有被识别的垃圾对象。下图展示了老年代回收使用串行标记回收压缩回收器和CMS回收器的区别。
自从某些任务之后,在重新标记阶段的重访对象,增加了回收器的工作量,同时也增加了它的开销,这是大多数回收器尝试减少停顿时间的一个权衡选择,CMS回收器是唯一一个没有压缩的回收器,这将导致它在释放死亡对象的空间之后,它不会移动存活对象到老年代的末尾,具体情况请看下图:
要节省这些时间,但由于这些空间不是连续的,回收器不再使用简单指针来指向下一个可被分配的位置。相反,它现在要使用这些空闲列表。意思就是说,它会创建一些链接把那些没被分配的内存区域连接在一起,在每次需要分配一个对象时,这些连接列表就搜索大到足够容纳这个对象的内存区域,为达到这个效果,在老年代中分配比简单使用bump-the-pointer技术需要消耗更多,这也会导致在年轻代回收中产生额外的消耗,就像在年轻代回收中把对象推入老年代的时候就会产生这些消耗。
CMS回收器还有另外一个缺点就是比其他回收器要申请更大的堆大小。在标记阶段区间,为了保证应用程序的运行,回收器只能不断的申请内存,从而导致老年代的不断增长,还有,虽然回收器保证在标记阶段识别所有存活对象,但有些对象在那区间还是有可能变成垃圾对象,并且它们也不会再生直到下一次老年代的回收,这些对象被称为漂浮垃圾。
最终,因为没有压缩所以还是会产生碎片,要解决这个碎片,CMS回收器会追踪普通对象的大小并预估未来的分配需求,有可能拆分或加入空闲块以满足需求。不像其他回收器,CMS回收器在老年代被充满的时候不会开始老年代回收。相反,它会在老年代被充满之前开始回收,使得在被充满之前回收完成。否则,CMS回收器会恢复到需要更多花时间来修复停止世界(stop-the-world)的使用标记清理压缩算法的并行和串行回收器,要避免这个情况,CMS回收器会在前一次回收结束时开始一个计时,统计老年代会多快被充满,如果老年代的占用率超过初始占用率,CMS回收器也会开始一次回收,这个初始占用率可以用命令行选项’-XX:CMSInitiatingOccupancyFraction=n’来设置,n是老年代的容量比例,默认是68。
总的来说,并行回收器的压缩,CMS回收器减少老年代的停顿时间-有时候会稍稍延长年轻代的停顿时间为代价,降低了吞吐量和增大了堆大小需求。
自增模式
CMS回收器在并行阶段使用了自增模式,这个模式旨在通过周期性的停止并发阶段来支持应用程序的返回处理减轻长时间的并发阶段的影响,回收器所做的工作被划分为在年轻代回收操作之间调度的小块时间内完成,这个特色非常有用,特别是应用程序运行在只有少量CPU(只有1颗或2颗)的机器上时还需要并发回收器提供短暂的停顿时间的时候非常有用。
使用CMS回收器的时机
如果你的应用程序需要短暂的垃圾回收停顿时间并且在应用程序运行时只能提供很少的处理器资源给垃圾回收器时,你就应该考虑使用CMS回收器。(因为它的并发性,CMS回收器在垃圾回收时不会占用太多CPU资源)一般来说,应用程序会拥有较大的长期存活数据集(较大的老年代),并且应用程序运行在多颗CPU的机器上,这样才能更好从这个回收器中受益。比如web服务器,所有需要短暂停顿时间的应用程序都应该考虑CMS回收器,它也可以在适度的单个处理器中,给交互性强的应用程序的老年代提供很好的处理结果。
CMS回收器的选项
如果你想使用CMS回收器,你必须明确使用命令行选项’-XX:+UseConcMarkSweepGC’,如果你想它运行在自增模式中,你可以通过’-XX:+CMSIncrementalMode’选项来打开。