关于jvm:jvm垃圾回收

1次阅读

共计 11815 个字符,预计需要花费 30 分钟才能阅读完成。

转载

https://mp.weixin.qq.com/s?__…

JVM 的内存区域

1、虚拟机栈:形容的是办法执行时的内存模型, 是线程公有的,生命周期与线程雷同, 每个办法被执行的同时会创立栈桢(下文会看到),次要保留执行办法时的局部变量表、操作数栈、动静连贯和办法返回地址等信息, 办法执行时入栈,办法执行完出栈,出栈就相当于清空了数据,入栈出栈的机会很明确,所以这块区域不须要进行 GC。
2、本地办法栈:与虚拟机栈性能十分相似,次要区别在于虚拟机栈为虚拟机执行 Java 办法时服务,而本地办法栈为虚拟机执行本地办法时服务的。这块区域也不须要进行 GC
3、程序计数器:线程独有的,能够把它看作是以后线程执行的字节码的行号指示器,比方如下字节码内容,在每个字节码后面都有一个数字(行号),咱们能够认为它就是程序计数器存储的内容

记录这些数字(指令地址)有啥用呢,咱们晓得 Java 虚拟机的多线程是通过线程轮流切换并调配处理器的工夫来实现的,在任何一个时刻,一个处理器只会执行一个线程,如果这个线程被调配的工夫片执行完了(线程被挂起),处理器会切换到另外一个线程执行,当下次轮到执行被挂起的线程(唤醒线程)时,怎么晓得上次执行到哪了呢,通过记录在程序计数器中的行号指示器即可晓得,所以程序计数器的次要作用是记录线程运行时的状态,不便线程被唤醒时能从上一次被挂起时的状态继续执行,须要留神的是,程序计数器是惟一一个在 Java 虚拟机标准中没有规定任何 OOM 状况的区域,所以这块区域也不须要进行 GC
本地内存:线程共享区域,Java 8 中,本地内存,也是咱们通常说的堆外内存,蕴含元空间和间接内存, 留神到上图中 Java 8 和 Java 8 之前的 JVM 内存区域的区别了吗,在 Java 8 之前有个永恒代的概念,实际上指的是 HotSpot 虚拟机上的永恒代,它用永恒代实现了 JVM 标准定义的办法区性能,次要存储类的信息,常量,动态变量,即时编译器编译后代码等,这部分因为是在堆中实现的,受 GC 的治理,不过因为永恒代有 -XX:MaxPermSize 的下限,所以如果动静生成类(将类信息放入永恒代)或大量地执行 String.intern(将字段串放入永恒代中的常量区),很容易造成 OOM,有人说能够把永恒代设置得足够大,但很难确定一个适合的大小,受类数量,常量数量的多少影响很大。所以在 Java 8 中就把办法区的实现移到了本地内存中的元空间中,这样办法区就不受 JVM 的管制了, 也就不会进行 GC,也因而晋升了性能(产生 GC 会产生 Stop The Word, 造成性能受到肯定影响,后文会提到),也就不存在因为永恒代限度大小而导致的 OOM 异样了(假如总内存 1G,JVM 被分配内存 100M,实践上元空间能够调配 2G-100M = 1.9G,空间大小足够),也不便在元空间中对立治理。综上所述,在 Java 8 当前这一区域也不须要进行 GC
 画外音:思考一个问题,堆外内存不受 GC 管制,无奈通过 GC 开释内存,那该以什么样的模式开释呢,总不能只创立不开释吧,这样的话内存可能很快就满了,这里不做具体论述,请看文末的参考文章
堆:后面几块数据区域都不进行 GC,那只剩下堆了,是的,这里是 GC 产生的区域!对象实例和数组都是在堆上调配的,GC 也次要对这两类数据进行回收,这块也是咱们之后重点须要剖析的区域

如何辨认垃圾

上一节咱们具体讲述了 JVM 的内存区域,晓得了 GC 次要产生在堆,那么 GC 该怎么判断堆中的对象实例或数据是不是垃圾呢,或者说判断某些数据是否是垃圾的办法有哪些。援用计数法最容易想到的一种形式是援用计数法,啥叫援用计数法,简略地说,就是对象被援用一次,在它的对象头上加一次援用次数,如果没有被援用(援用次数为 0),则此对象可回收

