Skip to content

Latest commit

 

History

History
856 lines (561 loc) · 37.9 KB

垃圾回收.md

File metadata and controls

856 lines (561 loc) · 37.9 KB

1 判断算法垃圾

主要是2种:引用计数法和根搜索算法

1.1 引用计数法( Reference Counting)

1. 概念 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值 就减1:任何时刻计数器都为0的对象就是不可能再被使用的。

**2. 优点 ** 实现简单,判断效率高,大部分情况下都是很不错的算法

3. 缺点 Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环 引用的问题。

public class Test { 
     public static void main(String[] args) {

        MyObject object1 = new MyObject(); 
        MyObject object2 = new MyObject();

        object1.object = object2; 
        object2.object = object1;

        object1 = null; 
        object2 = null;

    }
}

class MyObject{ 
     MyObject object;
}

1.2 根搜索算法( GCRoots Tracing)

1. 概念 又叫可达性算法。在主流的商用程序语言中(Java和C#),都是使用根搜索算法判定对象是否存活 的。 基本思路就是通过一系列的名为“GCRoot”的对象作为起始点,从这些节点开始向下搜索,搜索 所走过的路径称为引用链(Reference Chain),当一个对象到GCRoot没有任何引用链相连(就 是从GCRoot到这个对象不可达)时,则证明此对象是不可用的。 不可达不一定会被回收, 可以用finalize()方法抢救下, 但绝对不推荐

2. 可作GCRoots的对象

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。

  • 方法区中的类静态属性引用的对象。

  • 方法区中的常量引用的对象。

  • 本地方法栈中JNl(即一般说的Native方法)的引用的对象。

1.3 对象引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用 (SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种,这四种引用 强度依次逐渐减弱。

1. 强引用 代码中普遍存在,类似“Object obj=new Object()”这类引用,只要强引用还在,就不会被GC。

2. 软引用 非必须引用,内存溢出之前进行回收,如内存还不够,才会抛异常。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null

3. 弱引用 非必须引用,只要有GC,就会被回收。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。

4. 虚引用 垃圾回收时回收,无法通过引用取到对象值

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 虚引用主要用于检测对象是否已经从内存中删除。

1.4 回收过程

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;

第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

/**
 * 此代码演示了两点:
 * 1.对象可以在被GC时自我拯救。
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 
 */
public class finalizeEscapeGC {

    public static finalizeEscapeGC SAVE_HOOK = null;

    public void isApve() { 
        System.out.println("yes, i am still apve :)"); 
    }

    @Override
    protected void finalize() throws Throwable { 
        super.finalize(); 
        System.out.println("finalize mehtod executed!"); 
        finalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable { 
        SAVE_HOOK = new finalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc(); 
        //因为finalize方法优先级很低,所以暂停0.5秒以等待它 
        Thread.sleep(500);
        if (SAVE_HOOK != null) { 
            SAVE_HOOK.isApve();
        } else {
            System.out.println("no, i am dead :(");
        }

        //下面这段代码与上面的完全相同,但是这次自救却失败了 
        SAVE_HOOK = null;
        System.gc(); 
        //因为finalize方法优先级很低,所以暂停0.5秒以等待它 
        Thread.sleep(500);
        if (SAVE_HOOK != null) { 
            SAVE_HOOK.isApve();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}        

1.5 方法区回收

1. 概念 方法区也是有垃圾回收的,主要回收废弃常量和无用的类。 即使满足回收条件也不一定真得回收。主要性价比太低

2. 废弃常量 比如字符串常量,没有对象引用即可回收 常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

3. 无用的类( 需要同时满足3个条件)

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象在任何地方没有被引用,也无法通过反射访问该类的方法。 在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

2. 回收收集算法

2.1 标记-清除算法( Mark-Sweep)

1. 概念 最基本的算法,主要分为标记和清除2个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象

2. 缺点

  1. 效率不高,标记和清除过程的效率都不高

  2. 空间碎片,会产生大量不连续的内存碎片,会导致大对象可能无法分配,提前触发GC 。

2.2 复制回收算法( Copying)

1. 概念 为解决效率。它将可用内存按容量划分为相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

2. 回收新生代 现在商业虚拟机都是采用这种收集算法来回收新生代,当回收时,将Eden和Survivor中还存活着的对象拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

2.3 标记-整理算法( Mark-Compact)

老年代没有人担保,不能用复制回收算法。可以用标记-整理算法,标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

2.4 分代回收算法( Generational Collection)

当前商业虚拟机都是采用这种算法。根据对象的存活周期的不同将内存划分为几块。

  • 新生代,每次垃圾回收都有大量对象失去,选择复制算法。
  • 老年代,对象存活率高,无人进行分配担保,就必须采用标记清除或者标记整理算法

内存分配担保 在现实社会中,借款会指定担保人,就是当借款人还不起钱,就由担保人来还钱。

在JVM的内存分配时,也有这样的内存分配担保机制。就是当在新生代无法分配内存的时候,把新生代的对象转移到老生代,然后把新对象放入腾空的新生代。

现在假设,我们的新生代分为三个区域,分别为eden space,from space和to space。

现在是尝试分配三个2MB的对象和一个4MB的对象,然后我们通过JVM参数 -Xms20M、-Xmx20M、-Xmn10M

把Java堆大小设置为20MB,不可扩展。
其中10M分配给新生代,另外10M分配给老生代。

然后我们通过 -XX:SurvivorRatio=8 来分配新生代各区的比例,设置为8,表示eden与一个survivor区的空间比例为8:1。

客户端内存担保 JVM参数配置:

-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC

这里我们先手动指定垃圾收集器为客户端模式下的Serial+Serial Old的收集器组合进行内存回收。由于不同的收集器的收集机制不同,为了呈现出内存分配的担保效果,我们这里需要手动指定为Serial+Serial Old模式。

另外担保机制在JDK1.5以及之前版本中默认是关闭的,需要通过HandlePromotionFailure手动指定,JDK1.6之后就默认开启。这里我们使用的是JDK1.8,所以不用再手动去开启担保机制。

下面我们新建四个byte数组, 前三个分别为2MB大小的内存分配, 第四个是4MB的内存分配。 代码如下:

然后运行程序,看看GC日志:

[GC (Allocation Failure) [DefNew: 7836K->472K(9216K), 0.0120087 secs] 7836K->6616K(19456K), 0.0123203 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]

Heap

def new generation total 9216K, used 4732K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)

eden space 8192K, 52% used [0x00000007bec00000, 0x00000007bf0290f0, 0x00000007bf400000)

from space 1024K, 46% used [0x00000007bf500000, 0x00000007bf576018, 0x00000007bf600000)

to space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)

tenured generation total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)

the space 10240K, 60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)

Metaspace used 3160K, capacity 4494K, committed 4864K, reserved 1056768K

class space used 341K, capacity 386K, committed 512K, reserved 1048576K 

通过GC日志我们发现在分配allocation4的时候,发生了一次Minor GC,让新生代从7836K变为了472K,但是你发现整个堆的占用并没有多少变化。这是因为前面三个2MB的对象都还存活着,所以回收器并没有找到可回收的对象。但为什么会出现这次GC呢?

如果你算一笔账就知道了,前面三个对象2MB+2MB+2MB=6MB。

虚拟机分配内存优先会分配到新生代的eden space,通过图1我们知道新生代可用内存一共只有9216KB,现在新生代已经被用去了6MB,还剩下9216KB-6144KB=3072KB,然而第四个对象是4MB,显然在新生代已经装不下了。

于是发生了一次Minor GC!

而且本次GC期间,虚拟机发现eden space的三个对象(6MB)又无法全部放入Survivor空间(Survivor可用内存只有1MB)。

这时候该怎么办呢?第四个对象还要不要分配呢?

此时,JVM就启动了内存分配的担保机制,把这6MB的三个对象直接转移到了老年代。此时就把新生代的空间腾出来了,然后把第四个对象(4MB)放入了Eden区中,所以你看到的结果是4096/8192=0.5,也就是约50%:

eden space 8192K, 52% used [0x00000007bec00000, 0x00000007bf0290f0,0x00000007bf400000)

老年代则被占用了6MB,也就是前三个对象,102423=6144KB,6144KB/10240KB=0.6也就是60%:

the space 10240K, 60% used [0x00000007bf600000, 0x00000007bfc00030,0x00000007bfc00200, 0x00000007c0000000)

担保后, allocation4放入到新生代eden区

担保后, 之前在新生代的三个对象转移到了老生代

服务端模式下的担保机制实现: 上面我们演示的在客户端模式(Serial+Serial Old)的场景下的结果,接下来我们使用服务端模式(Parallel Scavenge+Serial Old的组合)来看看担保机制的实现。

修改GC组合为:-XX:+UseParallelGC 然后我们运行程序看看GC日志。

第四个对象是4MB的情况下:

[GC (Allocation Failure) [PSYoungGen: 6156K->592K(9216K)] 6156K->4696K(19456K),0.0032059 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]

Heap

PSYoungGen total 9216K, used 7057K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)

