100m等于多少g(100m等于多少mm)

点击上面轻松关注!及时获取有趣且信息丰富的技术文章。我之前整理过JVM系列的文章。第一季总结:从简单到深入JAVA内存管理核心故事第二季总结:JAVA内存管理由

点击上面轻松关注!及时获取有趣且信息丰富的技术文章。

100m等于多少g(100m等于多少mm)插图

我之前整理过JVM系列的文章。

第一季总结:从简单到深入JAVA内存管理核心故事

第二季总结:JAVA内存管理由浅入深的核心故事

下面这篇文章也挺好的。分享出来,大家在家也能学好。无论是面试还是升职,JVM都是不可或缺的。

与Java C/C++相比,最显著的特点是引入了自动垃圾收集(以下简称GC自动垃圾收集)。它解决了C/C++最麻烦的内存管理问题,让程序员可以专注于程序本身,而不用担心恼人的内存收集问题。这也是Java受欢迎的重要原因之一。GC真正释放了程序员的生产力。但是程序员很难感知到它的存在。就好像,我们吃完饭,把盘子放在桌子上就走了,服务员会帮你收拾。你不会在意服务员什么时候收,怎么收。

有人说既然已经自动清理了GC,那我们不了解GC似乎也没什么问题。大多数情况下没有问题,但是如果涉及到一些性能调优、故障排除等。,深入了解GC还是很有必要的。曾经,美团通过调整JVM的相关GC参数,将服务响应时间TP90和TP99降低了10ms+,服务可用性大幅提升!所以,深入了解GC是成为一名优秀Java程序员的必修课!

垃圾收集分为两部分。第一部分将讲述垃圾收集理论,主要包括

GC 的几种主要的收集方法:标记清除、标记整理、复制算法的原理与特点,各自的优劣势为啥会有 Serial ,CMS, G1 等各式样的回收器,各自的优劣势是什么,为啥没有一个统一的万能的垃圾回收器新生代为啥要设置成 Eden, S0,S1 这三个区,基于什么考虑呢堆外内存不受 GC 控制,那该怎么释放呢对象可回收,就一定会被回收吗?什么是 SafePoint,什么是 Stop The World

下一部分主要讲垃圾收集的实践,主要包括

GC 日志格式怎么看主要有哪些发生 OOM 的场景发生 OOM,如何定位,常用的内存调试工具有哪些

本文将从以下几个方面来阐述垃圾回收。

JVM 内存区域如何识别垃圾引用计数法可达性算法垃圾回收主要方法标记清除法复制法标记整理法分代收集算法垃圾回收器对比

文字很多,但也加了很多GC动画,方便读者理解。相信看完会有很多收获。

JVM存储区

要了解垃圾收集的机制,首先要知道垃圾收集主要收集哪些数据,这些数据主要在哪个区域,那么我们就来看看JVM的内存区域。

