20、有哪几种垃圾回收器,各自的优缺点是什么? ⚡
垃圾回收器主要分为以下几种:Serial,Parallel,CMS,G1。
- Serial
- 单线程垃圾回收器,该线程运行时,其他线程都暂停,使用复制算法;
- 使用场景: 堆内存较小,适合个人电脑(CPU核数较小);
- 工作在老年区,回收算法:标记整理;工作在新生代,回收算法:复制;
- 单线程的垃圾回收器,只有一个垃圾回收线程在运行;
- 垃圾回收线程结束后,其他线程恢复运行;
- 触发垃圾回收时让所有线程在安全点停下【在垃圾回收过程中,对象的地址可能发生改变,为了保证安全使用对象地址,要求所有用户进程到达安全点后停下】
- Parallel (吞吐量优先)
- 多线程垃圾回收器,堆内存较大,需要多核CPU的支持,工作在服务器上。
- 目标: 让单位时间内,STW的时间最短
例如:1小时内发生2次垃圾回收,每次垃圾回收0.2s,所以1小时内一共花费了0.4s,垃圾回收时间在程序运行时间中的占比,占比越小,说明吞吐量越大 - 新生代垃圾回收器使用复制算法,老年代垃圾回收器使用标记整理算法。
- 工作在老年区,回收算法:标记整理;工作在新生代,回收算法:复制;
- 垃圾回收器会开启多个线程进行垃圾回收,垃圾回收时开启线程的个数默认与CPU核数相关;
- 垃圾回收线程结束后,其他线程恢复运行;
- CPU的使用程度:例如四核CPU,在触发吞吐量优先的垃圾回收器时,会同时开启4个线程进行垃圾回收,所以在很短时间内,CPU占用率达到100%。
- CMS (响应时间优先)
- 多线程垃圾回收器,堆内存较大,需要多核CPU的支持,工作在服务器上。
- 目标: 垃圾回收时,让单次STW的时间尽可能短
- 例如:1小时内发生5次垃圾回收,每次STW时间都是0.1s所以一小时内一共花费了0.5s
- 运作过程:
- 初始标记:初始标记时,需要STW时间很快,只会列出根对象;
- 并发标记:继续标记剩余的垃圾并且不用STW;
- 重新标记:重新标记那些由于并发标记中用户程序产生的新的垃圾,需要STW;
- 并发清理:清除标记垃圾。
- 在并发清理时,其他工作线程可能会产生新的垃圾->浮动垃圾,这些垃圾不能在本次垃圾回收时清理,需要等下一次垃圾回收时才会清理,所以要预留一些区间保存浮动垃圾;
- 并发:垃圾回收线程和用户线程可以并发运行,抢占时间片,进一步减少了STW的时间【新生代:复制算法,老年代:标记清除,当CMS垃圾回收器并发失败后,会采取措施:让老年代的垃圾回收器退化到串行时的单线程垃圾回收,这会导致响应时间变得很长】
- 由于CMS采用的是标记清除垃圾回收算法,可能会出现较多的内存碎片
- G1
- 把堆划分成多个大小相等的Region,新生代和老年代不再物理隔离,多核 CPU 和大内存的场景下有很好的性能。新生代使用复制算法,老年代使用标记-整理算法。
- 运作过程:
- 初始标记:标记根对象(STW)
- 并发标记:跟踪标记根对象所有可达对象,找出要回收的对象【当老年代占用到整个堆空间的45%(默认)时,触发并发标记 】
- 最终标记:对并发标记阶段产生新的垃圾进行标记(STW)
- 筛选回收:对各个region区域进行回收价值与成本的排序,为了达到目标的回收时间,G1会挑选Region中最有价值的回收区域进行垃圾回收(最少时间回收最多的垃圾)(STW)
特点:
- 空间整合: 整体来看是基于“标记–整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿: 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。
21、什么是Stop The World ? 什么是安全点?
Stop The World
- 进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为
Stop The World
。也简称为STW。
安全点
- 安全点是在程序执行期间的所有GC Root已知并且所有堆对象的内容一致的点。
- 当线程运行到这类位置时,堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等。
22、为什么需要STW?
在 java 应用程序中引用关系是不断发生变化的,那么就会有会有很多种情况来导致垃圾标识出错。
举个例子:
Object a 目前是个垃圾,GC 把它标记为垃圾,但是在清除前又有其他对象指向了 Object a,那么此刻 Object a 又不是垃圾了,那么如果没有 STW 就要去无限维护这种关系来去采集正确的信息。
23、详细说一下CMS的回收过程?
CMS(并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器,它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
CMS 回收过程分为以下四步:
- 初始标记:初始标记时,需要STW时间很快,只会列出根对象;
- 并发标记:续标记剩余的垃圾并且不用STW ;
- 重新标记:重新标记那些由于并发标记中用户程序产生的新的垃圾,需要STW ;
- 并发清除:清除标记垃圾,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。
24、CMS的问题是什么?
CMS 的问题:
- 并发回收导致CPU资源紧张:
- 在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。
- 无法清理浮动垃圾:
- 在并发清理时,其他工作线程可能会产生新的垃圾:浮动垃圾,这些垃圾不能在本次垃圾回收时清理,需要等下一次垃圾回收时才会清理,所以要预留一些区间保存浮动垃圾。
- 并发失败
- 由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX: CMSInitiatingOccupancyFraction 参数来设置。
- 这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。
- 内存碎片比较多
- CMS 是使用了标记清除的算法去清理垃圾的,而这种算法的缺点就是会产生碎片化,后续可能会导致大对象无法分配从而触发和 Serial Old 一起配合使用来处理碎片化的问题,当然这也处于 STW的情况下,所以当 java 应用非常庞大时,如果采用了 CMS 垃圾回收器,产生了碎片化,那么在 STW 来处理碎片化的时间会非常之久。
25、详细说一下G1的回收过程?
G1把堆划分成多个大小相等的Region,新生代和老年代不再物理隔离,多核 CPU 和大内存的场景下有很好的性能。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器。
G1 回收过程分为以下四步:
- 初始标记:标记根对象(STW)
- 并发标记:跟踪标记根对象所有可达对象,找出要回收的对象【当老年代占用到整个堆空间的45%(默认)时,触发并发标记 】
- 最终标记:对并发标记阶段产生新的垃圾进行标记(STW)
- 筛选回收:对各个region区域进行回收价值与成本的排序,为了达到目标的回收时间G1会挑选Region中最有价值的回收区域进行垃圾回收(最少时间回收最多的垃圾(STW)
特点:
- 空间整合: 整体来看是基于“标记–整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿: 能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。
回收时机:G1在实现垃圾回收时一共提供了3种回收的方法,分别是新生代收集(Young GC),混合收集(Mixed GC),整堆收集(Full GC),这3种垃圾回收触发的时机通常如下:
- 应用程序分配对象时,发现内存不足,触发Young GC;
- 在Young GC执行中,判断整体内存使用是否大于一定的阈值,如果大于启动并发标记;在并发标记完成后,当下一次启动垃圾回收称为Mixed GC,在Mixed GC执行过程中不仅回收新生代分区,同时也回收部分老生代分区;
- 在用程序分配对象时,发现内存不足,触发Young GC或者Mixed GC;垃圾回收结束后再次尝试分配对象,如果内存还不足,此时将触发Full GC。
26、什么是三色标记算法?
三色标记算法,也就是垃圾回收器标记垃圾的时候使用的算法,简单来说,就是将对象分为三种颜色:
- 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
标记过程:
- 在 GC 并发开始的时候,所有的对象均为白色;
- 在将所有的 GC Roots 直接应用的对象标记为灰色集合;
- 如果判断灰色集合中的对象不存在子引用,则将其放入黑色集合,若存在子引用对象,则将其所有的子引用对象存放到灰色集合,当前对象放入黑色集合;
- 按照此步骤 3 ,依此类推,直至灰色集合中所有的对象变黑后,本轮标记完成,并且在白色集合内的对象称为不可达对象,即垃圾对象;
- 标记结束后,为白色的对象为 GC Roots 不可达,可以进行垃圾回收。
三色标记算法的缺点
- 浮动垃圾:并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,由于不会再对黑色标记过的对象重新扫描,所以不会被发现,那么这个对象不是白色的但是不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,浮动垃圾对系统的影响不大,留给下一次GC进行处理即可。
- 我们知道在并发标记的时候可能会出现误标的情况,这里举两个例子:
- 刚开始标记为垃圾的对象,但是在并发标记过程中变为了存活对象;
- 刚开始标记为存活的对象,但是在并发标记过程中变为了垃圾对象。
第一种情况影响还不算很大,只是相当于垃圾没有清理干净,待下一次清理的时候再清理一下就好了。第二种情况就危险了,正在使用的对象的突然被清理掉了,后果会很严重。那么产生上述第二种情况的原因是什么呢?
- 新增了一条或多条黑色到白色对象的新引用
- 删除了灰色对象到该白色对象的直接引用或间接引用。
当这两种情况都满足的时候就会出现这种问题了。所以为了解决这个问题,引入了增量更新 (Incremental Update)和 原始快照 (SATB)的方案:
- 增量更新破坏了第一个条件增加新引用时记录 该引用信息,在后续 STW 扫描中重新扫描(CMS的使用方案)。
- 原始快照破坏了第二个条件:删除引用时记录下来,在后续 STW 扫描时将这些记录过的灰色对象为根再扫描一次(G1的使用方案)。
27、有了CMS,为什么还要引入G1?
CMS最主要的优点在并发清除,低停顿,CMS同样有三个明显的缺点:
- 内存碎片比较多
- 并发回收导致CPU资源紧张
- 无法清理浮动垃圾
G1整体来看是基于“标记–整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
28、CMS和G1的区别?
- 使用的范围不一样:
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用。
- G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用 。
- STW的时间
- CMS收集器以最短的停顿时间为目标的收集器。
- G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)。
- 内存碎片
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片。
- G1收集器使用的是“标记-整理”算法,进行了空间整合,基本不会内存空间碎片。
- 回收过程不一样
- CMS是初始标记、并发标记、重新标记、并发清理。
- G1是初始标记、并发标记、最终标记、筛选回收。
29、JDK1.8默认垃圾收集器是什么?一般推荐用哪个垃圾回收器?
查看默认垃圾回收器:
java -XX:+PrintCommandLineFlags -version
JDK默认是Parallel (吞吐量优先)垃圾回收器,在并发并不是非常高的情况下,可以尽可能的利用处理器资源。
如果想要提高服务的响应速度,可以采用CMS来降低停顿时间,或者采用了设计比较优秀的G1垃圾收集器,因为它不仅满足我们低停顿的要求,而且解决了CMS的浮动垃圾问题、内存碎片问题。
30、垃圾回收器应该怎么选择?
垃圾回收器的适用场景:
- Serial: 堆内存较小,适合个人电脑(CPU核数较小);
- Parallel:堆内存较大,需要多核CPU的支持,工作在服务器上,优先考虑应用程序的峰值性能;
- CMS/G1:如果响应时间比吞吐量优先级高,或者垃圾收集暂停必须保持在大约1秒以内。
31、JVM中一次完整的GC是什么样子的? ⚡
- 对象优先在Eden分配:
- 新生代空间不足时,会触发Minor GC,Eden和From存活的对象使用Copy复制到to中,存活的对象年龄加1并且交换from和 to
- Minor GC会引发Stop The World: 垃圾回收时会暂停其他用户线程,直到垃圾回收线程工作完了,才能恢复其他用户线程的运行,该暂停时间较短
- 当对象寿命超过阈值时【最大为15次:因为存储对象寿命的长度为4bit,所以最大能存储15】,会晋升到老年代
- 当老年代空间不足时,会先尝试触发Minor GC,如果触发Minor GC之后,空间仍不足,会触发Full GC,Full GC 清理整个内存堆 – 包括新生代和老年代,STW时间更长, Full GC的垃圾回收算法是标记清除或是标记整理。
- 如果Full GC之后内存仍不足,就会报错。
32、Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC都是什么意思?
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
33、Minor GC 和 Full GC 有什么不同呢?
- Minor GC:只是新生代的垃圾收集。
- Full GC: 收集整个堆,包括新生代,老年代,方法区(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间实现)
34、Minor GC/Full GC 触发条件?
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
- Minor GC之前检查老年代:在要进行 Minor GC 的时候,发现老年代可用的连续内存空间 < 新生代历次Minor GC后升入老年代的对象总和的平均大小,说明本次Minor GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC。
- Minor GC之后老年代空间不足:执行Minor GC之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次Full GC
- 老年代空间不足,老年代内存使用率过高,达到一定比例,也会触发Full GC。
- 空间分配担保失败( Promotion Failure),新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
- 方法区内存空间不足:如果方法区由永久代实现,永久代空间不足 Full GC。
- System.gc()等命令触发:System.gc()、jmap -dump 等命令会触发 full gc。
35、对象什么时候会进入老年代?
- 长期存活的对象将进入老年代
- 每次Minor GC之后存活的对象年龄加1,当对象寿命超过阈值时【最大为15次:因为存储对象寿命的长度为4bit,所以最大能存储15】,会晋升到老年代。
- 大对象直接进入老年代
- 有一些占用大量连续内存空间的对象在被加载就会直接进入老年代。这样的大对象一般是一些数组,长字符串之类的对象。
- 空间分配担保
- 假如在Young GC之后,新生代仍然有大量对象存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。
- 动态对象年龄判定
- 为了能更好地适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄必须达到阈值15才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
36、对象一定分配在堆中吗?有没有了解逃逸分析技术?
对象一定分配在堆中吗?
不一定的。
- 随着JIT编译期的发展与逃逸分析技术逐渐成熟,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。其实,在编译期间,JIT会对代码做很多优化。其中有一部分优化的目的就是减少内存堆分配压力,其中一种重要的技术叫做逃逸分析。
什么是逃逸分析?
- 通俗点讲,当一个对象被new出来之后,它可能被外部所调用,如果是作为参数传递到外部了,就称之为方法逃逸。
- 除此之外,如果对象还有可能被外部线程访问到,例如赋值给可以在其它线程中访问的实例变量,这种就被称为线程逃逸。
逃逸分析的好处
- 栈上分配
- 如果确定一个对象不会逃逸到线程之外,那么就可以考虑将这个对象在栈上分配,对象占用的内存随着栈帧出栈而销毁,这样一来,垃圾收集的压力就降低很多。
- 同步消除
- 线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施( 锁机制 )也就可以安全地消除掉。
- 变量替换
- 假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么可以不创建对象,直接创建若干个成员变量代替,可以让对象的成员变量在栈上分配和读写。
37、JVM新生代中为什么要分为Eden和Survivor
- 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC,老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比 Minor GC长得多,所以需要分为Eden和Survivor。
- Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
- 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分 的存活对象占用连续的内存空间,避免了碎片化的发生)