eden space 8192K, 78% used [0x00000007bf600000,0x00000007bfc505f8,0x00000007bfe00000)

from space 1024K, 57% used [0x00000007bfe00000,0x00000007bfe94010,0x00000007bff00000)

to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000) ParOldGen total 10240K, used 4104K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)

object space 10240K, 40% used [0x00000007bec00000,0x00000007bf002020,0x00000007bf600000)

Metaspace used 3299K, capacity 4494K, committed 4864K, reserved 1056768K 

class space used 357K, capacity 386K, committed 512K, reserved 1048576K

第四个对象是3MB的情况下:

[GC (Allocation Failure) [PSYoungGen: 8192K->544K(9216K)] 8192K->6688K(19456K),0.0052943 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

[Full GC (Ergonomics) [PSYoungGen: 544K->0K(9216K)] [ParOldGen: 6144K->6627K(10240K)] 6688K->6627K(19456K), [Metaspace: 3286K->3286K(1056768K)], 0.0063048 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

Heap

PSYoungGen total 9216K, used 3238K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)

eden space 8192K, 39% used [0x00000007bf600000,0x00000007bf929918,0x00000007bfe00000)

from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000) 

to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)

ParOldGen total 10240K, used 6627K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)

object space 10240K, 64% used [0x00000007bec00000,0x00000007bf278f70,0x00000007bf600000)

