深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记

学习垃圾回收的意义:

Java与C++等语言最大的技术区别:自动化的垃圾回收机制(GC)

为什么要了解GC和内存分配策略?

1、面试需要

2、GC对应用的性能是有影响的;

3、写代码有好处

栈:栈中的生命周期是跟随线程,所以一般不需要关注。

堆:堆中的对象是垃圾回收的重点。

方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点

一、对象是否存活

程序计数器、虚拟机栈、本地方法栈3个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

在java中使用根搜索算法(GC Roots Tracing)判断一个对象是否是可达的。算法的基本思路就是通过一系列的根节点"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有引用链相连时,则说明这个对象是不可达的,就会被判断为可被回收的对象。

1、引用计数算法

给对象添加一个引用计数器,当有一个地方引用他就给他+1 , 如果有一个地方的引用失效就-1 ,实现简单,效率高,微软公司的COM(component object model)技术,使用ActionScript3的Flashplaye,python语言在游戏脚本领域被广泛使用的Squirrel 中都使用了引用计数器算法对内存进行管理,但是主流的java 虚拟机中没有采用这种算法,原因是很难解决对象之间的互相循环引用问题。

优点:快,方便,实现简单。

缺陷:对象相互引用时(A.instance=B同时B.instance=A),很难判断对象是否该回收。

2、可达性分析算法

Java C# lisp 都是使用的可达性分析算法,判断对象是否存活,基本思路就式通过一系列成为 gc roots 的对象为起点,从节点开始往下搜索,搜索走过的路径成为引用链(Reference Chain),当一个对象到gc root 没有任何的引用链相链接,就证明了这个对象是不可用的。

作为GC Roots的对象包括下面几种:

  • 当前虚拟机栈中局部变量表中的引用的对象
  • 当前本地方法栈中JNI(即一般所说的native方法)局部变量表中的引用的对象。方法区中类静态属性引用的对象。
  • 方法区中的常量引用的对象

备注:面试时重要的知识点,牢记

3、各种引用(Reference)

传统定义:Reference中存储的数据代表的是另一块内存的起始地址。

  • 强引用一般的Object obj = new Object() ,就属于强引用。(如果有GCroots的强引用)垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止。
  • 软引用 SoftReference垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它,软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。

一些有用但是并非必需,用软引用关联的对象,系统将要发生OOM之前,这些对象就会被回收。

参见代码:VM参数: -Xms10m  -Xmx10m -XX:+PrintGC

public static void main(String[] args) {
    User u = new User(1,"King"); //new是强引用
    SoftReference userSoft = new SoftReference(u);
    u = null;//干掉强引用,确保这个实例只有userSoft的软引用
    System.out.println(userSoft.get()); //看一下这个对象是否还在
    System.gc();//进行一次GC垃圾回收  千万不要写在业务代码中。
    System.out.println("After gc");
    System.out.println(userSoft.get());
    //往堆中填充数据,导致OOM
    List list = new LinkedList<>();
    try {
        for(int i=0;i<100;i++) {
            System.out.println("*************"+userSoft.get());
            list.add(new byte[1024*1024*1]); //1M的对象
        }
    } catch (Throwable e) {
        //抛出了OOM异常时打印软引用对象
        System.out.println("Exception*************"+userSoft.get());
    }
}

运行结果:

User [id=1, name=King]
[GC (System.gc())  1873K->744K(9728K), 0.0008347 secs]
[Full GC (System.gc())  744K->644K(9728K), 0.0052287 secs]
After gc
User [id=1, name=King]

*************User [id=1, name=King]
*************User [id=1, name=King]
*************User [id=1, name=King]
*************User [id=1, name=King]
*************User [id=1, name=King]
*************User [id=1, name=King]
*************User [id=1, name=King]
*************User [id=1, name=King]

[GC (Allocation Failure) -- 7849K->7849K(9728K), 0.0009049 secs]
[Full GC (Ergonomics)  7849K->7808K(9728K), 0.0061756 secs]
[GC (Allocation Failure) -- 7808K->7808K(9728K), 0.0003995 secs]
[Full GC (Allocation Failure)  7808K->7790K(9728K), 0.0061535 secs]

