详解JVM垃圾收集算法&垃圾收集器(图解)

垃圾收集算法:内存回收的方法论

垃圾收集器:内存回收的具体实现


在正式讨论垃圾回收算法和垃圾收集器以前,咱们应该了解一下JVM是如何判断一个对象已经死亡的?
JVM主要使用了两种方法判断对象是否已经死亡:
  1. 引用计数法:这种方法实现起来简单,并且也好理解,就是给对象维护了一个计数器,当有一个地方引用它的时候,它就加一,当引用失效的时候计数器就减一。当这个计数器的值为0的时候,那么就能够判断该对象能够被清理。
  2. 可达性分析算法:该方法的基本思想就是经过一些”GC Roots” 对象做为起始点,从这些节点开始向下搜索。就像一棵树结构同样,从它的根节点开始搜索,若某些对象不属于这些树的子节点,那么就能够断定这些对象为不可达。能够被清理。(在咱们下面介绍的垃圾回收算法中,主要是使用了这种方法判断对象是否须要被回收)
    注:固然,实际的判断方法没有这么简单,这里只是简单的介绍一下。

OK,接下来先说垃圾回收算法

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

    该算法就和他的名字同样,分为标记和清除两个阶段。首先标记出全部须要回收的对象,在标记完成以后统一回收全部被标记的对象。算法

图解:

回收前 (黑色:可回收 | 灰色:存活对象 | 白色:未使用 )
这里写图片描述多线程

回收后:
这里写图片描述并发

从上面的图解中咱们就能够看出:
1. 这种算法在收集结束后,内存是乱七八糟的(也就是产生了不少内存碎片),这种内存碎片太多的话,会致使咱们在下一次分配较大对象的时候,没法找到足够的连续内存,不得不触发另一次垃圾回收。
2. 还有就是其实,标记和清理的过程的效率都是不高的。jvm

该算法适合于那些对象存活率较高的内存区域的收集工做。
2. 复制算法(Copying)

    该算法的出现是为了解决效率问题。它将内存划分为等大的两份,每次只是使用其中的一块。当一块用完了,就将还活着的对象复制到另一块上,完后再将已经使用过的那一块一次性清除。布局

图解:

回收前 (黑色:可回收 | 浅灰色:存活对象 | 白色:未使用 | 深灰色:保留区域 )
这里写图片描述
回收后:
这里写图片描述优化

算法优势:这种算法不存在出现垃圾碎片的状况,再分配较大对象时候也不会有没法找到足够内存的状况,实现简单,运行高效。
算法缺点:使用该算法的代价就是直接将内存缩小一半,代价有点高。其次,再某些状况下须要进行很大规模的复制动做,会直接影响到效率。线程

该算法适合于那些对象存活率较低的内存区域的收集工做。
3. 标记整理算法(Mark Compact)

    标记整理和标记清除的很是类似,可是标记整理的过程是这样的,首先是标记要清理的对象,而后将剩下全部存活的对象都移动到一端,而后直接清理端边界之外的内存。其实也就是标记-整理-清除算法,多了一个对内存的整理的过程。server

图解:

回收前回收前 (黑色:可回收 | 灰色:存活对象 | 白色:未使用 )
这里写图片描述
回收后:
这里写图片描述对象

你们能够在图示中很清楚的看到,该种收集算法的好处就是首先没有像复制算法那样浪费掉一半的空间,而后收集后的内存也很是的整洁,没有内存碎片。
相比标记清除,它能很好的整理内存,但同时也多了整理内存的代价。blog

该算法适合于那些对象存活率较高的内存区域的收集工做。
4. 分代收集算法(Generational Collection)

    这种算法其实并无什么新的思想,只是根据对象存活周期的不一样将内存划分为几块。就是将Java堆划分为新生代和老年代,新生代中每次收集都会有大量的对象死去,因此建议采用复制算法,只须要付出较少的复制成本就能完成收集。可是老年代中由于对象的存活率高且没有额外的空间对它进行分配担保。因此虽好,使用标记-清理或者标记整理算法来进行收集。


接下来就是垃圾收集器了