Metaspace used 3294K, capacity 4494K, committed 4864K, reserved 1056768K

class space used 356K, capacity 386K, committed 512K, reserved 1048576K 

发现当我们使用Server模式下的ParallelGC收集器组合( Parallel Scavenge+Serial Old的组合) 下, 担保机制的实现和之前的Client模式下( SerialGC收集器组合) 有所变化。 在GC前还会进行一次判断, 如果要分配的内存>=Eden区大小的一半, 那么会直接把要分配的内存放入老年代中。 否则才会进入担保机制。

这里我们的第四个对象是4MB的时候,也就是(1024KB*4)/8192KB=0.5,刚好一半,于是就这第四个对象分配到了老年代。

第二次,我们把第四个对象由4MB,改为3MB,此时3MB/8192KB=0.37,显然不到一半,此时发现3MB还是无法放入,那么就执行担保机制,把前三个对象转移到老生代,然后把第四个对象(3MB)放入eden区。

总结 内存分配是在JVM在内存分配的时候,新生代内存不足时,把新生代的存活的对象搬到老生代,然后新生代腾出来的空间用于为分配给最新的对象。这里老生代是担保人。在不同的GC机制下,也就是不同垃圾回收器组合下,担保机制也略有不同。在Serial+Serial Old的情况下,发现放不下就直接启动担保机制;在Parallel Scavenge+Serial Old的情况下,却是先要去判断一下要分配的内存是不是>=Eden区大小的一半,如果是那么直接把该对象放入老生代,否则才会启动担保机制。

3. 垃圾回收器

3.1. 概念

垃圾收集器是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法、火车算法)的具体实现,不同种类JVM所提供的垃圾收集器可能会有很大差别,HotSpot虚拟机中的7种垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1

3.2. 垃圾收集器组合

分类 分类新生代收集器:Serial、ParNew、Parallel Scavenge; 老年代收集器: Serial Old、Parallel Old、CMS; 整堆收集器: G1;

搭配使用 两个收集器间有连线,表明它们可以搭配使用:

  • Serial/Serial Old

  • Serial/CMS

  • ParNew/Serial Old

  • ParNew/CMS

  • Parallel Scavenge/Serial Old

  • Parallel Scavenge/Parallel Old

  • G1

3.3. 并发垃圾收集和并行垃圾收集的区别

并行( Parallel) 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态; 如ParNew、 Parallel Scavenge、 Parallel Old;