Exception*************null

例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。

  • 弱引用 WeakReference

垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收

参看代码:

public static void main(String[] args) {
    User u = new User(1,"King");
    WeakReference userWeak = new WeakReference(u);
    u = null;//干掉强引用,确保这个实例只有userWeak的弱引用
    System.out.println(userWeak.get());
    System.gc();//进行一次GC垃圾回收
    System.out.println("After gc");
    System.out.println(userWeak.get());
}

输入结果:

User [id=1, name=King]

After gc

null

注意:软引用 SoftReference和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。

实际运用:WeakHashMap、ThreadLocal

  • 虚引用PhantomReference

幽灵引用,最弱,被垃圾回收的时候收到一个通知,如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收,虚引用主要用来跟踪对象被垃圾回收器回收的活动。

4、判断一个对象是否可回收的过程

请忘记 finalizefinalize可以完成对象的拯救,但是JVM不保证一定能执行,所以请忘记这个“坑”。

不可达对象要经历两次标记才宣告死亡。

发现没有与 GC Roots 相连接的引用链,第一次标记筛选判断是否需要执行finalize()方法,当对象没有覆盖finalize()或者已经被虚拟机调用,则不需要执行。有必要执行则放F-Queue的队列中,稍后由虚拟机自建的、低级别的线程Finalizer去执行—触发操作,不保证运行结束。finalize()方法是对象逃脱死亡的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何一个对象简单关联即可。

即使在可达性分析算法中不可达的对象,它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与GC Roots相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

  • 内存回收 GC(Garbage Collection)

GC中的垃圾,指的是存在于内存中的、不会再被使用的对象。而垃圾回收就是把那些不再被使用的对象进行清除,收回占用的内存空间。如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。如果大量不会被使用的对象一致占着空间不放,如果应用程序需要内存空间,没有多余的内存空间供其使用的话,就会导致内存溢出。因此,对内存空间的管理来说,识别和清理垃圾对象是至关重要。

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:young GC与Full GC:

  • Partial GC:并不收集整个GC堆的模式
  • Young GC:只收集young gen的GC
  • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
  • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

案例Oom类:

-Xms  堆区内存初始内存分配的大小

-Xmx  堆区内存可被分配的最大上限

-XX:+PrintGCDetails  打印GC详情

-XX:+HeapDumpOnOutOfMemoryError 当堆内存空间溢出时输出堆的内存快照。

新生代大小配置参数的优先级。

中间 -Xmn  限定大小。

-XX:SurvivorRatio:参数设为2,表示2个Survivor区和Eden区的比值

注意:

设置为8 表示 两个Survivor :Eden = 2:8 ,每个Survivor占 1/10,

可以修改为2 表示 两个Survivor : Eden = 2: 2  各占一半。

GC overhead limit exceeded 超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常

  • 垃圾回收会占据资源
  • 回收效率过低也会有限制

为什么new出的对象不会被回收了,我们来看看GC是如何判断对象的存活。

1、Minor GC方式

young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。

特点: 发生在新生代上,发生的较频繁,执行速度较快。

触发条件: Eden区空间不足、空间分配担保。

2、Full GC方式

  • 当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC)。
  • 或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC。
  • 或者System.gc()、heap dump带GC,默认也是触发full GC。

特点: 主要发生在老年代上(新生代也会回收),较少发生,执行速度较慢

触发条件:

① 调用 System.gc()。② 老年代区域空间不足。③ 空间分配担保失败。

JDK 1.7 及以前的永久代(方法区)空间不足,

CMS GC处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发Full GC。

三、垃圾回收算法

1、复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

注意:内存移动是必须实打实的移动(复制),不能使用指针玩。

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第1张图片

专门研究表明,新生代中的对象98%是“朝生夕死”的,所以一般来说回收占据10%的空间够用了,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。

2、标记——清除

过程: 首先标记所有需要回收的对象,然后统一回收被标记的对象。

缺点:

1)效率问题,标记和清除效率都不高。

2)标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第2张图片

3、标记——整理

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第3张图片

4、分代回收算法

分代收集:

  • 根据各个年代的特点选取不同的垃圾收集算法。
  • 新生代使用复制算法。
  • 老年代使用标记-整理或者标记-清除算法。

