垃圾回收算法与 JVM 垃圾回收器综述

 

咱们常说的垃圾回收算法能够分为两部分:对象的查找算法与真正的回收方法。不一样回收器的实现细节各有不一样,但总的来讲基本全部的回收器都会关注以下两个方面:找出全部的存活对象以及清理掉全部的其它对象——也就是那些被认为是废弃或无用的对象。Java 虚拟机规范中对垃圾收集器应该如何实现并无任何规定,所以不一样的厂商、不一样版本的虚拟机所提供的垃圾收集器均可能会有很大差异,而且通常都会提供参数供用户根据本身的应用特色和要求组合出各个年代所使用的收集器。其中最主流的四个垃圾回收器分别是:一般用于单 CPU 环境的 Serial GC、Throughput/Parallel GC、CMS GC、G1 GC。java

当咱们在讨论垃圾回收器时,每每也会涉及到不少的概念;譬如并行(Parallel)与并发(Concurrent)、Minor GC 与 Major / Full GC。并行指多条垃圾收集线程并行工做,但此时用户线程仍然处于等待状态;并发指用户线程与垃圾收集线程同时执行(但不必定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另外一个CPU上。Minor GC 指发生在新生代的垃圾收集动做,由于Java对象大多都具有朝生夕灭的特性,因此Minor GC很是频繁,通常回收速度也比较快;Major GC 指发生在老年代的GC,出现了Major GC,常常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程),Major GC的速度通常会比Minor GC慢10倍以上。从不一样角度分析垃圾回收器,能够将其分为不一样的类型:面试

分类标准 描述
线程数 分为串行垃圾回收器和并行垃圾回收器。串行垃圾回收器一次只使用一个线程进行垃圾回收;并行垃圾回收器一次将开启多个线程同时进行垃圾回收。在并行能力较强的 CPU 上,使用并行垃圾回收器能够缩短 GC 的停顿时间。
工做模式 分为并发式垃圾回收器和独占式垃圾回收器。并发式垃圾回收器与应用程序线程交替工做,以尽量减小应用程序的停顿时间;独占式垃圾回收器 (Stop the world) 一旦运行,就中止应用程序中的其余全部线程,直到垃圾回收过程彻底结束。
碎片处理方式 分为压缩式垃圾回收器和非压缩式垃圾回收器。压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片;非压缩式的垃圾回收器不进行这步操做。
工做的内存区间 新生代垃圾回收器和老年代垃圾回收器

咱们最经常使用的评价垃圾回收器的指标就是吞吐量与停顿时间,停顿时间越短就越适合须要与用户交互的程序,良好的响应速度能提高用户的体验;而高吞吐量则能够最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务;具体的指标列举以下:算法

  • 吞吐量:指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值。系统总运行时间=应用程序耗时+GC 耗时。若是系统运行了 100min,GC 耗时 1min,那么系统的吞吐量就是 (100-1)/100=99%。缓存

  • 垃圾回收器负载:和吞吐量相反,垃圾回收器负载指来记回收器耗时与系统运行总时间的比值。安全

  • 停顿时间:指垃圾回收器正在运行时,应用程序的暂停时间。对于独占回收器而言,停顿时间可能会比较长。使用并发的回收器时,因为垃圾回收器和应用程序交替运行,程序的停顿时间会变短,可是,因为其效率极可能不如独占垃圾回收器,故系统的吞吐量可能会较低。服务器

  • 垃圾回收频率:指垃圾回收器多长时间会运行一次。通常来讲,对于固定的应用而言,垃圾回收器的频率应该是越低越好。一般增大堆空间能够有效下降垃圾回收发生的频率,可是可能会增长回收产生的停顿时间。数据结构

  • 反应时间:指当一个对象被称为垃圾后多长时间内,它所占据的内存空间会被释放。多线程

  • 堆分配:不一样的垃圾回收器对堆内存的分配方式多是不一样的。一个良好的垃圾回收器应该有一个合理的堆内存区间划分。架构