这里写图片描述

    上图所示的就是接下来要介绍的集中垃圾收集器,上图中用线链接起来的两个垃圾收集器之间是能够搭配使用的。

1. Serial:基本、历史悠久

    该收集器是一个单线程的收集器。这里的单线程收集器并非说明它只会使用一个CPU或者使用一条线程去完成垃圾的收集工做,更为重要的是在它进行垃圾收集的时候须要”Stop the World”直到它的工做结束。
优势:简单高效、在单个CPU环境来讲,Serial没有线程交互的开销,专心作天然高效率。
缺点:Stop the World 影响用户体验!

运行图示

这里写图片描述

2. ParNew:Serial的多线程版本

    该线程除了使用多条线程进行垃圾收集以外,其他的东西和行为和Serial是如出一辙的。虽说没有多大的创新,可是倒是许多运行在Server模式下虚拟机目前首选的新生代垃圾收集器,其主要缘由是,它能够于另一个吊炸天的老年代的垃圾收集器CMS配合使用。
优势:一样具有Serial的优势。
缺点:Stop the World 影响用户体验。

运行图示

这里写图片描述

插入:在谈论垃圾收集器色上下文语境中,并发并行的理解以下:

并行:多条垃圾收集线程并行工做,但此时的用户线程仍处于等待状态。
并发:用户线程和垃圾收集线程同时处理,用户线程在继续运行、而垃圾收集程序运行在另外一个CPU。

3. Parallel Scavenge:做用于新生代、使用复制算法、多线程

    从表面看,该收集器和ParNew是很是类似的,但实际上是有所区别的。他们主要的区别在于他们的关注点不一样,CMS或者ParNew等收集器的关注点主要在于缩短垃圾收集时用户线程的停顿时间(缩短 Stop The World的时间),可是Parallel Scavenge的关注点是去达到一个能够控制的吞吐量。(吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间))。
    分析:停顿的时间越短就越适合须要与用户交互的程序,可是吞吐量高则能够高效的利用CPU时间,尽快完成程序的任务,主要适合在后台运算而不须要太多交互的任务。

4. Serial Old:Serial的老年代版本、单线程、标记-整理算法
    工做原理与Serial基本一致。主要做用于Client端。在server端主要有两种用途:

    1. JDK1.5以前,与Parallel Scavenge搭配使用
    2. 做为CMS收集器的后备预案,并发收集发生Concurrent Mode Failure时使用。

运行图示

这里写图片描述

5. Parallel Old收集器:Parallel Scavenge的老年版、多线程、标记-整理、JDK1.6中开始加入
    主要是搭配Parallel Scavenge使用,可使得整个系统的吞吐量达到最大化的优化,在注重吞吐量和CPU资源敏感的场合,能够优先考虑Parallel套件。
运行图示

这里写图片描述

6. CMS收集器(Concurrent Mark Sweap):并发收集、低停顿、标记-清除、JDK1.6中加入

该收集器意在获取最短的停留时间,多应用在B/S结构的服务端上,该收集器的运做过程。

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 从新标记(CMS remark)
  4. 并发清除(CMS concurrent sweap)

    在上述的四个过程当中:并发标记和从新标记两个步骤仍是须要”Stop The World”。初始标记只是标记一下 GCRoots 不能直接关联到的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing 的过程。并发标记就至关于再次去判断该对象是否已经死亡的过程,由于对象是很是可能”复活”的。
    其中的并发标记和并发清除步骤:从整体上来讲是和用户一块儿来工做的。因此,这也就是它最大的优势。

    可是它并不完美,它主要有如下三个缺点:
1.对CPU资源敏感

    CMS默认启动的回收线程数是 (CPU数量+3)/4,也就是CPU在4个以上时,并发回收时垃圾线程很多于25%的CPU资源,而且随着CPU数的上升而降低。可是,当CPU数不足4个时,好比2个,CMS对用户程序的影响就比较大了,CPU须要分出一半以上的资源供给垃圾回收,用户的程序执行速度会下降50%以上。为了应对这种状况发生,虚拟机提供了一种称为”增量式并发收集器”(Incremental Concurrent Mark Sweap / i-CMS)的变种CMS收集器,但在实际应用中效果不理想,如今已经不提倡使用。