并发( Concurrent) 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行) 用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上; 如CMS、G1(也有并行);

3.4. Minor GC、 Major GC和Full GC之间的区别

Minor GC 新生代GC(包括 Eden 和 Survivor 区域),Minor GC非常频繁,回收速度也比较快 当年轻代满时会触发Minor GC,这里年轻代满指的是Eden代满,Survivor满不会引发GC

Major GC 老年代GC Major GC速度一般比Minor GC慢10倍以上;

Full GC Minor GC+Major GC,清理整个堆空间—包括年轻代和永久代

  1. 调用System.gc时,系统建议执行Full GC,但是不必然执行

  2. 老年代空间不足

  3. 方法区空间不足

  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

  5. 年轻代需要把该对象转存到老年代,且老年代的可用内存小于该对象大小

3.5 Serial收集器

Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器; JDK1.3.1前是HotSpot新生代收集的唯一选择;

1. 特点 采用复制算法 单线程收集 Stop The World 进行垃圾收集时,必须暂停所有工作线程,直到完成;

Serial/Serial Old组合收集器运行示意图如下:

2. 应用场景 依然是HotSpot在Client模式下默认的新生代收集器;

也有优于其他收集器的地方: *简单高效(与其他收集器的单线程相比); *对单个CPU环境,Serial收集器没有线程切换开销,可获得最高的单线程收集效率; *在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的。

3. 设置参数 "-XX:+UseSerialGC":添加该参数来显式的使用串行垃圾收集器;

4. Stop TheWorld说明 JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;

从JDK1.3到现在,从Serial收集器-》Parallel收集器-》CMS-》G1,用户线程停顿时间不断缩短,但仍然无法完全消除;

3. 6 ParNew收集器

ParNew垃圾收集器是Serial收集器的多线程版本。

1. 特点 除了多线程外,其余的行为、特点和Serial收集器一样

如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等; 两个收集器共用了不少代码;

ParNew/Serial Old组合收集器运行示意图如下:

2. 应用场景 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;

但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

3. 设置参数 "-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器 "-XX:+UseParNewGC":强制指定使用ParNew "-XX:ParallelGCThreads":指定垃圾收集线程数量,默认收集线程与CPU数量相同

4. 为什么只有ParNew能与CMS收集器配合 CMS是HotSpot在JDK1.5第一款并发收集器,让垃圾收集线程与用户线程(基本上)同时工作;

CMS作为老年代收集器,但无法与JDK1.4新生代收集器Parallel Scavenge配合工作;

因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

3. 7 Parallel Scavenge收集器

Scavenge [ˈskævɪndʒ] v. (从废弃物中) 觅食; 捡破烂; 拾荒; 吃(动物尸体);

因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。

1. 特点 a. 有一些特点与ParNew收集器相似 新生代收集器 采用复制算法 多线程收集

b.主要特点是:它的关注点与其他收集器不同 CMS等收集器的关注点是缩短垃圾收集时用户线程的停顿时间; 而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)

2. 应用场景 提高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间 适用于多CPU,对停顿时间没特别要求的后台统计等运算任务

3. 设置参数 Parallel Scavenge收集器提供两个参数用于精确控制吞吐量: a "-XX:MaxGCPauseMillis" 控制最大垃圾收集停顿时间,大于0的毫秒数; 如果值过小,停顿时间会缩短,但是垃圾收集次数会更频繁,使吞吐量下降;

b "-XX:GCTimeRatio" 设置垃圾收集时间占总时间的比率,0<n<100的整数;相当于设置吞吐量大小;

垃圾收集时间占应用执行时间的比例算法:1 / (1 + n)。默认值是1%--1/(1+99),即n=99;

垃圾收集所花费的时间是年轻一代和老年代收集的总时间;

如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;

c. "-XX:+UseAdptiveSizePolicy"

开启这个参数后,就不用手工指定一些细节参数,如: 新生代的大小(-Xmn). Eden与Survivor区的比例(-XX:SurvivorRation). 晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;

JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);

这是一种值得推荐的方式: (1). 只需设置好内存数据大小(如"-Xmx"设置最大堆); (2). 然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标; (3). 那些具体细节参数的调节就由JVM自适应完成;

这也是Parallel Scavenge收集器与ParNew收集器一个重要区别

