• 我们刚刚聊过,在引用计数中,如果其引用计数器的值为0,则占用的内存会被回收掉。而在可达性分析中,如果没有某个对象没有任何引用,它也不一定会被回收掉。

    垃圾回收算法

    聊完了JVM如何判断一个对象是否需要回收,接下来我们再聊一下JVM是如何进行回收的。

    标记-清除

    顾名思义,其过程分为两个阶段,分别是标记清除。首先标记出所有需要回收的对象,然后统一对标记的对象进行回收。这个算法的十分的局限,首先标记和清除的两个过程效率都不高,而且这样的清理方式会产生大量的内存碎片,什么意思呢?

    就是虽然总体看起来还有足够的剩余内存空间,但是他们都是以一块很小的内存分散在各个地方。如果此时需要为一个大对象申请空间,即使总体上的内存空间足够,但是JVM无法找到一块这么大的连续内存空间,就会导致触发一次GC。

    复制

    其大致的思路是,将现有的内存空间分为两半A和B,所有的新对象的内存都在A中分配,然后当A用完了之后,就开始对象存活判断,将A中还存活的对象复制到B去,然后一次性将A中的内存空间回收掉。

    这样一来就不会出现使用标记-清除所造成的内存碎片的问题了。但是,它仍然有自己的不足。那就是以内存空间缩小了一半为代价,而在某些情况下,这种代价其实是很高的。

    堆中新生代就是采用的复制算法。刚刚提到过,新生代被分为了Eden、From Survivor、To Survivor,由于几乎所有的新对象都会在这里分配内存,所以Eden区比Survivor区要大很多。因此Eden区和Survivor区就不需要按照复制算法默认的1:1的来分配内存。

    在HotSpot中Eden和Survivor的比例默认是8:1,也就意味着只有10%的空间会被浪费掉。

    看到这你可能会发现一个问题。

    的确,在新生代GC时,最坏的情况就是Eden区的所有对象都是存活的,那这个JVM会怎么处理呢?这里需要引入一个概念叫做内存分配担保

    当发生了上面这种情况,新生代需要老年代的内存空间来做担保,把Survivor存放不下的对象直接存进老年代中。

    标记-整理

    标记-整理其GC的过程与标记-清楚是一样的,只不过会让所有的存活对象往同一边移动,这样一来就不会像标记-整理那样留下大量的内存碎片。

    分代收集

    这也是当前主流虚拟机所采用的算法,其实就是针对不同的内存区域的特性,使用上面提到过的不同的算法。

    例如新生代的特性是大部分的对象都是需要被回收掉的,只有少量对象会存活下来。所以新生代一般都是采用复制算法

    而老年代属于对象存活率都很高的内存空间,则采用标记-清除标记-整理算法来进行垃圾回收。

    垃圾收集器

    新生代收集器

    聊完了垃圾回收的算法,我们需要再了解一下GC具体是通过什么落地的, 也就是上面的算法的实际应用。

    Serial

    Serial采用的是复制算法的垃圾收集器,而且是单线程运作的。也就是说,当Serial进行垃圾收集时,必须要暂停其他所有线程的工作,直到垃圾收集完成,这个动作叫STW(Stop The World) 。Golang中的GC也会存在STW,在其标记阶段的准备过程中会暂停掉所有正在运行的Goroutine。

    而且这个暂停动作对用户来说是不可见的,用户可能只会知道某个请求执行了很久,没有经验的话是很难跟GC挂上钩的。

    但是从某些方面来看,如果你的系统就只有单核,那么Serial就不会存在线程之间的交互的开销,可以提高GC的效率。这也是为什么Serial仍然是Client模式下的默认新生代收集器。

    ParNew

    ParNew与Serial只有一个区别,那就是ParNew是多线程的,而Serial是单线程的。除此之外,其使用的垃圾收集算法和收集行为完全一样。

    该收集器如果在单核的环境下,其性能可能会比Serial更差一些,因为单核无法发挥多线程的优势。在多核环境下,其默认的线程与CPU数量相同。

    Parallel Scavenge

    Parallel Scavenge是一个多线程的收集器,也是在server模式下的默认垃圾收集器。上面的两种收集器关注的重点是如何减少STW的时间,而Parallel Scavenge则更加关注于系统的吞吐量

    例如JVM已经运行了100分钟,而GC了1分钟,那么此时系统的吞吐量(100 - 1)/100 = 99%

    吞吐量短停顿时间其侧重的点不一样,需要根据自己的实际情况来判断。

    高吞吐量

    GC的总时间越短,系统的吞吐量则越高。换句话说,高吞吐量则意味着,STW的时间可能会比正常的时间多一点,也就更加适合那种不存在太多交互的后台的系统,因为对实时性的要求不是很高,就可以高效率的完成任务。

    短停顿时间

    STW的时间短,则说明对系统的响应速度要求很高,因为要跟用户频繁的交互。因为低响应时间会带来较高的用户体验。

    老年代收集器

    Serial Old

    Serial Old是Serial的老年代版本,使用的标记-整理算法, 其实从这看出来,新生代和老年代收集器的一个差别。

    所以,新生代收集器基本都是用的复制算法,老年代收集器基本都是用的标记-整理算法。

    Serial Old也是给Client模式下JVM使用的。

    Parallel Old

    Parallel Old是Parallel Scavenge的老年代版本,也是一个多线程的、采用标记-整理算法的收集器,刚刚讨论过了系统吞吐量,那么在对CPU的资源十分敏感的情况下, 可以考虑Parallel Scavenge和Parallel Old这个新生代-老年代的垃圾收集器组合。

    CMS

    CMS全称(Concurrent Mark Sweep),使用的是标记-清除的收集算法。重点关注于最低的STW时间的收集器,如果你的应用非常注重与响应时间,那么就可以考虑使用CMS。

    从图中可以看出其核心的步骤:

    CMS是一个优点很明显的的垃圾收集器,例如可以多线程的进行GC,且拥有较低的STW的时间。但是同样的,CMS也有很多缺点。

    缺点

    我们开篇也提到过,使用标记-清除算法会造成不连续的内存空间,也就是内存碎片。如果此时需要给较大的对象分配空间,会发现内存不足,重新触发一次Full GC。

    其次,由于CMS可能会比注重吞吐量的收集器占用更多的CPU资源,但是如果应用程序本身就已经对CPU资源很敏感了,就会导致GC时的可用CPU资源变少,GC的整个时间就会变长,那么就会导致系统的吞吐量降低。

    G1

    G1全称Garbage First,业界目前对其评价很高,JDK9中甚至提议将其设置为默认的垃圾收集器。我们前面讲过,Parallel Scavenge更加关注于吞吐量,而CMS更加关注于更短的STW时间,那么G1就是在实现高吞吐的同时,尽可能的减少STW的时间。

    我们知道,上面聊过的垃圾收集器都会把连续的堆内存空间分为新生代、老年代,新生代则被划分的更加的细,有Eden和两个较小的Survivor空间,而且都是连续的内存空间。而G1则与众不同,它引入了新的概念,叫Region

    Region是一堆大小相等但是不连续的内存空间,同样是采用了分代的思想,但是不存在其他的收集器的物理隔离,属于新生代和老年代的region分布在堆的各个地方。

    上面H则代表大对象,也叫Humongous Object。为了防止大对象的频繁拷贝,会直接的将其放入老年代。G1相比于其他的垃圾收集器有什么特点呢?

    从宏观上来看,其采用的是标记-整理算法, 而从region到region来看,其采用的是复制算法的,所以G1在运行期间不会像CMS一样产生内存碎片。

    除此之外,G1还可以通过多个CPU,来缩短STW的时间,与用户线程并发的执行。并且可以建立可预测的停顿时间模型,让使用者知道在某个时间片内,消耗在GC上的时间不得超过多少毫秒。之所以G1能够做到这点,是因为没像其余的收集器一样收集整个新生代和老年代,而是在有计划的避免对整个堆进行全区域的垃圾收集。

    总结

    这个图来自于参考中的博客,总结的很到位。

    参考

    拜了个拜
    浅谈JVM和垃圾回收-LMLPHP

    本文使用 mdnice 排版

    07-03 14:40