在对象查找算法的帮助下咱们能够找到内存能够被使用的,或者说那些内存是能够回收,更多的时候咱们确定愿意作更少的事情达到一样的目的。并发

想了解Java工程化、高性能及分布式、高性能、性能调优、Spring,MyBatis,Netty源码分析学习的能够看过来。

一、具备1-5工做经验的,面对目前流行的技术不知从何下手,须要突破技术瓶颈的能够加群。

二、在公司待久了,过得很安逸,但跳槽时面试碰壁。须要在短期内进修、跳槽拿高薪的能够加群。

三、若是没有工做经验,但基础很是扎实,对java工做机制,经常使用设计思想,经常使用java开发框架掌握熟练的,能够加群。

四、以为本身很牛B,通常需求都能搞定。可是所学的知识点没有系统化,很难在技术领域继续突破的能够加群。

5. 群号:高级架构群 283943715备注好信息!

6.阿里Java高级架构师免费直播讲解知识点,分享知识,多年工做经验的梳理和总结,带着你们全面、科学地创建本身的技术体系和技术认知!

对象引用

在 JDK 1.2 之前的版本中,若一个对象不被任何变量引用,那么程序就没法再使用这个对象。也就是说,只有对象处于可触及(Reachable)状态,程序才能使用它。从 JDK 1.2 版本开始,把对象的引用分为 4 种级别,从而使程序能更加灵活地控制对象的生命周期。这 4 种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

StrongReference: 强引用