String ref = new String("Java");

以上代码 ref1 援用了右侧定义的对象,所以援用次数是 1

如果在上述代码前面增加一个 ref = null,则因为对象没被援用,援用次数置为 0,因为不被任何变量援用,此时即被回收,动图如下

看起来用援用计数的确没啥问题了,不过它无奈解决一个次要的问题:循环援用!啥叫循环援用

public class TestRC {

    TestRC instance;
    public TestRC(String name) { }

    public static  void main(String[] args) {
        // 第一步
    A a = new TestRC("a");
    B b = new TestRC("b");

        // 第二步
    a.instance = b;
    b.instance = a;

        // 第三步
    a = null;
    b = null;
    }
}

按步骤一步步画图

到了第三步,尽管 a,b 都被置为 null 了,然而因为之前它们指向的对象相互指向了对方(援用计数都为 1),所以无奈回收,也正是因为无奈解决循环援用的问题,所以古代虚拟机都不必援用计数法来判断对象是否应该被回收。

可达性算法

古代虚拟机根本都是采纳这种算法来判断对象是否存活,可达性算法的原理是以一系列叫做  GC Root  的对象为终点登程,引出它们指向的下一个节点,再以下个节点为终点,引出此节点指向的下一个结点。。。(这样通过 GC Root 串成的一条线就叫援用链),直到所有的结点都遍历结束, 如果相干对象不在任意一个以 GC Root 为终点的援用链中,则这些对象会被判断为「垃圾」, 会被 GC 回收。

如图示,如果用可达性算法即可解决上述循环援用的问题,因为从 GC Root 登程没有达到 a,b, 所以 a,b 可回收 a, b 对象可回收,就肯定会被回收吗?
并不是,对象的 finalize 办法给了对象一次负隅顽抗的机会,当对象不可达(可回收)时,当产生 GC 时,会先判断对象是否执行了 finalize 办法,如果未执行,则会先执行 finalize 办法,咱们能够在此办法里将以后对象与 GC Roots 关联,这样执行 finalize 办法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!
留神:finalize 办法只会被执行一次,如果第一次执行 finalize 办法此对象变成了可达的确不会回收,但如果对象再次被 GC,则会疏忽 finalize 办法,对象会被回收!这一点切记!
那么这些 GC Roots 到底是什么货色呢,哪些对象能够作为 GC Root 呢,有以下几类

1、虚拟机栈(栈帧中的本地变量表)中援用的对象办法区中类动态属性援用的对象。
2、办法区中常量援用的对象。
3、本地办法栈中 JNI(即个别说的 Native 办法)援用的对象

虚拟机栈中援用的对象