4. 吞吐量与收集器关注点说明 a. 吞吐量(Throughput) CPU用于运行用户代码的时间与CPU总消耗时间的比值; 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间); 高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;

b. 垃圾收集器期望的目标(关注点) \1. 停顿时间 停顿时间越短就适合需要与用户交互的程序; 良好的响应速度能提升用户体验; \2. 吞吐量高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务; 主要适合在后台计算而不需要太多交互的任务; \3. 覆盖区(Footprint) 在达到前面两个目标的情况下,尽量减少堆的内存空间; 可以获得更好的空间局部性;

3. 8 Serial Old收集器

Serial Old是 Serial收集器的老年代版本; 1. 特点 针对老年代; 采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact); 单线程收集;

Serial/Serial Old收集器运行示意图如下:

2. 应用场景 主要用于Client模式;

而在Server模式有两大用途: a. 在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配) b. 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);

更多Serial Old收集器信息请参考: 内存管理白皮书 4.3.2节:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

3.9 Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;

JDK1.6中才开始提供;

1. 特点 针对老年代; 采用"标记-整理"算法;多线程收集; Parallel Scavenge/Parallel Old收集器运行示意图如下:

2. 应用场景 JDK1.6及之后用来代替老年代的Serial Old收集器;

特别是在Server模式,多CPU的情况下; 在注重吞吐量以及CPU资源敏感场景,就有Parallel Scavenge加Parallel Old收集器"给力"组合

3. 设置参数 "-XX:+UseParallelOldGC":指定使用Parallel Old收集器; 更多Parallel Old收集器收集过程介绍请参考: 《内存管理白皮书》 4.5.2节:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

3.10 CMS收集器

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent LowPause Collector)或低延迟(low-latency)垃圾收集器;

1. 特点 针对老年代; 基于"标记-清除"算法(不进行压缩操作,产生内存碎片); 以获取最短回收停顿时间为目标; 并发收集、低停顿; 需要更多的内存(看后面的缺点); 是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器; 第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

2. 应用场景 与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用;

3. 设置参数 "-XX:+UseConcMarkSweepGC":指定使用CMS收集器;

4. CMS收集器运作过程, 可以分为4个步骤: (A). 初始标记(CMS initial mark) 仅标记一下GC Roots能直接关联到的对象; 速度很快;但需要"Stop The World";

(B). 并发标记(CMS concurrent mark) 进行GC Roots Tracing的过程; 刚才产生的集合中标记出存活对象; 应用程序也在运行; 并不能保证可以标记出所有的存活对象;

(C). 重新标记(CMS remark) 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率;

(D). 并发清除(CMS concurrent sweep) 回收所有的垃圾对象; 整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作; 所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行; CMS收集器运行示意图如下:

5. CMS收集器3个明显的缺点 a. 对CPU资源非常敏感 并发收集虽不暂停用户线程,但会占用部分CPU资源,导致应用程序变慢,总吞吐量降低。 CMS的默认收集线程数量是=(CPU数量+3)/4;

当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

增量式并发收集器:(Incremental Concurrent Mark Sweep/i-CMS)

针对这种情况,曾出现了"增量式并发收集器"

类似使用抢占式来模拟多任务机制7的思想,让收集线程和用户线程交替运行,减少收集线程运行时间;但效果并不理想,JDK1. 6后就官方不再提倡用户使用。更多请参考:

官方的《垃圾收集调优指南》8.8节 Incremental Mode:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#CJAGIIEJ 《内存管理白皮书》 4.6.3节可以看到一些描述;

b. 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败

  1. 浮动垃圾(Floating Garbage) 在并发清除时,用户线程新产生的垃圾,称为浮动垃圾; 这使得并发清除时需预留一定空间,不能像其他收集器在老年代快满再进行收集; 也要可以认为CMS所需要的空间比其他垃圾收集器大; "-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间; JDK1.5默认值为68%; JDK1.6变为大约92%;

\2. "Concurrent Mode Failure"失败 如CMS预留空间无法满足需求,就会出现一次"Concurrent Mode Failure"失败; 这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生; 这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。

c. 产生大量内存碎片 由于CMS基于"标记-清除"算法,清除后不进行压缩操作; 前面《Java虚拟机垃圾回收(二) 垃圾回收算法》"标记-清除"算法介绍时曾说过: 产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。