强引用是使用最广泛的引用。若是一个对象具备强引用,那垃圾回收器毫不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具备强引用的对象来解决内存不足的问题。好比下面这段代码:

 public class Main {

当运行至 Object[] objArr = new Object[1000]; 这句时,若是内存不足,JVM 会抛出 OOM 错误也不会回收 object 指向的对象。不过要注意的是,当 fun1 运行完以后,object 和 objArr 都已经不存在了,因此它们指向的对象都会被 JVM 回收。若是想中断强引用和某个对象之间的关联,能够显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。好比 Vector 类的 clear 方法中就是经过将引用赋值为 null 来实现清理工做的:

/**

     * Removes the element at the specified position in this Vector.

     * Shifts any subsequent elements to the left (subtracts one from their

     * indices).  Returns the element that was removed from the Vector.

     *

     * @throws ArrayIndexOutOfBoundsException if the index is out of range

     *         ({@code index < 0 || index >= size()})

     * @param index the index of the element to be removed

     * @return element that was removed

     * @since 1.2

     */

    public synchronized E remove(int index) {

    modCount++;

    if (index >= elementCount)

        throw new ArrayIndexOutOfBoundsException(index);

    Object oldValue = elementData[index];

 

    int numMoved = elementCount - index - 1;

    if (numMoved > 0)

        System.arraycopy(elementData, index+1, elementData, index,

                 numMoved);

    elementData[--elementCount] = null; // Let gc do its work

 

    return (E)oldValue;

    }

 

SoftReference: 软引用

软引用是用来描述一些有用但并非必需的对象,在 Java 中用 java.lang.ref.SoftReference 类来表示。对于软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。所以,这一点能够很好地用来解决 OOM 的问题,而且这个特性很适合用来实现缓存:好比网页缓存、图片缓存等。软引用能够和一个引用队列(ReferenceQueue)联合使用,若是软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。下面是一个使用示例:

import java.lang.ref.SoftReference;

 

public class Main {

    public static void main(String[] args) {

 

        SoftReference<String> sr = new SoftReference<String>(new String("hello"));

        System.out.println(sr.get());

    }

}

 

WeakReference: 弱引用

弱引用与软引用的区别在于:只具备弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程当中,一旦发现了只具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。不过,因为垃圾回收器是一个优先级很低的线程,所以不必定会很快发现那些只具备弱引用的对象。

import java.lang.ref.WeakReference;

 

public class Main {

    public static void main(String[] args) {

 

        WeakReference<String> sr = new WeakReference<String>(new String("hello"));

 

        System.out.println(sr.get());

        System.gc();                //通知JVM的gc进行垃圾回收

        System.out.println(sr.get());

    }

}

 

输出结果为:

 

第二个输出结果是 null,这说明只要 JVM 进行垃圾回收,被弱引用关联的对象一定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,若是存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。弱引用能够和一个引用队列(ReferenceQueue)联合使用,若是弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

PhantomReference: 虚引用

“虚引用”顾名思义,就是形同虚设,与其余几种引用都不一样,虚引用并不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收器回收。虚引用和前面的软引用、弱引用不一样,它并不影响对象的生命周期。在 Java 中用 java.lang.ref.PhantomReference 类表示。若是一个对象与虚引用关联,则跟没有引用与之关联同样,在任什么时候候均可能被垃圾回收器回收。要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,若是发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序能够经过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。若是程序发现某个虚引用已经被加入到引用队列,那么就能够在所引用的对象的内存被回收以前采起必要的行动。

 

对象存活性判断

经常使用的对象存活性判断方法有引用计数法与可达性分析,不过因为引用计数法没法解决对象循环引用的问题,所以主流的 JVM 倾向于使用可达性分析。

Reference Counting: 引用计数

引用计数器在微软的 COM 组件技术中、Adobe 的 ActionScript3 种都有使用。引用计数器的原理很简单,对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1,当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,则对象 A 就不可能再被使用。引用计数器的实现也很是简单,只须要为每一个对象配置一个整形的计数器便可。可是引用计数器有一个严重的问题,即没法处理循环引用的状况。所以,在 Java 的垃圾回收器中没有使用这种算法。一个简单的循环引用问题描述以下:有对象 A 和对象 B,对象 A 中含有对象 B 的引用,对象 B 中含有对象 A 的引用。此时,对象 A 和对象 B 的引用计数器都不为 0。可是在系统中却不存在任何第 3 个对象引用了 A 或 B。也就是说,A 和 B 是应该被回收的垃圾对象,但因为垃圾对象间相互引用,从而使垃圾回收器没法识别,引发内存泄漏。

0?wx_fmt=png

引用树遍历

所谓的引用树本质上是有根的图结构,它沿着对象的根句柄向下查找到活着的节点,并标记下来;其他没有被标记的节点就是死掉的节点,这些对象就是能够被回收的,或者说活着的节点就是能够被拷贝走的,具体要看所在 HeapSize中 的区域以及算法,它的大体示意图以下图所示(注意这里是指针是单向的):

0?wx_fmt=jpeg

首先,全部回收器都会经过一个标记过程来对存活对象进行统计。JVM 中用到的全部现代 GC 算法在回收前都会先找出全部仍存活的对象。下图中所展现的JVM中的内存布局能够用来很好地阐释这一律念:

0?wx_fmt=jpeg

而所谓的GC根对象包括:当前执行方法中的全部本地变量及入参、活跃线程、已加载类中的静态变量、JNI 引用。接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从 GC 根对象开始,而后是根对象引用的其它对象,好比实例变量。回收器将访问到的全部对象都标记为存活。存活对象在上图中被标记为蓝色。当标记阶段完成了以后,全部的存活对象都已经被标记完了。其它的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。

不过那些发现不能到达 GC Roots 的对象并不会当即回收,在真正回收以前,对象至少要被标记两次。当第一次被发现不可达时,该对象会被标记一次,同时调用此对象的 finalize()方法(若是有);在第二次被发现不可达后,对象被回收。利用 finalisze() 方法,对象能够逃离一次被回收的命运,可是只有一次。逃命方法以下,须要在 finalize() 方法中给本身加一个 GCRoots 中的 hook:

 

通用垃圾回收算法

算法名 优点 缺陷
Mark-Sweep / 标记-清除 简单 效率低下且会产生不少不连续内存,分配大对象时,容易提早引发另外一次垃圾回收。
Copying / 复制 效率较高,不用考虑内存碎片化 存在空间浪费
Mark-Compact / 标记-整理 避免了内存碎片化 GC 暂停时间增加

Mark-Sweep: 标记-清除算法

0?wx_fmt=jpeg

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段首先经过根节点,标记全部从根节点开始的较大对象。所以,未被标记的对象就是未被引用的垃圾对象。而后,在清除阶段,清除全部未被标记的对象。该算法最大的问题是存在大量的空间碎片,由于回收后的空间是不连续的。在对象的堆空间分配过程当中,尤为是大对象的内存分配,不连续的内存空间的工做效率要低于连续的空间。

从概念上来说,标记-清除算法使用的方法是最简单的,只须要忽略这些对象即可以了。也就是说当标记阶段完成以后,未被访问到的对象所在的空间都会被认为是空闲的,能够用来建立新的对象。这种方法须要使用一个空闲列表来记录全部的空闲区域以及大小。对空闲列表的管理会增长分配对象时的工做量。这种方法还有一个缺陷就是——虽然空闲区域的大小是足够的,但却可能没有一个单一区域可以知足此次分配所需的大小,所以本次分配仍是会失败(在Java中就是一次 OutOfMemoryError)。

Copying: 复制算法

0?wx_fmt=jpeg

将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,以后,清除正在使用的内存块中的全部对象,交换两个内存的角色,完成垃圾回收。若是系统中的垃圾对象不少,复制算法须要复制的存活对象数量并不会太大。所以在真正须要垃圾回收的时刻,复制算法的效率是很高的。又因为对象在垃圾回收过程当中统一被复制到新的内存空间中,所以,可确保回收后的内存空间是没有碎片的。该算法的缺点是将系统内存折半。

Java 的新生代串行垃圾回收器中使用了复制算法的思想。新生代分为 eden 空间、from 空间、to 空间 3 个部分。其中 from 空间和 to 空间能够视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。from 和 to 空间也称为 survivor 空间,即幸存者空间,用于存放未被回收的对象。在垃圾回收时,eden 空间中的存活对象会被复制到未使用的 survivor 空间中 (假设是 to),正在使用的 survivor 空间 (假设是 from) 中的年轻对象也会被复制到 to 空间中 (大对象,或者老年对象会直接进入老年带,若是 to 空间已满,则对象也会直接进入老年代)。此时,eden 空间和 from 空间中的剩余对象就是垃圾对象,能够直接清空,to 空间则存放这次回收后的存活对象。这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间浪费。

标记-复制算法与标记-整理算法很是相似,它们都会将全部存活对象从新进行分配。区别在于从新分配的目标地址不一样,复制算法是为存活对象分配了另外的内存 区域做为它们的新家。标记复制算法的优势在于标记阶段和复制阶段能够同时进行。它的缺点是须要一块能容纳下全部存活对象的额外的内存空间。

Mark-Compact: 标记-压缩算法

0?wx_fmt=jpeg

复制算法的高效性是创建在存活对象少、垃圾对象多的前提下的。这种状况在年轻代常常发生,可是在老年代更常见的状况是大部分对象都是存活对象。若是依然使用复制算法,因为存活的对象较多,复制的成本也将很高。

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上作了一些优化。也首先须要从根节点开始对全部可达对象作一次标记,但以后,它并不简单地 清理未标记的对象,而是将全部的存活对象压缩到内存的一端。以后,清理边界外全部的空间。这种方法既避免了碎片的产生,又不须要两块相同的内存空间,所以,其性价比比较高。

标记-压缩算法修复了标记-清除算法的短板——它将全部标记的也就是存活的对象都移动到内存区域的开始位置。这种方法的缺点就是GC暂停的时间会增 长,由于你须要将全部的对象都拷贝到一个新的地方,还得更新它们的引用地址。相对于标记-清除算法,它的优势也是显而易见的——通过整理以后,新对象的分 配只须要经过指针碰撞便能完成(pointer bumping),至关简单。使用这种方法空闲区域的位置是始终可知的,也不会再有碎片的问题了。

Incremental Collecting: 增量回收算法

在垃圾回收过程当中,应用软件将处于一种 CPU 消耗很高的状态。在这种 CPU 消耗很高的状态下,应用程序全部的线程都会挂起,暂停一切正常的工做,等待垃圾回收的完成。若是垃圾回收时间过长,应用程序会被挂起好久,将严重影响用户体验或者系统的稳定性。

增量算法现代垃圾回收的一个前身,其基本思想是,若是一次性将全部的垃圾进行处理,须要形成系统长时间的停顿,那么就可让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,因为在垃圾回收过程当中,间断性地还执行了应用程序代码,因此能减小系统的停顿时间。可是,由于线程切换和上下文转换的消耗,会使得垃圾回收的整体成本上升,形成系统吞吐量的降低。

Generational Collecting: 分代回收算法

分代回收器是增量收集的另外一个化身,根据垃圾回收对象的特性,不一样阶段最优的方式是使用合适的算法用于本阶段的垃圾回收,分代算法便是基于这种思想,它将内存区间根据对象的特色分红几块,根据 每块内存区间的特色,使用不一样的回收算法,以提升垃圾回收的效率。以 Hot Spot 虚拟机为例,它将全部的新建对象都放入称为年轻代的内存区域,年轻代的特色是对象会很快回收,所以,在年轻代就选择效率较高的复制算法。当一个对象通过几 次回收后依然存活,对象就会被放入称为老生代的内存空间。在老生代中,几乎全部的对象都是通过几回垃圾回收后依然得以幸存的。所以,能够认为这些对象在一 段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。若是依然使用复制算法回收老生代,将须要复制大量对象。再加上老生代的回收性价比也要低于新 生代,所以这种作法也是不可取的。根据分代的思想,能够对老年代的回收使用与新生代不一样的标记-压缩算法,以提升垃圾回收效率。

Concurrent Collecting: 并发回收算法

所谓的并发回收算法便是指垃圾回收器与应用程序可以交替工做,并发回收 器其实也会暂停,可是时间很是短,它并不会在从开始回收寻找、标记、清楚、压缩或拷贝等方式过程彻底暂停服务,它发现有几个时间比较长,一个就是标记,因 为这个回收通常面对的是老年代,这个区域通常很大,而通常来讲绝大部分对象应该是活着的,因此标记时间很长,还有一个时间是压缩,可是压缩并不必定非要每 一次作完GC都去压缩的,而拷贝呢通常不会用在老年代,因此暂时不考虑;因此他们想出来的办法就是:第一次短暂停机是将全部对象的根指针找到,这个很是容 易找到,并且很是快速,找到后,此时GC开始从这些根节点标记活着的节点(这里能够采用并行),而后待标记完成后,此时可能有新的 内存申请以及被抛弃(java自己没有内存释放这一律念),此时JVM会记录下这个过程当中的增量信息,而对于老年代来讲,必需要通过屡次在 survivor倒腾后才会进入老年代,因此它在这段时间增量通常来讲会很是少,并且它被释放的几率前面也说并不大(JVM若是不是彻底作Cache,自 己作pageCache并且发生几率不大不小的pageout和pagein是不适合的);JVM根据这些增量信息快速标记出内部的节点,也是很是快速 的,就能够开始回收了,因为须要杀掉的节点并很少,因此这个过程也很是快,压缩在必定时间后会专门作一次操做,有关暂停时间在Hotspot版本,也就是 SUN的jdk中都是能够配置的,当在指定时间范围内没法回收时,JVM将会对相应尺寸进行调整,若是你不想让它调整,在设置各个区域的大小时,就使用定 量,而不要使用比例来控制;当采用并发回收算法的时候,通常对于老年代区域,不会等待内存小于10%左右的时候才会发起回收,由于并发回收是容许在回收的 时候被分配,那样就有可能来不及了,因此并发回收的时候,JVM可能会在68%左右的时候就开始启动对老年代GC了。

JVM 垃圾回收器对比

0?wx_fmt=jpeg

1999 年随 JDK1.3.1 一块儿来的是串行方式的 Serial GC,它是第一款垃圾回收器;此后,JDK1.4 和 J2SE1.3 相继发布。2002 年 2 月 26 日,J2SE1.4 发布;Parallel GC 和Concurrent Mark Sweep (CMS)GC 跟随 JDK1.4.2 一块儿发布,而且 Parallel GC 在 JDK6 以后成为 HotSpot 默认 GC。这三个垃圾回收器也是各有千秋,Serial GC 适合最小化地使用内存和并行开销的场景、Parallel GC 适合最大化应用程序吞吐量的场景、CMS GC 适合最小化中断或停顿时间的场景。上图即展现了多种垃圾回收器之间的关系;不过随着应用程序所应对的业务愈来愈庞大、复杂,用户愈来愈多,没有合适的回收器就不能保证应用程序正常进行,而常常形成 STW 停顿的回收器又跟不上实际的需求,因此才会不断地尝试对搜集器进行优化。Garbage First(G1)GC 正是面向这种业务需求所生,它是一个并行回收器,把堆内存分割为不少不相关的区间(Region);每一个区间能够属于老年代或者年轻代,而且每一个年龄代区间能够是物理上不连续的。

名称 做用域 算法 特性 设置
Serial Serial GC 做用于新生代,Serial Old GC 做用于老年代垃圾收集 两者皆采用了串行回收与 "Stop-the-World",Serial 使用的是复制算法,而 Serial Old 使用的是电俄式-标记压缩算法 基于串行回收的垃圾回收器适用于大多数对于暂停时间要求不高的 Client 模式下的 JVM 使用 -XX:+UserSerialGC手动指定使用 Serial 回收器执行内存回收任务
Throughput/Parallel Parallel 做用于新生代,Parallel Old 做用于老年代 并行回收和 "Stop-the-World",Parallel 使用的是复制算法,Parallel Old 使用的是标记-压缩算法 程序吞吐量优先的应用场景中,在 Server 模式下内存回收的性能较为不错 使用 -XX:+UseParallelGC 手动指定使用 Parallel 回收器执行内存回收任务
CMS,Concurrent-Mark-Sweep 老年代垃圾回收器,又称做 Mostly-Concurrent 回收器 使用了标记清除算法,分为初始标记( Initial-Mark,Stop-the-World )、并发标记( Concurrent-Mark )、再次标记( Remark,Stop-the-World )、并发清除( Concurrent-Sweep ) 并发低延迟,吞吐量较低。通过CMS收集的堆会产生空间碎片,会带来堆内存的浪费 使用 -XX:+UseConcMarkSweepGC来手动指定使用 CMS 回收器执行内存回收任务
G1,Garbage First 没有采用传统物理隔离的新生代和老年代的布局方式,仅仅以逻辑上划分为新生代和老年代,选择的将 Java 堆区划分为 2048 个大小相同的独立 Region 块 使用了标记压缩算法 基于并行和并发、低延迟以及暂停时间更加可控的区域化分代式服务器类型的垃圾回收器 使用 -XX:UseG1GC 来手动指定使用 G1 回收器执行内存回收任务

关于标记阶段有几个关键点是值得注意的:

  • 开始进行标记前,须要先暂停应用线程,不然若是对象图一直在变化的话是没法真正去遍历它的。暂停应用线程以便 JVM 能够尽情地收拾家务的这种状况又被称之为安全点(Safe Point),这会触发一次Stop The World(STW)暂停。触发安全点的缘由有许多,但最多见的应该就是垃圾回收了。

  • 暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。所以,调高堆的大小并不会影响到标记阶段的时间长短。

当标记阶段完成后,GC开始进入下一阶段,删除不可达对象。

Serial GC

串行回收器主要有两个特色:第一,它仅仅使用单线程进行垃圾回收;第二,它独占式的垃圾回收。在串行回收器进行垃圾回收时,Java 应用程序中的线程都须要暂停,等待垃圾回收的完成,这样给用户体验形成较差效果。虽然如此,串行回收器倒是一个成熟、通过长时间生产环境考验的极为高效的 回收器。新生代串行处理器使用复制算法,实现相对简单,逻辑处理特别高效,且没有线程切换的开销。在诸如单 CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合,它的性能表现能够超过并行回收器和并发回收器。在 HotSpot 虚拟机中,使用-XX:+UseSerialGC 参数能够指定使用新生代串行回收器和老年代串行回收器。当 JVM 在 Client 模式下运行时,它是默认的垃圾回收器。老年代串行回收器使用的是标记-压缩算法。和新生代串行回收器同样,它也是一个串行的、独占式的垃圾回收器。因为老年代垃圾回收一般会使用比新生代垃圾回 收更长的时间,所以,在堆空间较大的应用程序中,一旦老年代串行回收器启动,应用程序极可能会所以停顿几秒甚至更长时间。虽然如此,老年代串行回收器能够 和多种新生代回收器配合使用,同时它也能够做为 CMS 回收器的备用回收器。若要启用老年代串行回收器,能够尝试使用如下参数:-XX:+UseSerialGC: 新生代、老年代都使用串行回收器。

Serial GC 的工做步骤以下所示:

0?wx_fmt=jpeg

ParNew GC

并行回收器是工做在新生代的垃圾回收器,它只简单地将串行回收器多线程化。它的回收策略、算法以及参数和串行回收器同样。

并行回收器 也是独占式的回收器,在收集过程当中,应用程序会所有暂停。但因为并行回收器使用多线程进行垃圾回收,所以,在并发能力比较强的 CPU 上,它产生的停顿时间要短于串行回收器,而在单 CPU 或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,因为多线程的压力,它的实际表现极可能比串行回收器差。开启并行回收器可使用参数-XX:+UseParNewGC,该参数设置新生代使用并行回收器,老年代使用串行回收器。老年代的并行回收回收器也是一种多线程并发的回收器。和新生代并行回收回收器同样,它也是一种关注吞吐量的回收器。老年代并行回收回收器使用标记-压缩算法,JDK1.6 以后开始启用。

Parallel GC

Parallel Scavenge 收集器的特色是它的关注点与其余收集器不一样,CMS 等收集器的关注点尽量地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的。使用 -XX:+UseParallelOldGC能够在新生代和老生代都使用并行回收回收器,这是一对很是关注吞吐量的垃圾回收器组合,在对吞吐量敏感的系统中,能够考虑使用。参数 -XX:ParallelGCThreads 也能够用于设置垃圾回收时的线程数量。

Parallel GC 的工做步骤以下所示:

0?wx_fmt=jpeg

CMS GC

CMS( Concurrent Mark-Sweep ) 是以牺牲吞吐量为代价来得到最短回收停顿时间的垃圾回收器,适用于对停顿比较敏感,而且有相对较多存活时间较长的对象(老年代较大)的应用程序;不过 CMS 虽然减小了回收的停顿时间,可是下降了堆空间的利用率。CMS GC 采用了 Mark-Sweep 算法,所以通过CMS收集的堆会产生空间碎片;为了解决堆空间浪费问题,CMS回收器再也不采用简单的指针指向一块可用堆空间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当 JVM 分配对象空间的时候,会搜索这个列表找到足够大的空间来存放住这个对象。另外一方面,因为 CMS 线程和应用程序线程并发执行,CMS GC 须要更多的 CPU 资源。同时,由于CMS标记阶段应用程序的线程仍是在执行的,那么就会有堆空间继续分配的状况,为了保证在CMS回收完堆以前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已避免上面提到的状况:在回收完成以前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。

CMS GC 工做步骤以下所示:

0?wx_fmt=jpeg

  • 初始标记(STW initial mark):在这个阶段,须要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到可以和"根对象"直接关联的对象,并做标记。因此这个过程虽然暂停了整个JVM,可是很快就完成了。

  • 并发标记(Concurrent marking):这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,因此用户不会感觉到停顿。

  • 并发预清理(Concurrent precleaning):并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象重新生代晋升到老年代,或者有一些对象被分配到老年代)。经过从新扫描,减小下一个阶段"从新标记"的工做,由于下一个阶段会Stop The World。

  • 从新标记(STW remark):这个阶段会暂停虚拟机,回收器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。

  • 并发清理(Concurrent sweeping):清理垃圾对象,这个阶段回收器线程和应用程序线程并发执行。

  • 并发重置(Concurrent reset):这个阶段,重置CMS回收器的数据结构,等待下一次垃圾回收。