jps -v显示当前使用的垃圾回收器

例如: -Xms128m -Xmx750m -XX:ReservedCodeCacheSize=240m -XX:+UseConcMarkSweepGC (CMS)

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

请记住下图的垃圾收集器和之间的连线关系。

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第4张图片

并行:垃圾收集的多线程的同时进行(同一时刻)。

并发:垃圾收集的多线程和应用的多线程同时进行(同一时间段内)。

注:吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间)

垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间

五、垃圾收集器

上图中展示了7种不同分代的收集器:

Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

新生代收集器:Serial、ParNew、Parallel Scavenge;

老年代收集器:Serial Old、Parallel Old、CMS;

整堆收集器:G1;

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

  • 并行Parallel

指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态(同一时刻)。

如ParNew、Parallel Scavenge、Parallel Old;

  • 并发Concurrent

指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上(同一时间段内)。

如CMS、G1(也有并行);

1、Serial收集器

 jdk1.3之前是HotSpot新生代收集唯一选择,用来收集新生代对象的垃圾回收器,采用复制算法,是单线程的垃圾回收器。这里的单线程有两个意义:一个就是该垃圾回收器只有一条线程来回收垃圾。还有一层意义是在它工作的时候,所有其他正常工作的线程都必须停止等待(stop the world),所以等待时间长短是它的限制。

1.1、特点

  • 针对新生代;
  • 采用复制算法;
  • 单线程收集;
  • 进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World"。

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

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第5张图片

 

1.2、应用场景

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

1.3、设置参数:

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

1.4、Stop TheWorld说明

  • JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿。
  • 会带给用户不良的体验。
  • 从JDK1.3到现在,从Serial收集器-》Parallel收集器-》CMS-》G1,用户线程停顿时间不断缩短,但仍然无法完全消除。

2、ParNew收集器

ParNew垃圾收集器是Serial收集器的多线程版本。和Serial唯一的区别是:多线程、多CPU的,停顿时间比Serial少。

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old

除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。

2.1、特点

  • 除了多线程外,其余的行为、特点和Serial收集器一样。
  • 如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等。
  • 两个收集器共用了不少代码。

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

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第6张图片

2.2、应用场景

在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作; 但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

2.3、设置参数

-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器。

-XX:+UseParNewGC:强制指定使用ParNew。    

-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同。

2.4、为什么只有ParNew能与CMS收集器配合

  • CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
  • CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码。

3、Parallel Scavenge收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器Throughput Collector。关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,

即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),

虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

3.1、特点

  • 有一些特点与ParNew收集器相似:新生代收集器;采用复制算法;多线程收集;
  • 主要特点是:它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)。

3.2、应用场景

高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;

例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

3.3、设置参数

Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:

A:-XX:MaxGCPauseMillis

控制最大垃圾收集停顿时间,大于0的毫秒数;

 MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;

 因为可能导致垃圾收集发生得更频繁;

B:-XX:GCTimeRatio

设置垃圾收集时间占总时间的比率,0

GCTimeRatio相当于设置吞吐量大小;

垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n)。

例如:选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%=1/(1+19);默认值是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收集器一个重要区别; 更多目标调优和GC自适应的调节策略说明请参考: 

《Memory Management in the Java HotSpot™ Virtual Machine》 5节 Ergonomics -- Automatic Selections and Behavior Tuning:

http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》 第2节

Ergonomics:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html#ergonomics

4、Serial Old收集器

 Serial Old是 Serial收集器的老年代版本;

4.1、特点

针对老年代:采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);单线程收集;

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

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第7张图片

2.2、应用场景

主要用于Client模式,而在Server模式有两大用途:

  • 在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配)。
  • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解)。

5、Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本,JDK1.6中才开始提供。

5.1、特点

  • 针对老年代;
  • 采用"标记-整理"算法;
  • 多线程收集;

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

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第8张图片

5.2、应用场景

JDK1.6及之后用来代替老年代的Serial Old收集器,特别是在Server模式,多CPU的情况下;这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;

5.3、设置参数

 -XX:+UseParallelOldGC:指定使用Parallel Old收集器;