虚拟机栈:描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈桢(下文会看到),主要保存执行方法时的局部变量表、操作数栈、动态连接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据,入栈出栈的时机很明确,所以这块区域不需要进行 GC。本地方法栈:与虚拟机栈功能非常类似,主要区别在于虚拟机栈为虚拟机执行 Java 方法时服务,而本地方法栈为虚拟机执行本地方法时服务的。这块区域也不需要进行 GC程序计数器:线程独有的, 可以把它看作是当前线程执行的字节码的行号指示器,比如如下字节码内容,在每个字节码`前面都有一个数字(行号),我们可以认为它就是程序计数器存储的内容记录这些数字(指令地址)有啥用呢,我们知道 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根的对象开始,通向它们指向的下一个节点,然后通向这个节点指向的下一个节点。。。(所以一条串过GC根的线叫做引用链),直到遍历完所有节点。如果相关对象不在任何以GC根为起点的引用链中,这些对象将被判断为“垃圾”,并被GC回收。

如图,如果使用可达性算法,可以解决上面提到的循环引用问题,A和B因为没有从GC根到达A和B,所以可以回收。

a、b物体可以回收,就一定要回收?不,对象的finalize方法给了对象一个死的机会。当对象不可达(可回收)时,发生GC时,会先判断对象是否执行了finalize方法。如果没有,它将首先执行finalize方法。在这个方法中,我们可以将当前对象与GC根相关联,这样在finalize方法执行后,GC将再次确定该对象是否可达,如果不可达,它将被回收。

注意:finalize方法只会执行一次。如果finalize方法是第一次执行,那么当对象变得可访问时,它将不会被回收。但是,如果对象再次被GC,finalize方法将被忽略,对象将被回收!记住这个!

那么这些GC根是什么呢?哪些对象可以作为GC根?有以下几类。

虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中 JNI(即一般说的 Native 方法)引用的对象虚拟机堆栈中引用的对象

如下面的代码所示,A是堆栈框架中的一个局部变量。当a = null时,由于此时A作为GC根,A与原来指向的实例new Test()断开连接,因此对象将被回收。

publicclass Test { public static void main(String[] args) { Test a = new Test(); a = null; }}方法中类的静态属性引用的对象。

如下面的代码所示,当堆栈框架中的局部变量a = null时,A原本指向的对象会因为与GC根(变量A)断开连接而被回收,而且由于我们给S赋了一个变量引用,此时S是一个类静态属性引用,充当GC根,它指向的对象还活着!

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在本地方法栈中的对象引用,所以只有在本地方法被执行后才会被释放。

垃圾回收的主要方法

上一节我们学习了哪些数据可以被可达性算法识别为垃圾,那么如何回收这些垃圾呢?主要有以下几种方式。

标记清除算法复制算法标记整理法标记算法

步骤很简单。

先根据可达性算法标记出相应的可回收对象(图中黄色部分)对可回收的对象进行回收操作起来确实很简单,也不用做移动数据的操作,那有啥问题呢?仔细看上图,没错,内存碎片!假如我们想在上图中的堆中分配一块需要连续内存占用 4M 或 5M 的区域,显然是会失败,怎么解决呢,如果能把上面未使用的 2M, 2M,1M 内存能连起来就能连成一片可用空间为 5M 的区域即可,怎么做呢?复制算法

把堆分成A和B两个区域,A区负责分配对象,B区不负责。对于A区,使用上述标记方法标记幸存对象(无需清除下图),然后将A区所有幸存对象复制到B区(所有幸存对象依次相邻排列)。最后将A区的对象全部清空释放到空,解决了内存碎片问题。

然而,复制算法的缺点是显而易见的。比如500M内存分配给堆,结果只有250M可用,空之间的间隔无缘无故减少一半!这肯定是不能接受的!另外,每次回收都要把幸存的对象移到另一半,效率很低(可以考虑删除数组元素,然后把没删除的元素移到一端,效率明显堪忧)。

标记整理方法

前两步与标记清除法相同,不同的是它在标记清除法的基础上增加了一个排序过程,即将所有存活的对象移到一端,一个挨着一个排列(如图),然后将另一端的区域全部清除,解决了内存碎片问题。

但是缺点也很明显:每次清除垃圾,幸存的对象都要频繁移动,效率非常低。

世代收集算法

代收集算法综合了上述算法,综合了这些算法的优点,最大程度地避免了它们的缺点,因此是现代虚拟机采用的首选算法。与其说是算法,不如说是策略,因为它集成了上述算法。为什么需要代收集?我们来看看对象分配的规则。

例如,纵轴代表分配的字节,而横轴代表程序运行时间。

从图中可以看出,大部分对象都是短命的,在很短的时间内被回收(IBM专业研究表明,一般来说,98%的对象在生日当天死亡,经过一次小的GC后就会被回收),所以分代回收算法根据对象生命周期的不同将堆分为新的一代和旧的一代(Java8之前有一个永久的一代),默认比例为1: 2。还分为伊甸园区,从幸存者区(简称S0)到新生代幸存者区(简称S1),比例为8: 1: 1,这样就可以根据新生代和老一代的特点选择最合适的垃圾收集算法。我们把新生代的GC称为年轻GC(也叫小GC),老一代的GC称为老GC(也叫全GC)。

画外音:想想吧。为什么新一代要分那么多区?

那么分代垃圾收集是如何工作的呢?让我们来看看。

代收集的工作原理

1.新生代物体的分布与恢复。

根据以上分析,大部分对象会在短时间内被回收,对象一般分布在伊甸园区域。

当伊甸园区域将满时触发次要垃圾收集。

我们之前怎么说的?大部分对象会在短时间内被回收,所以只有少数对象会在次要GC后存活,它们会被移动到S0区域(这就是为什么大小在空 Eden: S0: S1 = 8:1:1之间,Eden区域比S0,S1大很多,因为大部分对象(接近98%)被Eden区域触发的次要GC回收,只剩下少数存活的对象。此时,将它们移动到S0或S1就足够了。同时,对象的年龄增加1(对象的年龄是次要GC出现的次数)。最后将Eden区的对象全部清理到release 空。动画如下

当下一次次要GC被触发时,伊甸区的幸存对象和S0(或S1)的幸存对象(S0或S1的幸存对象可能在每次次要GC后被回收)将被移动到S1(伊甸和S0 +1的幸存对象年龄),伊甸和S0的空同时被清除。

如果下一个次要的GC被触发,前面的步骤将被重复,但此时,幸存的对象将从S1区的伊甸复制到S0区。每次垃圾收集时,S0、S1的角色都会互换,幸存的对象会从伊甸园、S0(或S1)移到S1(或S0)。也就是说,我们在伊甸区使用复制算法进行垃圾收集,因为伊甸区分配的大部分对象在小GC后都消亡了,只剩下少数存活的对象(这也是为什么伊甸: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发生之前,虚拟机首先检查旧时代中的最大可用连续空间隔是否大于新时代中所有对象的总空间隔。如果是,次要GC可以保证其安全性。如果不是,虚拟机检查HandlePromotionFailure设置是否允许保证失败。如果是,它将继续检查旧年龄的最大可用连续间隔空是否大于提升到旧年龄的对象的平均大小。如果是这样,将执行较小的GC;否则,可能会执行完整的GC。

4、停止世界

如果陈年是全GC,就会触发全GC。全GC会同时回收新世代和旧时代(即GC整个堆),这会导致停世界(简称STW),造成相当大的性能开销。

什么是STW?所谓STW,是指在GC(次要GC或完全GC)期间,只有垃圾收集器线程在工作,而其他工作线程被挂起。

画外音:为什么在垃圾收集期间其他工作线程会被挂起?想象一下,当你在收垃圾的时候,另一群人在扔垃圾。垃圾能清理吗?

一般来说,完全GC会导致工作线程暂停太久(因为完全GC会清理整个堆中不可用的对象,这通常需要很长时间)。如果这个服务器收到很多请求,它将被拒绝服务!所以要尽量减少满GC(小GC也会造成STW,但只会引发轻微的STW,因为伊甸区的大部分对象都被回收了,只有少数幸存的对象会通过复制算法转移到S0或S1区,所以相对来说比较好)。

现在我们应该明白,将新生代设置为伊甸、S0、S1,或者为对象设置年龄阈值,或者将新生代和老年之间的空的大小默认设置为1:2,是为了避免对象尽早进入老年,尽可能晚地触发完全GC。想想如果伊甸园只设定在新生代会怎么样。其后果是,每次通过Minor GC,存活的对象都会过早进入老年期,所以老年期很快就满了,触发Full GC。但是,大多数对象实际上会在两三次较小的GC通过后消亡,因此有了S0和S1的缓冲,只有少数对象会进入老年期,老年期不会增长得那么快,从而避免过早地触发完全GC。

由于完全GC(或次要GC)会影响性能,我们应该在一个合适的时间点启动GC,这个时间点称为安全点。这个时间点不能选得太小,以免GC时间太长,导致程序阻塞太久,也不能选得太频繁,以免过度增加运行时负载。一般来说,当可以确定这个时间点的线程状态时,比如确定GC根的信息,JVM就可以安全地启动GC了。安全点主要指以下具体位置:

循环的末尾方法返回前调用方法的 call 之后抛出异常的位置 另外需要注意的是由于新生代的特点(大部分对象经过 Minor GC后会消亡), Minor GC 用的是复制算法,而在老生代由于对象比较多,占用的空间较大,使用复制算法会有较大开销(复制算法在对象存活率较高时要进行多次复制操作,同时浪费一半空间)所以根据老生代特点,在老年代进行的 GC 一般采用的是标记整理法来进行回收。垃圾收集器类型

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范并没有规定如何实现垃圾收集器,所以一般来说,不同厂商、不同版本的虚拟机可能会提供不同的垃圾收集器实现。通常会给出参数,允许用户根据应用的特点组合不同年代使用的垃圾收集器,主要包括以下垃圾收集器

在新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge在老年代工作的垃圾回收器:CMS,Serial Old, Parallel Old同时在新老生代工作的垃圾回收器:G1

如果图中的垃圾收集器连接在一起,就可以一起使用。接下来,我们来看看每个垃圾收集器的具体功能。

新生代收集器串行收集器

串行收集器是新一代的单线程垃圾收集器。单线程意味着它将只使用一个CPU或一个收集线程来完成垃圾收集。不仅如此,还记得我们上面提到的STW吗?当它在收集垃圾时,其他用户线程将暂停,直到垃圾收集结束,这意味着在GC期间,此时的应用程序不可用。

单线程垃圾收集器似乎不是很实用,但是任何我们需要了解的技术的使用都不能脱离场景。在客户端模式下,简单有效(相比其他收集器的单线程)。对于单CPU受限的环境,串行单线程模式不需要与其他线程交互,降低了开销。专心GC可以充分发挥单线程的优势。此外,在用户的桌面应用场景下,分配给虚拟机的内存一般不会很大,几十甚至一两百兆(只有新一代内存,桌面应用不会再大),STW时间可以控制在一百毫秒以上。只要不经常发生,这种暂停是可以接受的,因此对于在客户端模式下运行的虚拟机,串行收集器是新一代的默认收集器。

Par收集器

PAR收集器是串行收集器的多线程版本。除了多线程之外,收集算法、STW、对象分配规则和收集策略等其他方面与串行收集器相同。在底层,这两个收集器也共享相当多的代码,它们的垃圾收集过程如下

PAR主要工作在服务器模式。我们知道,如果服务器接收到更多的请求,响应时间非常重要。多线程可以让垃圾收集更快,也就是可以减少STW时间,提高响应时间,所以是很多运行在服务器模式下的虚拟机首选的新一代收集器。另一个和性能无关的原因是,除了串口采集器,只有它能和CMS采集器一起工作。CMS是划时代的垃圾收集器,是真正的并发收集器。它第一次实现了垃圾收集线程和用户线程(基本上)同时工作。它采用传统的GC收集器代码框架,与Serial和Parnew共享一个代码框架,因此可以与它们协同工作。然后,本文中提到的并行清除和G1收集器是独立实现的,而不是传统的GC收集器代码框架,而其他收集器只共享部分框架代码,因此无法与CMS收集器协同工作。

在多CPU的情况下,由于ParNew的多线程回收特性,垃圾回收无疑会更快,这也可以有效减少STW时间,提高应用的响应速度。

平行花葶收集器

并行清除收集器也是一个垃圾收集器,它使用复制算法、多线程,并在新一代中工作。它的功能似乎和ParNew collector一样。它有什么特别之处?

关注点不同,CMS等垃圾收集器侧重于尽可能缩短用户线程在垃圾收集过程中的暂停时间,而并行清除的目标是实现一个可控的吞吐量(吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间))。也就是说,CMS等垃圾收集器更适合与用户交互的程序,因为暂停时间越短,用户体验越好,而并行的Scavenge收集器侧重于吞吐量,所以更适合后台操作等不需要太多用户交互的任务。

并行清除收集器提供了两个参数来精确控制吞吐量,即-XX:MaxGCPauseMillis参数来控制最大垃圾收集时间,以及-XX:GCTimeRatio(默认为99%)来直接设置吞吐量。

除了以上两个参数,还可以使用并行清除收集器提供的第三个参数-XX:UseAdaptiveSizePolicy。开启该参数后,不需要手动指定新生代大小、Eden、幸存者比例等细节。只需设置基本堆大小(-Xmx设置最大堆大小)、最大垃圾收集时间和吞吐量大小,虚拟机会根据当前系统运行情况收集监控信息,动态调整这些参数,尽可能达到最大垃圾收集时间或吞吐量大小。自适应策略也是并行清除和ParNew的重要区别!

老年收藏家串行收集器

从上面我们知道,串行收集器是工作在新一代的单线程收集器。相比之下,Serial Old是一个单线程的收集器,工作在晚年。这个收集器的主要作用是供虚拟机在客户端模式下使用。如果在服务器模式下,它也有两个用途:一是配合JDK 1.5及之前版本的并行清除使用,二是在并发收集失败时作为CMS收集器的备份方案(后面会介绍)。其与串行采集器的使用示意图如下。

平行旧收集器

Parallel Old是Parallel Scavenge collector的旧版本,使用多线程和标记排序方法。两者的组合图如下。因为两者都是多线程收集器,真正实现了“吞吐量第一”的目标。

CMS收集器

CMS收集器是一个以实现最短STW时间为目标的收集器。如果应用非常重视服务响应速度,想要给用户最好的体验,CMS collector是非常好的选择!

之前我们说老年主要使用标记排序法,而CMS虽然在老年工作,但是使用标记清除法,主要包括以下四个步骤

初始标记并发标记重新标记并发清除

从图中可以看出,STW会在初始标记和重新标记时发生,导致用户线程挂起。但是初始标记只标记可以和GC根关联的对象,速度非常快。并发标记是GC根追踪的过程。重新标记是对并发标记时由于用户线程的持续运行而导致标记发生变化的一些对象的标记记录进行修正。这一阶段的停顿时间一般比初始打标阶段稍长,但远短于并发打标时间。

整个过程中时间最长的是并发打标和打标清洗。但是,用户线程可以在这两个阶段工作,因此不会影响应用程序的正常使用。因此,总的来说,可以认为CMS collector的内存回收过程是与用户线程并发执行的。

然而,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(垃圾优先)收集器

G1是一个面向服务的垃圾收集器,被称为控制一切的垃圾收集器。它具有以下特点。

像 CMS 收集器一样,能与应用程序线程并发执行。整理空闲空间更快。需要 GC 停顿时间更好预测。不会像 CMS 那样牺牲大量的吞吐性能。不需要更大的 Java Heap

与CMS相比,它在以下两个方面表现更好

运作期间不会产生内存碎片,G1 从整体上看采用的是标记-整理法,局部(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片,收集后提供规整的可用内存,这样有利于程序的长时间运行。在 STW 上建立了可预测的停顿时间模型,用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内。

为什么G1可以建立一个可预测的停顿模型?主要原因是G1对heap 空的分配不同于传统的垃圾收集器。正如我们前面提到的,传统的内存分配是连续的,分为新生代、老年期,而新生代又分为伊甸、S0、S1,如下

G1各代的存储地址并不连续,每代使用N个大小相同的不连续区域,每个区域占用一个连续的虚拟内存地址,如图所示。

Region除了生存区域的空和传统的新旧代的区别之外,还有一个额外的H,代表humongous object,意思是这些区域存储了巨大的对象(H-obj),也就是大小大于等于Region一半的对象,这样超大型对象就直接分配到老年,防止重复复制和移动。那么这样的G1分布有什么优势呢?

在传统收集器中,如果使用全GC从整个区域的整个堆中收集垃圾并分配到各个区域,便于G1跟踪各个区域的垃圾累积值(回收得到的空之间的大小和回收需要的经验值),这样根据值维护一个优先级列表,根据允许的收集时间优先收集回收值最高的区域,从而避免整个老年期的回收,减少STW。同时,由于只采集部分区域,STW时间可以控制。

G1收集器的工作方式如下

初始标记并发标记最终标记筛选回收

可以看出,整个过程与CMS collector非常相似。在筛选阶段,对各个区域的回收价值和成本进行排序,根据用户期望的GC暂停时间制定回收计划。

摘要

本文简要介绍了垃圾收集的原理和垃圾收集器的类型。我相信你应该对开头提到的一些问题有了更深入的了解。在生产环境中,要根据不同的场景选择垃圾收集器的组合。如果是在桌面环境下以客户端模式运行,使用串口+串口旧收集器绰绰有余。如果需要快速的响应时间和良好的用户体验,那么,有了ParNew+CMS的搭配模式,即使是号称“控制一切”的G1,也需要根据吞吐量等需求适当调整相应的JVM参数。没有最好的技术,只有最适合的使用场景。记住!

有了理论,下一篇文章我们就进入手动操作环节。我们将一起操作一些演示,并做一些实验来验证我们看到的一些现象。比如对象一般分布在新生代,什么情况下会直接到老年?应该怎么实验?当OOM发生时,应该使用哪些工具进行调试?等等,继续关注!

涉及

堆外内存恢复机制分析

调用java本地方法-JNI 简介

从头到尾说说Java垃圾收集吧,

深入了解Java虚拟机

G1 GC Java热点的若干关键技术

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。

作者:美站资讯,如若转载,请注明出处:https://www.meizw.com/n/153134.html

发表回复

登录后才能评论