G1 GC

G1 GC 是 JDK 1.7 中正式投入使用的用于取代 CMS 的压缩回收器,它虽然没有在物理上隔断新生代与老生代,可是仍然属于分代垃圾回收器;G1 GC 仍然会区分年轻代与老年代,年轻代依然分有 Eden 区与 Survivor 区。G1 GC 首先将堆分为大小相等的 Region,避免全区域的垃圾收集,而后追踪每一个 Region 垃圾堆积的价值大小,在后台维护一个优先列表,根据容许的收集时间优先回收价值最大的Region;同时 G1 GC 采用 Remembered Set 来存放 Region 之间的对象引用以及其余回收器中的新生代与老年代之间的对象引用,从而避免全堆扫描。G1 GC 的分区示例以下图所示:

0?wx_fmt=jpeg

随着 G1 GC 的出现,Java 垃圾回收器经过引入 Region 的概念,从传统的连续堆内存布局设计,逐步走向了物理上不连续可是逻辑上依旧连续的内存块;这样咱们可以将某个 Region 动态地分配给 Eden、Survivor、老年代、大对象空间、空闲区间等任意一个。每一个 Region 都有一个关联的 Remembered Set(简称RS),RS 的数据结构是 Hash 表,里面的数据是 Card Table (堆中每 512byte 映射在 card table 1byte)。简单的说RS里面存在的是Region中存活对象的指针。当Region中数据发生变化时,首先反映到Card Table中的一个或多个Card上,RS经过扫描内部的Card Table得知Region中内存使用状况和存活对象。在使用Region过程当中,若是Region被填满了,分配内存的线程会从新选择一个新的Region,空闲Region被组织到一个基于链表的数据结构(LinkedList)里面,这样能够快速找到新的Region。