2.没法处理浮动垃圾,可能会出现(Concurrent Mode Failure)失败而致使的另外一次Full GC产生。

    因为CMS在并发清理阶段用户的线程仍在运行。伴随着程序的运行指定会产生新的垃圾,这种垃圾出如今标记过程以后,CMS没法在当此处理中处理掉他们,这些就是浮动垃圾。一样由于,在清理过程当中用户线程也得跑,就须要预留有足够的内存空间给用户使用。若是运行的过程当中的预留的空间不够使用,那么就会发生(Concurrent Mode Failure),触发一次Full GC,临时启用 Serial Old收集器从新对老年代进行收集。这样的停顿时间过长。

3.会产生大量的内存碎片。

    因为该收集器使用的标记-清除收集算法,就会不可避免的产生大量的内存碎片。给较大的对象分配带来了很大的麻烦。

运行图示:

这里写图片描述

7. G1收集器:当今收集器技术发展最前沿的成果之一
它具备以下特色:

    1.并发与并行:G1充分利用多CPU,多核环境下的硬件优点,使用多个CPU来缩短 Stop-The-World停顿的时间,其它收集器须要停顿Java线程执行的GC动做,G1仍然能够经过并发的方式让Java程序继续执行。
    2.分代收集:分代的概念在G1中仍然得以保留。虽然G1能够不须要其它收集器的配合就能独立管理整个GC堆。
    3.空间整合:与CMS的标记清除不一样,G1从总体上看是基于“标记清理”算法实现的收集器。从局部(两个Region)之间上来看是基于“复制”算法实现的。不管如何,结论就是G1运行期间不会产生内存碎片。
    4.可预测的停顿:G1除了追求低停顿以外,还能创建可预测的停顿时间模型,可让使用者明确指定一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不能超过N毫秒。

内存布局的变化:

    G1以前的其它收集器进行收集的范围都是整个新生代或者老年代,可是G1再也不这样。—->他将整个Java堆划分为多个大小相等的独立区域(Region),虽然还有新生代和老年代的概念,可是新生代和老年代再也不是物理隔离,他们都是一部分Region的集合。

G1作了什么?让它具备了能创建可预测的停顿时间模型?

    首先,G2会跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需的时间的经验值),在后台维护一个优先列表,每侧根据容许的收集时间,优先收集经验值较大的Region。这种具备必定的优先级的回收策略,使得G1在有效的时间里能够得到尽量大的回收效率。
    Java堆分为多个Region以后,那么在处理对象间依赖这个问题就天然而然的出现了。在G1中,Region之间的对象引用还有其它版本收集器中的新生代和老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。

具体使用 Remembered Set 怎么实现避免全堆扫描?

    G1中每一个Region中都有一个与之对应的Remembered Set ,虚拟机发现程序在对 Reference类型的数据进行写操做时。会产生一个 Write Barrier 暂时中断操做,检查 Reference 引用的对象是否处于不一样的Region中,若是是,便经过 CardTable 吧相关的引用信息记录到被引用对象的Region的 Remembered Set 中。当有GC操做时,在GC Roots 的枚举范围中加入 Remembered Set 便可保证不对全堆扫描,且不会有遗漏。

G1收集器运行的大概过程(忽略维护 Remembered Set)
  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

· 初始标记仅仅只能标记一下 GC Roots节点能直接关联到的对象。
· 并发标记是从 GC Roots 开始对堆中的对象进行可达性分析,找出存活的对象,这阶段耗时较长,可是能够用户程序并发执行。
· 最终标记是为了修正在并发标记过程当中由于用户程序继续运做而致使标记产生变更的那一部分记录。JVM将变更记录在 Remembered Set Logs中,最终将 Remembered Set LogsRemembered Set 合并。虽需停顿,可是可并行执行。
· 筛选回收,首先对各个Rgion的回收价值和成本进行排序,根据用户所指望的GC停顿时间制定回收计划。

运行图示

这里写图片描述

终:上面就是我要说的垃圾收集器和垃圾回收算法,若是有问题还需大神指出。
相关文章
相关标签/搜索