解决方法: (1). "-XX:+UseCMSCompactAtFullCollection" 使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程; 但合并整理过程无法并发,停顿时间会变长; 默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);

(2). "-XX:+CMSFullGCsBeforeCompaction" 设置执行多少次不压缩的Full GC后,来一次压缩整理; 为减少合并整理过程的停顿时间; 默认为0,也就是说每次都执行Full GC,不会进行压缩整理;由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;

更多关于内存分配方式请参考:《Java对象在Java虚拟机中的创建过程》 总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间; 但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;

更多CMS收集器信息请参考: 《垃圾收集调优指南》 8节 Concurrent Mark Sweep (CMS) Collector:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector 《内存管理白皮书》 4.6节 Concurrent Mark-Sweep (CMS) Collector:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

3.11 G1收集器

G1(Garbage-First)是JDK7-u4才推出商用的收集器;

1. 特点 a. 并行与并发 能充分利用多CPU、多核环境下的硬件优势; 可以并行来缩短"Stop The World"停顿时间; 也可以并发让垃圾收集与用户程序同时进行;

b. 分代收集,收集范围包括新生代和老年代 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配; 能够采用不同方式处理不同时期的对象; 虽然保留分代概念,但Java堆的内存布局有很大差别; 将整个堆划分为多个大小相等的独立区域(Region); 新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合; 更多G1内存布局信息请参考: 《垃圾收集调优指南》 9节:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection

c. 结合多种垃圾收集算法,空间整合,不产生碎片 从整体看,是基于标记-整理算法; 从局部(两个Region间)看,是基于复制算法; 这是一种类似火车算法的实现; 都不会产生内存碎片,有利于长时间运行;

d. 可预测的停顿:低停顿的同时实现高吞吐量G1除了追求低停顿处,还能建立可预测的停顿时间模型; 可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;

2. 应用场景 面向服务端应用,针对具有大内存、多处理器的机器; 最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案; 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒; 用来替换掉JDK1.5中的CMS收集器; 在下面的情况时,使用G1可能比CMS好: (1). 超过50%的Java堆被活动数据占用; (2). 对象分配频率或年代提升频率变化很大; (3). GC停顿时间过长(长于0.5至1秒)。 是否一定采用G1呢?也未必: 如果现在采用的收集器没有出现问题,不用急着去选择G1; 如果应用程序追求低停顿,可以尝试选择G1; 是否代替CMS需要实际场景测试才知道。

3. 设置参数 "-XX:+UseG1GC":指定使用G1收集器; "-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45; "-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒; "-XX:HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region; 更多关于G1参数设置请参考: 《垃圾收集调优指南》 10.5节:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#important_defaults

4. 为什么G1收集器可以实现可预测的停顿 G1可以建立可预测的停顿时间模型,是因为: 可以有计划地避免在Java堆的进行全区域的垃圾收集; G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表; 每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来); 这就保证了在有限的时间内可以获取尽可能高的收集效率;

5. 一个对象被不同区域引用的问题 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确? 在其他的分代收集器,也存在这样的问题(而G1更突出): 回收新生代也不得不同时扫描老年代? 这样的话会降低Minor GC的效率;

解决方法: 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描: 每个Region都有一个对应的Remembered Set; 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作; 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集 器:检查老年代对象是否引用了新生代对象); 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的RememberedSet中;

当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;

就可以保证不进行全局扫描,也不会有遗漏。

6. G1收集器运作过程 不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。 (A). 初始标记(Initial Marking) 仅标记一下GC Roots能直接关联到的对象; 且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的 Region中创建新对象; 需要"Stop The World",但速度很快;

(B). 并发标记(Concurrent Marking) 进行GC Roots Tracing的过程; 刚才产生的集合中标记出存活对象; 耗时较长,但应用程序也在运行; 并不能保证可以标记出所有的存活对象;

(C). 最终标记(Final Marking) 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 上一阶段对象的变化记录在线程的Remembered Set Log; 这里把Remembered Set Log合并到Remembered Set中; 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率;

(D). 筛选回收(Live Data Counting and Evacuation) 首先排序各个Region的回收价值和成本; 然后根据用户期望的GC停顿时间来制定回收计划; 最后按计划回收一些价值高的Region中垃圾对象; 回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在 此过程中压缩和释放内存; 可以并发进行,降低停顿时间,并增加吞吐量;