如下代码所示,a 是栈帧中的本地变量,当 a = null 时,因为此时 a 充当了 GC Root 的作用,a 与原来指向的实例 new Test() 断开了连贯,所以对象会被回收。`
public class Test {

public static  void main(String[] args) {Test a = new Test();
a = null;
}

}

办法区中类动态属性援用的对象

如下代码所示,当栈帧中的本地变量 a = null 时,因为 a 原来指向的对象与 GC Root (变量 a) 断开了连贯,所以 a 原来指向的对象会被回收,而因为咱们给 s 赋值了变量的援用,s 在此时是类动态属性援用,充当了 GC Root 的作用,它指向的对象仍然存活!

public class Test {
    public static Test s;
    public static  void main(String[] args) {Test a = new Test();
    a.s = new Test();
    a = null;
    }
}

办法区中常量援用的对象

如下代码所示,常量 s 指向的对象并不会因为 a 指向的对象被回收而回收

public class Test {public static final Test s = new Test();
        public static void main(String[] args) {Test a = new Test();
        a = null;
        }
}

本地办法栈中 JNI 援用的对象

这是简略给不分明本地办法为何物的童鞋简略解释一下:所谓本地办法就是一个 java 调用非 java 代码的接口,该办法并非 Java 实现的,可能由 C 或 Python 等其余语言实现的,Java 通过 JNI 来调用本地办法,而本地办法是以库文件的模式寄存的(在 WINDOWS 平台上是 DLL 文件模式,在 UNIX 机器上是 SO 文件模式)。通过调用本地的库文件的外部办法,使 JAVA 能够实现和本地机器的紧密联系,调用零碎级的各接口办法,还是不明确?见文末参考,对本地办法定义与应用有具体介绍。当调用 Java 办法时,虚构机会创立一个栈桢并压入 Java 栈,而当它调用的是本地办法时,虚构机会放弃 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简略地动静连贯并间接调用指定的本地办法。

JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
   // 缓存 String 的 class
   jclass jc = (*env)->FindClass(env, STRING_PATH);
}

如上代码所示,当 java 调用以上本地办法时,jc 会被本地办法栈压入栈中, jc 就是咱们说的本地办法栈中 JNI 的对象援用,因而只会在此本地办法执行实现后才会被开释。

垃圾回收次要办法

上一节咱们晓得了能够通过可达性算法来辨认哪些数据是垃圾,那该怎么对这些垃圾进行回收呢。次要有以下几种形式形式

1、标记革除算法
2、复制算法
3、标记整顿法

标记革除算法

步骤很简略
1、先依据可达性算法标记出相应的可回收对象(图中黄色局部)
2、对可回收的对象进行回收

操作起来的确很简略,也不必做挪动数据的操作,那有啥问题呢?认真看上图,没错,内存碎片!如果咱们想在上图中的堆中调配一块须要间断内存占用 4M 或 5M 的区域,显然是会失败,怎么解决呢,如果能把下面未应用的 2M,2M,1M 内存能连起来就能连成一片可用空间为 5M 的区域即可,怎么做呢?

复制算法

把堆等分成两块区域, A 和 B,区域 A 负责调配对象,区域 B 不调配, 对区域 A 应用以上所说的标记法把存活的对象标记进去(下图有误无需革除),而后把区域 A 中存活的对象都复制到区域 B(存活对象都顺次紧邻排列)最初把 A 区对象全副清理掉开释出空间,这样就解决了内存碎片的问题了。

不过复制算法的毛病很显著,比方给堆调配了 500M 内存,后果只有 250M 可用,空间平白无故缩小了一半!这必定是不能承受的!另外每次回收也要把存活对象挪动到另一半,效率低下(咱们能够想想删除数组元素再把非删除的元素往一端移,效率显然堪忧)

标记整顿法

后面两步和标记革除法一样,不同的是它在标记革除法的根底上增加了一个整顿的过程,行将所有的存活对象都往一端挪动, 紧邻排列(如图示),再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。

然而毛病也很显著:每进一次垃圾革除都要频繁地挪动存活的对象,效率非常低下。

分代收集算法

分代收集算法整合了以上算法,综合了这些算法的长处,最大水平防止了它们的毛病,所以是古代虚拟机采纳的首选算法, 与其说它是算法,倒不是说它是一种策略,因为它是把上述几种算法整合在了一起,为啥须要分代收集呢,来看一下对象的调配有啥法则

如图示:纵轴代表已调配的字节,而横轴代表程序运行工夫由图可知,大部分的对象都很长寿,都在很短的工夫内都被回收了(IBM 业余钻研表明,一般来说,98% 的对象都是朝生夕死的,通过一次 Minor GC 后就会被回收),所以分代收集算法依据对象存活周期的不同将堆分成新生代和老生代(Java8 以前还有个永恒代), 默认比例为 1 : 2,新生代又分为 Eden 区,from Survivor 区(简称 S0),to Survivor 区 (简称 S1), 三者的比例为 8: 1 : 1,这样就能够依据新老生代的特点抉择最合适的垃圾回收算法,咱们把新生代产生的 GC 称为 Young GC(也叫 Minor GC), 老年代产生的 GC 称为 Old GC(也称为 Full GC)。

1、对象在新生代的调配与回收由以上的剖析可知,大部分对象在很短的工夫内都会被回收,对象个别调配在 Eden 区

当 Eden 区将满时,触发 Minor GC

咱们之前怎么说来着,大部分对象在短时间内都会被回收, 所以通过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区(这就是为啥空间大小  Eden: S0: S1 = 8:1:1, Eden 区远大于 S0,S1 的起因,因为在 Eden 区触发的 Minor GC 把大部对象(靠近 98%)都回收了, 只留下大量存活的对象,此时把它们移到 S0 或 S1 入不敷出)同时对象年龄加一(对象的年龄即产生 Minor GC 的次数),最初把 Eden 区对象全副清理以开释出空间, 动图如下

当触发下一次 Minor GC 时,会把 Eden 区的存活对象和 S0(或 S1)中的存活对象(S0 或 S1 中的存活对象通过每次 Minor GC 都可能被回收)一起移到 S1(Eden 和 S0 的存活对象年龄 +1), 同时清空 Eden 和 S0 的空间。

若再触发下一次 Minor GC,则反复上一步,只不过此时变成了 从 Eden,S1 区将存活对象复制到 S0 区, 每次垃圾回收, S0, S1 角色调换,都是从 Eden ,S0(或 S1) 将存活对象挪动到 S1(或 S0)。也就是说在 Eden 区的垃圾回收咱们采纳的是复制算法,因为在 Eden 区调配的对象大部分在 Minor GC 后都沦亡了,只剩下极少局部存活对象(这也是为啥 Eden:S0:S1 默认为 8:1:1 的起因),S0,S1 区域也比拟小,所以最大限度地升高了复制算法造成的对象频繁拷贝带来的开销。2、对象何时降职老年代当对象的年龄达到了咱们设定的阈值,则会从 S0(或 S1)降职到老年代

如图示:年龄阈值设置为 15,当产生下一次 Minor GC 时,S0 中有个对象年龄达到 15,达到咱们的设定阈值,降职到老年代!大对象 当某个对象调配须要大量的间断内存时,此时对象的创立不会调配在 Eden 区,会间接调配在老年代,因为如果把大对象调配在 Eden 区, Minor GC 后再挪动到 S0,S1 会有很大的开销(对象比拟大,复制会比较慢,也占空间),也很快会占满 S0,S1 区,所以罗唆就间接移到老年代. 还有一种状况也会让对象降职到老年代,即在 S0(或 S1)区雷同年龄的对象大小之和大于 S0(或 S1)空间一半以上时,则年龄大于等于该年龄的对象也会降职到老年代。3、空间调配担保在产生 MinorGC 之前,虚构机会先查看老年代最大可用的间断空间是否大于新生代所有对象的总空间,如果大于,那么 Minor GC 能够确保是平安的, 如果不大于,那么虚构机会查看 HandlePromotionFailure 设置值是否容许担保失败。如果容许,那么会持续查看老年代最大可用间断空间是否大于历次降职到老年代对象的均匀大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。4、Stop The World 如果老年代满了,会触发 Full GC, Full GC 会同时回收新生代和老年代(即对整个堆进行 GC),它会导致 Stop The World(简称 STW), 造成挺大的性能开销。什么是 STW?所谓的 STW, 即在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其余工作线程则被挂起。

画外音:为啥在垃圾收集期间其余工作线程会被挂起?设想一下,你一边在收垃圾,另外一群人一边丢垃圾,垃圾能拾掇洁净吗。个别 Full GC 会导致工作线程进展工夫过长(因为 Full GC 会清理整个堆中的不可用对象,个别要花较长的工夫),如果在此 server 收到了很多申请,则会被拒绝服务!所以咱们要尽量减少 Full GC(Minor GC 也会造成 STW, 但只会触发轻微的 STW, 因为 Eden 区的对象大部分都被回收了,只有极少数存活对象会通过复制算法转移到 S0 或 S1 区,所以绝对还好)。当初咱们应该明确把新生代设置成 Eden, S0,S1 区或者给对象设置年龄阈值或者默认把新生代与老年代的空间大小设置成 1:2 都是为了尽可能地防止对象过早地进入老年代,尽可能晚地触发 Full GC。想想新生代如果只设置 Eden 会产生什么,结果就是每通过一次 Minor GC,存活对象会过早地进入老年代,那么老年代很快就会装满,很快会触发 Full GC,而对象其实在通过两三次的 Minor GC 后大部分都会沦亡,所以有了 S0,S1 的缓冲,只有多数的对象会进入老年代,老年代大小也就不会这么快地增长,也就防止了过早地触发 Full GC。因为 Full GC(或 Minor GC)会影响性能,所以咱们要在一个适合的工夫点发动 GC,这个工夫点被称为 Safe Point,这个工夫点的选定既不能太少以让 GC 工夫太长导致程序过长时间卡顿,也不能过于频繁以至于过分增大运行时的负荷。个别当线程在这个工夫点上状态是能够确定的,如确定 GC Root 的信息等,能够使 JVM 开始平安地 GC。Safe Point 次要指的是以下特定地位:循环的开端办法返回前调用办法的 call 之后抛出异样的地位 另外须要留神的是因为新生代的特点(大部分对象通过 Minor GC 后会沦亡),Minor GC 用的是复制算法,而在老生代因为对象比拟多,占用的空间较大,应用复制算法会有较大开销(复制算法在对象存活率较高时要进行屡次复制操作,同时节约一半空间)所以依据老生代特点,在老年代进行的 GC 个别采纳的是标记整顿法来进行回收。垃圾收集器品种如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java 虚拟机标准并没有规定垃圾收集器应该如何实现,因而一般来说不同厂商,不同版本的虚拟机提供的垃圾收集器实现可能会有差异,个别会给出参数来让用户依据利用的特点来组合各个年代应用的收集器,次要有以下垃圾收集器

在新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge 在老年代工作的垃圾回收器:CMS,Serial Old, Parallel Old 同时在新老生代工作的垃圾回收器:G1 图片中的垃圾收集器如果存在连线,则代表它们之间能够配合应用,接下来咱们来看看各个垃圾收集器的具体性能。新生代收集器 Serial 收集器 Serial 收集器是工作在新生代的,单线程的垃圾收集器,单线程意味着它只会应用一个 CPU 或一个收集线程来实现垃圾回收,不仅如此,还记得咱们上文提到的 STW 了吗,它在进行垃圾收集时,其余用户线程会暂停,直到垃圾收集完结,也就是说在 GC 期间,此时的利用不可用。看起来单线程垃圾收集器不太实用,不过咱们须要晓得的任何技术的应用都不能脱离场景,在 Client 模式下,它简略无效(与其余收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 单线程模式无需与其余线程交互,缩小了开销,分心做 GC 能将其单线程的劣势施展到极致,另外在用户的桌面利用场景,调配给虚拟机的内存个别不会很大,收集几十甚至一两百兆(仅是新生代的内存,桌面利用根本不会再大了),STW 工夫能够管制在一百多毫秒内,只有不是频繁产生,这点进展是能够承受的,所以对于运行在 Client 模式下的虚拟机,Serial 收集器是新生代的默认收集器 ParNew 收集器 ParNew 收集器是 Serial 收集器的多线程版本,除了应用多线程,其余像收集算法,STW, 对象调配规定,回收策略与 Serial 收集器实现一样,在底层上,这两种收集器也共用了相当多的代码,它的垃圾收集过程如下

ParNew 次要工作在 Server 模式,咱们晓得服务端如果接管的申请多了,响应工夫就很重要了,多线程能够让垃圾回收得更快,也就是缩小了 STW 工夫,能晋升响应工夫,所以是许多运行在 Server 模式下的虚拟机的首选新生代收集器,另一个与性能无关的起因是因为除了 Serial  收集器,只有它能与 CMS 收集器配合工作,CMS 是一个划时代的垃圾收集器,是真正意义上的并发收集器,它第一次实现了垃圾收集线程与用户线程(基本上)同时工作,它采纳的是传统的 GC 收集器代码框架,与 Serial,ParNew 共用一套代码框架,所以能与这两者一起配合工作,而后文提到的 Parallel Scavenge 与 G1 收集器没有应用传统的 GC 收集器代码框架,而是重整旗鼓独立实现的,另外一些收集器则只是共用了局部的框架代码, 所以无奈与 CMS 收集器一起配合工作。在多 CPU 的状况下,因为 ParNew 的多线程回收个性,毫无疑问垃圾收集会更快,也能无效地缩小 STW 的工夫,晋升利用的响应速度。Parallel Scavenge 收集器 Parallel Scavenge 收集器也是一个应用复制算法,多线程,工作于新生代的垃圾收集器,看起来性能和 ParNew 收集器一样,它有啥特别之处吗关注点不同,CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的进展工夫,而 Parallel Scavenge 指标是达到一个可管制的吞吐量(吞吐量 = 运行用户代码工夫 /(运行用户代码工夫 + 垃圾收集工夫)),也就是说 CMS 等垃圾收集器更适宜用到与用户交互的程序,因为进展工夫越短,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,所以更适宜做后盾运算等不须要太多用户交互的工作。Parallel Scavenge 收集器提供了两个参数来准确管制吞吐量,别离是管制最大垃圾收集工夫的 -XX:MaxGCPauseMillis 参数及间接设置吞吐量大小的 -XX:GCTimeRatio(默认 99%)除了以上两个参数,还能够用 Parallel Scavenge 收集器提供的第三个参数 -XX:UseAdaptiveSizePolicy,开启这个参数后,就不须要手工指定新生代大小,Eden 与 Survivor 比例(SurvivorRatio)等细节,只须要设置好根本的堆大小(-Xmx 设置最大堆), 以及最大垃圾收集工夫与吞吐量大小,虚拟机就会依据以后零碎运行状况收集监控信息,动静调整这些参数以尽可能地达到咱们设定的最大垃圾收集工夫或吞吐量大小这两个指标。自适应策略也是 Parallel Scavenge  与 ParNew 的重要区别!老年代收集器 Serial Old 收集器上文咱们晓得,Serial 收集器是工作于新生代的单线程收集器,与之绝对地,Serial Old 是工作于老年代的单线程收集器,此收集器的次要意义在于给 Client 模式下的虚拟机应用,如果在 Server 模式下,则它还有两大用处:一种是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合应用,另一种是作为 CMS 收集器的后备预案, 在并发收集产生 Concurrent Mode Failure 时应用(后文讲述), 它与 Serial 收集器配合应用示意图如下

Parallel Old 收集器 Parallel Old 是绝对于 Parallel Scavenge 收集器的老年代版本,应用多线程和标记整顿法,两者组合示意图如下, 这两者的组合因为都是多线程收集器,真正实现了「吞吐量优先」的指标

CMS 收集器 CMS 收集器是以实现最短 STW 工夫为指标的收集器,如果利用很器重服务的响应速度,心愿给用户最好的体验,则 CMS 收集器是个很不错的抉择!咱们之前说老年代次要用标记整顿法,而 CMS 尽管工作于老年代,但采纳的是标记革除法,次要有以下四个步骤初始标记并发标记从新标记并发革除

从图中能够的看到初始标记和从新标记两个阶段会产生 STW,造成用户线程挂起,不过初始标记仅标记 GC Roots 能关联的对象,速度很快,并发标记是进行 GC Roots  Tracing 的过程,从新标记是为了修改并发标记期间因用户线程持续运行而导致标记产生变动的那一部分对象的标记记录,这一阶段进展工夫个别比初始标记阶段稍长,但远比并发标记工夫短。整个过程中耗时最长的是并发标记和标记清理,不过这两个阶段用户线程都可工作,所以不影响利用的失常应用,所以总体上看,能够认为 CMS 收集器的内存回收过程是与用户线程一起并发执行的。然而 CMS 收集器远达不到完满的水平,次要有以下三个毛病 CMS 收集器对 CPU 资源十分敏感  起因也能够了解,比方原本我原本能够有 10 个用户线程解决申请,当初却要分出 3 个作为回收线程,吞吐量降落了 30%,CMS 默认启动的回收线程数是(CPU 数量 +3)/ 4, 如果 CPU 数量只有一两个,那吞吐量就间接降落 50%, 显然是不可承受的 CMS 无奈解决浮动垃圾(Floating Garbage), 可能呈现「Concurrent Mode Failure」而导致另一次 Full GC 的产生,因为在并发清理阶段用户线程还在运行,所以清理的同时新的垃圾也在一直呈现,这部分垃圾只能在下一次 GC 时再清理掉(即浮云垃圾),同时在垃圾收集阶段用户线程也要持续运行,就须要预留足够多的空间要确保用户线程失常执行,这就意味着 CMS 收集器不能像其余收集器一样等老年代满了再应用,JDK 1.5 默认当老年代应用了 68% 空间后就会被激活,当然这个比例能够通过 -XX:CMSInitiatingOccupancyFraction 来设置,然而如果设置地太高很容易导致在 CMS 运行期间预留的内存无奈满足程序要求,会导致 Concurrent Mode Failure 失败,这时会启用 Serial Old 收集器来从新进行老年代的收集,而咱们晓得 Serial Old 收集器是单线程收集器,这样就会导致 STW 更长了。CMS 采纳的是标记革除法,上文咱们曾经提到这种办法会产生大量的内存碎片,这样会给大内存调配带来很大的麻烦,如果无奈找到足够大的间断空间来调配对象,将会触发 Full GC,这会影响利用的性能。当然咱们能够开启 -XX:+UseCMSCompactAtFullCollection(默认是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整顿过程,内存整理会导致 STW,进展工夫会变长,还能够用另一个参数 -XX:CMSFullGCsBeforeCompation 用来设置执行多少次不压缩的 Full GC 后跟着带来一次带压缩的。G1(Garbage First)收集器 G1 收集器是面向服务端的垃圾收集器,被称为驾驭所有的垃圾回收器,次要有以下几个特点像 CMS 收集器一样,能与应用程序线程并发执行。整顿闲暇空间更快。须要 GC 进展工夫更好预测。不会像 CMS 那样就义大量的吞吐性能。不须要更大的 Java Heap 与 CMS 相比,它在以下两个方面体现更杰出运作期间不会产生内存碎片,G1 从整体上看采纳的是标记 - 整顿法,部分(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片,收集后提供规整的可用内存,这样有利于程序的长时间运行。在 STW 上建设了可预测的进展工夫模型,用户能够指定冀望进展工夫,G1 会将进展工夫管制在用户设定的进展工夫以内。为什么 G1 能建设可预测的进展模型呢,次要起因在于 G1 对堆空间的调配与传统的垃圾收集器不一器,传统的内存调配就像咱们前文所述,是间断的,分成新生代,老年代,新生代又分 Eden,S0,S1, 如下

而 G1 各代的存储地址不是间断的,每一代都应用了 n 个不间断的大小雷同的 Region,每个 Region 占有一块间断的虚拟内存地址,如图示

除了和传统的新老生代,幸存区的空间区别,Region 还多了一个 H,它代表 Humongous,这示意这些 Region 存储的是微小对象(humongous object,H-obj),即大小大于等于 region 一半的对象,这样超大对象就间接调配到了老年代,避免了重复拷贝挪动。那么 G1 调配成这样有啥益处呢?传统的收集器如果产生 Full GC 是对整个堆进行全区域的垃圾收集,而调配成各个 Region 的话,不便 G1 跟踪各个 Region 里垃圾沉积的价值大小(回收所取得的空间大小及回收所需经验值),这样依据价值大小保护一个优先列表,依据容许的收集工夫,优先收集回收价值最大的 Region, 也就防止了整个老年代的回收,也就缩小了 STW 造成的进展工夫。同时因为只收集局部 Region, 可就做到了 STW 工夫的可控。G1 收集器的工作步骤如下初始标记并发标记最终标记筛选回收

能够看到整体过程与 CMS 收集器十分相似,筛选阶段会依据各个 Region 的回收价值和老本进行排序,依据用户冀望的 GC 进展工夫来制订回收打算。总结本文简述了垃圾回收的原理与垃圾收集器的品种,置信大家对结尾提的一些问题应该有了更粗浅的意识,在生产环境中咱们要依据不同的场景来抉择垃圾收集器组合,如果是运行在桌面环境处于 Client 模式的,则用 Serial + Serial Old 收集器入不敷出,如果须要响应工夫快,用户体验好的,则用 ParNew + CMS 的搭配模式,即便是号称是「驾驭所有」的 G1,也须要依据吞吐量等要求适当调整相应的 JVM 参数,没有最牛的技术,只有最合适的应用场景,切记!实践有了,下一篇咱们会进入手动操作环节,咱们会一起来入手操作一些 demo,做一些试验,来验证咱们看到的一些景象,比方对象个别调配在新生代,什么状况下会间接到老年代,该怎么试验?产生了 OOM,该用哪些工具调试呢?等等,敬请期待!

正文完
 0