6、CMS收集器

从名字(Concurrent Mark Sweep)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些。

收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

6.1、特点

  • 针对老年代;
  • 基于"标记-清除"算法(不进行压缩操作,产生内存碎片);            
  • 以获取最短回收停顿时间为目标;
  • 并发收集、低停顿;
  • 需要更多的内存(看后面的缺点);

是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

6.3、设置参数

-XX:+UseConcMarkSweepGC,一般新生代使用ParNew,老年代的用CMS

6.4、CMS收集器运作过程

垃圾回收过程分为4个步骤:

1)初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要停顿(STW -Stop the world)。

2)并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。

3)重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

4)并发清除:不需要停顿。

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

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第9张图片

6.5、CMS收集器缺点

优点:由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

缺点:

1)CPU资源敏感:因为并发阶段多线程占据CPU资源,如果CPU资源不足,效率会明显降低。并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。CMS的默认收集线程数量是=(CPU数量+3)/4,当CPU数量多4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

增量式并发收集器:针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS);类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间;但效果并不理想,JDK1.6后就官方不再提倡用户使用。

2)浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在1.6的版本中老年代空间使用率阈值(92%),如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

a、浮动垃圾(Floating Garbage)

在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集,也要可以认为CMS所需要的空间比其他垃圾收集器大;

-XX:CMSInitiatingOccupancyFraction:设置CMS预留内存空间;

 JDK1.5默认值为68%;JDK1.6变为大约92%;

b、Concurrent Mode Failure失败

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

3)会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片

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

解决方法:                

a、-XX:+UseCMSCompactAtFullCollection

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

b、-XX:+CMSFullGCsBeforeCompaction

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

更多关于内存分配方式请参考:《Java对象在Java虚拟机中的创建过程》。

7、G1收集器

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

7.1、特点

A并行与并发

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

B分代收集,收集范围包括新生代和老年代    

  • 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
  • 能够采用不同方式处理不同时期的对象;
  • 虽然保留分代概念,但Java堆的内存布局有很大差别;
  • 将整个堆划分为多个大小相等的独立区域(Region);
  • 新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;

C)结合多种垃圾收集算法,空间整合,不产生碎片

从整体看,是基于标记-整理算法;

  • 从局部(两个Region间)看,是基于复制算法;
  • 这是一种类似火车算法的实现;
  • 都不会产生内存碎片,有利于长时间运行;

D)可预测的停顿:低停顿的同时实现高吞吐量

  • G1除了追求低停顿处,还能建立可预测的停顿时间模型;
  • 可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;

7.3、设置参数

-XX:+UseG1GC:指定使用G1收集器;

-XX:InitiatingHeapOccupancyPercent:当Java堆的占用率达到参数值时,开始并发标记阶段;默认为45

-XX:MaxGCPauseMillis:为G1设置暂停时间目标,默认值为200毫秒;

-XX:G1HeapRegionSize:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region。

7.4、一个对象被不同区域引用的问题

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

解决方法:

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

如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。

7.5、内部布局改变

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

算法:标记—整理 (humongous) 和复制回收算法(survivor)。

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第10张图片

7.6、GC模式

不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

1)Young GC:选定所有年轻代里的Region,通过控制年轻代的region个数,即年轻代内存大小来控制young GC的时间开销(复制回收算法)。

2)Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region,在用户指定的开销目标范围内尽可能选择收益高的老年代Region。

Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap,所以我们可以知道,G1是不提供full GC的。

7.7、G1收集器运作过程全局并发标记(global concurrent marking

  • 初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程(STW),但耗时很短。
  • 并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执行。
  • 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

特点:

  • 空间整合,不会产生内存碎片。
  • 算法:标记—整理 (humongous) 和复制回收算法(survivor)

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第11张图片

7.8、G1收集器实现可预测的停顿

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1把内存“化整为零”的思路,理解起来似。

7.9、G1 GC主要的参数

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第12张图片

7.10、垃圾回收器的重要参数(使用-XX:)

深入理解JVM——第三章 垃圾回收算法与垃圾回收器学习笔记_第13张图片

8、Stop The World现象

GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数。

 

 

你可能感兴趣的:(深入理解JVM)