总结而言,G1 GC 的特性以下:

  • 并行性:G1在回收期间,能够有多个GC线程同时工做,有效利用多核计算能力;

  • 并发性:G1拥有与应用程序交替执行的能力,部分工做能够和应用程序同时执行,所以,通常来讲,不会在整个回收阶段发生彻底阻塞应用程序的状况;

  • 分代GC:G1依然是一个分代回收器,可是和以前的各种回收器不一样,它同时兼顾年轻代和老年代。对比其余回收器,或者工做在年轻代,或者工做在老年代;

  • 空间整理:G1在回收过程当中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不一样,它每次回收都会有效地复制对象,减小空间碎片,进而提高内部循环速度。

  • 可预见性:为了缩短停顿时间,G1创建可预存停顿的模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。

G1 GC 的工做步骤以下所示:

0?wx_fmt=jpeg

  • 初始标记(标记一下GC Roots能直接关联的对象并修改TAMS值,须要STW但耗时很短)

  • 并发标记(从GC Root从堆中对象进行可达性分析找存活的对象,耗时较长但能够与用户线程并发执行)

  • 最终标记(为了修正并发标记期间产生变更的那一部分标记记录,这一期间的变化记录在Remembered Set Log 里,而后合并到Remembered Set里,该阶段须要STW可是可并行执行)

  • 筛选回收(对各个Region回收价值排序,根据用户指望的GC停顿时间制定回收计划来回收)

 


0?wx_fmt=png
相关文章
相关标签/搜索