这次我们来讲讲垃圾回收,前边或多或少的都提及过垃圾回收的知识点,我们经常说的GC(Garbage Collection)就是垃圾回收,我们都知道JAVA都是由C++演化而来,那么JAVA和C++很重要的一点不同就是自动分配内存和自动回收内存,这两块已经不需要JAVA开发者来操心。但是GC对性能是有影响的,有时候会暂停所有的线程,触发STW(Stop the world),所以GC是把双刃剑。那么一般情况下,垃圾回收的重点是在堆区,栈区是随线程的消亡而消亡的,不需要垃圾回收器去管理,方法区虽说可以进行垃圾回收,但是效率太低,基本也不用考虑。
分代回收主要说的是堆区,这个理论大体可以这么理解:绝大多数的对象是朝生夕死的,且经过多次垃圾回收还存活的对象会越来越难以回收,是一个长期存活的对象。所以基于以上两点,我们就把朝生夕死的对象放在一个区域--新生代,多次垃圾回收之后还坚挺的对象放在另外一个区域--老年代。
依据此图,我们来看一下堆区的分代划分:
新生代:分为Eden区以及两个survival 区--From区和to区,默认的情况下它们的内存大小比例是8:1:1,也就是说如果新生代分配了100M空间,Eden区大小是80M,From区和to区各10M。
老年代:是一个整块区域Tenured区,新生代和老年代的内存比例默认是1:2,也就是如果堆空间是300M,那么新生代是100M,老年代是200M。
所谓的分代回收指的就是对不同代的内存空间使用不同的垃圾回收器进行垃圾回收,对新生代进行回收我们一般叫MinorGC/YoungGC,对老年代回收我们一般叫做MajorGC/OldGC,目前只有CMS 垃圾回收器会有这个单独的回收老年代的行为。FullGC则是回收整个堆区和方法区,而实际上FullGC是调用了不同代所属的垃圾回收器进行回收,方法区使用的是老年代的垃圾回收器。
复制算法(Copying):将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。但是要注意:内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整。复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。
Appel 式回收:这是JVM基于复制算法的优化,因为Oracle做过统计发现,绝大部分对象(98%)都撑不过第一次垃圾回收,如果还是分为两块相等的内存块实际上是很浪费的,所以会将内存分为Eden区+From区和to区,这样每次浪费的内存空间是10%,当然这只是大多数情况下,如果存活的对象太多,to区装不下了,就会发生上一篇文章中的对象晋级。
这里我再着重说一下Eden区、From区和to区在垃圾回收中复制问题,MinorGC = Eden区+From区->to区,意思就是每次是把Eden区和From区存活的对象复制到to区,然后From区清空,然后From区变成to区,to区变成From区(就是名字的互换),第二次垃圾回收以此类推。
标记清除算法(Mark-Sweep):分为标记阶段和清除阶段,首先进行第一次扫描,标记所有可以回收的对象,然后进行第二次扫描,清理所有已标记的对象,需要两次扫描,回收效率比复制算法低但是不会浪费内存空间,所以适用于老年代这种单次可回收对象不多的情况下,缺点是会产生许多的内存碎片,如果一个大对象放入进来,很可能会因为没有一个较大的连续内存空间而报OOM异常。
标记整理算法:首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。
对算法做个总结:复制算法相对于标记清除/整理算法,效率高,速度快(不需要二次扫描),但是内存录用率相对较低,适用于对象大规模死亡的情况,标记清除相对于标记整理来说,内存碎片多,需要维护一个空闲列表,效率略高,而且对对象的直接引用不用换,因为垃圾回收后,对象本身的内存地址没有变,标记整理的好处是没有内存碎片,但是指针引用需要变更,效率比清除要低。
Serial/Serial Old:最古老的垃圾回收器,且是串行单线程的,想要进行垃圾回收,就必须暂停所有的用户线程。这种垃圾回收器只适合几十兆到一两百兆的堆空间进行垃圾回收(可以控制停顿时间再100ms 左右)。
Parallel Scavenge(ParallerGC)/Parallel Old/ParNew:多线程垃圾回收器,其实和Serial差不多,无非就是把GC线程由单线程变成了多线程。是关注吞吐量的垃圾收集器。吞吐量则是CPU用于运行用户代码时间与CPU总消耗时间的比值,该垃圾回收器适合回收堆空间上百兆~几个G。
CMS(Concurrent Mark Sweep):相对于前几种垃圾回收器,这是一款采用标记清除算法且以获取最短回收停顿时间为目标的收集器,最重要的是,它可以并发标记和并发清理,大大缩短了暂停所有用户线程的时间。
整个过程分为4 个步骤,包括:
初始标记:短暂,仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快。
并发标记:和用户的应用程序同时进行,进行GC Roots 追踪的过程,标记从GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)
重新标记:短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除:由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
缺点:
CPU敏感:如果CPU核心数少于4核,CMS对于用户的影响比较大。
浮动垃圾:浮动垃圾:由于CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在1.6 的版本中老年代空间使用率阈值(92%),如果预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,这时虚拟机将临时启用Serial Old 来替代CMS。
内存碎片:由于CMS采用的是标记清除算法,所以不可避免的会产生内存碎片。
之所以CMS会选用标记清除算法是因为它是要实现并发清理的,如果选用标记整理算法,在并发清除阶段,用户线程和垃圾回收线程在同时跑,如果此时进行标记整理,那么对象的引用可就全乱了,用户线程也绝对会出问题。
G1垃圾回收器:G1是一款追求最短停顿时间、实现停顿时间可预测且采用复制/标记整理算法。它不像其它回收器一样只负责新生代或者老年代的垃圾回收,它贯穿了整个堆内存。它将堆内存化整为零,将堆内存划分成多个大小相等独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region
Region 可能是Eden,也有可能是Survivor,也有可能是Old,另外Region 中还有一类特殊的Humongous 区域,专门用来存储大对象。G1 认为只要大小超过了一个Region 容量一半的对象即可判定为大对象。每个Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为1MB~32MB,且应为2 的N 次幂。而对于那些超过了整个Region 容量的超级大对象,将会被存放在N 个连续的Humongous Region 之中,G1 的进行回收大多数情况下都把Humongous Region 作为老年代的一部分来进行看待。
运行过程
G1 的运作过程大致可划分为以下四个步骤:
初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC 的时候同步完成的,所以G1 收集器在这个阶段实际并没有额外的停顿。
并发标记:GC线程和用户线程并发运行,标记可回收的对象,会存在漏标的情况,这个可以通过三色标记和STAB算法来解决。
最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结后仍遗留下来的最后那少量的SATB 记录(漏标对象)。
筛选回收:负责更新Region 的统计数据,对各个Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region 的存活对象复制到空的Region 中,再清理掉整个旧Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
特点:
内存要求高:G1由于要把堆内存分为不同的Region,相当于每次创建对象是在独立的Region区进行的,所以如果分配的内存空间过少,性能会迅速下降,不如使用CMS,推荐内存空间大小:6~8G以上。
并发并行:和CMS一样,充分利用了多核CPU的多线程处理,尽力缩短了STW的时间,耗时较长的标记阶段也采用并行操作。
分代收集:与其他收集器一样,分代概念在G1 中依然得以保留。虽然G1 可以不需要其他收集器配合就能独立管理整个GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC 的旧对象以获取更好的收集效果。
无内存碎片:由于采用复制/标记整理算法,所以内存空间在回收后是不会产生碎片的,但是清理的效率可能会比CMS的清理效率略低。
追求停顿时间:
-XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1 尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间,同时还可以依靠筛选回收来选择指定回收的Region区域来尽力追求配置的停顿时间。
STW(Stop the World)
STW是GC回收中不可避免的,它的意思就是GC需要暂停所有的用户线程来执行标记/清除整理操作,造成的卡顿现象就会影响用户的体验。垃圾回收器的发展也是以追求最大吞吐量和最小停顿时间(STW)为目标的,而我们所说的JVM调优就是降低MajorGC和FullGC执行频率,从而使得STW时间尽量的小。