1 垃圾回收机制
1.1 判断对象的存活(重点)
在堆里面存放着几乎所有的对象实例,垃圾回收器在对对进行回收前,要做的事情就是确定这些对象中哪些还是"存活"着,哪些已经"死去"(死去代表着不可能再被任何途径使用得对象了)
什么是垃圾?
C语言申请内存:mallocfree C++: newdelete C/C++ 手动回收内存 Java:new Java是自动内存回收,编程上简单,系统不容易出错。 手动释放内存,容易出两种类型的问题: 1 、忘记回收 2 、多次回收 没有任何引用指向的一个对象或者多个对象(循环引用)
1.1.1 引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1 ,当引用失效时,计数器减 1.
Python在用,但主流虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,这样做影响效率,
在代码中看到,只保留相互引用的对象还是被回收掉了,说明JVM中采用的不是引用计数法。
1.1.2 可达性分析
(面试时重要的知识点,牢记)
来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为"GCRoots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(ReferenceChain),当一个对象到GCRoots没有任何引用链相连时,则证明此对象是不可用的。
作为GCRoots的对象包括下面几种(重点是前面 4 种):
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
- 方法区中类静态属性引用的对象; java 类的引用类型静态变量。
- 方法区中常量引用的对象;比如:字符串常量池里的引用。
- 本地方法栈中 JNI (即一般说的 Native 方法)引用的对象。
- JVM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器)。(非重点)
- 所有被同步锁(synchronized关键)持有的对象。(非重点)
- JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等(非重点)
- JVM实现中的"临时性"对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象,这个后续会细讲,先大致了解概念)(非重点)
以上的回收都是对象,类的回收条件: 注意 Class 要被回收,条件比较苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):
1 、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
2 、加载该类的ClassLoader已经被回收。
3 、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
4 、参数控制:
废弃的常量和静态变量的回收其实就和Class回收的条件差不多。
1.1.3 Finalize 方法
即使通过可达性分析判断不可达的对象,也不是"非死不可",它还会处于"缓刑"阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize中去拯救。 代码演示:
运行结果:
可以看到,对象可以被拯救一次 (finalize 执行第一次,但是不会执行第二次 ) 代码改一下,再来一次。
运行结果:
对象没有被拯救,这个就是 finalize 方法执行缓慢,还没有完成拯救,垃圾回收器就已经回收掉了。 所以建议大家尽量不要使用 finalize ,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议大家忘了 finalize 方法!因为 在 finalize 方法能做的工作, java 中有更好的,比如 try-finally 或者其他方式可以做得更好
1.2 各种引用
1.2.1 强引用
一般的Objectobj=newObject(),就属于强引用。在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象。
1.2.2 软引用 SoftReference
一些有用但是并非必需,用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收(如果这次回收后还是没有足够的 空间,才会抛出内存溢出)。参见代码: VM参数 -Xms 10 m -Xmx 10 m-XX:+PrintGC
运行结果
例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。
1.2.3 弱引用 WeakReference
一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。
参看代码:
注意:软引用 SoftReference和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓 存中的内容是可以被释放的。 实际运用(WeakHashMap、ThreadLocal)
1.2.4 虚引用 PhantomReference
幽灵引用,最弱(随时会被回收掉)
垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。
JDK中还有一个典型运用:DirectByteBuffer。 DirectByteBuffer,它有着零拷贝等特点,被 Netty等各种 NIO 框架使用,会使用到堆外内存。堆内存由 JVM 自己管理,堆外内存必须要手动释放, DirectByteBuffer没有 Finalizer,它的 NativeMemory 的清理工作是通过sun.misc.Cleaner自动完成的,是一种基于 PhantomReference 的清理工具, 比普通的 Finalizer轻量些。
下图为DirectByteBuffer构造函数,创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构 建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。
1.3 对象的分配策略
1.3.1 栈上分配
1.3.1.1 没有逃逸(了解即可)
即方法中的对象没有发生逃逸。
逃逸分析的原理:分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用。
比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。
从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。
如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高JVM的效率。
不过逃逸分析的触发前提条件必须触发 JIT 执行。
解释执行与 JIT
Java程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种就是解释执行。 那些被频繁调用的代码,比如调用次数很高或者在 for循环里的那些代码,如果按照解释执行,效率是非常低的。 以上的这些代码称为热点代码。 所以,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。 完成这个任务的编译器,就称为即时编译器(JustInTimeCompiler),简称 JIT 编译器。 在 HotSpot虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执 行次数,如果执行次数超过一定的阈值就认为它是"热点方法" 虚拟机为每个方法准备了两类计数器:方法调用计数器(InvocationCounter)和回边计数器(BackEdgeCounter)。在确定虚拟机运行参数的前提下, 这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发 JIT编译。 方法调用计数器的默认阈值在 客户端模式下是 1500 次,在服务端模式下是 10000 次(我们用的都是服务端,java--version查询),可通过 -XX: CompileThreshold来设定
通过 java-XX:+PrintFlagsFinal--version 查询 :
回边计数器在服务端模式下是 10700 。
1.3.1.2 逃逸分析代码
这段代码在调用的过程中Myboject这个对象属于不可逃逸,JVM可以做栈上分配
然后通过开启和关闭DoEscapeAnalysis开关观察不同。
开启逃逸分析(JVM默认开启)
查看执行速度
关闭逃逸分析
查看执行速度
测试结果可见,开启逃逸分析对代码的执行性能有很大的影响!那为什么有这个影响?
1.3.1.3 逃逸分析
如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。
采用了逃逸分析后,满足逃逸的对象在栈上分配
没有开启逃逸分析,对象都在堆上分配,会频繁触发垃圾回收(垃圾回收会影响系统性能),导致代码运行慢
代码验证
开启GC打印日志
- XX:+PrintGC
开启逃逸分析
可以看到没有GC日志
关闭逃逸分析
可以看到关闭了逃逸分析, JVM 在频繁的进行垃圾回收( GC ),正是这一块的操作导致性能有较大的差别。
1.3.2 大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到--群"朝生夕灭"的"短命大对象",我们写程序的时候应注意避免。 在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们。而当复制对象时,大对象就意味着高额的内存复制开销。
HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor 区之间来回复制,产生大量的内存复制操作。
这样做的目的: 1 .避免大量内存复制, 2 .避免提前进行垃圾回收,明明内存有空间进行分配。 PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效。 - XX:PretenureSizeThreshold= 4 m
1.3.3 对象优先在 Eden 区分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机将发起一次MinorGC。
1.3.4 长期存活对象进入老年区
HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。 为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。
如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为 1 ,对象在Survivor 区中每熬过一次 MinorGC,年龄就增加 1 ,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15 ),CMS是 6 时,就会被晋升到老年代中。
- XX:MaxTenuringThreshold调整
1.3.5 对象年龄动态判定
诞生背景
JVM 通过-XX:MaxTenuringThreshold参数来控制晋升年龄,每经过一次 GC,年龄就会加一,达到最大年龄就可以进入 Old 区,最大值为 15 (因为JVM 中使用 4 个比特来表示对象的年龄)。设定固定的 MaxTenuringThreshold 值作为晋升条件。会诞生以下问题:
1 、MaxTenuringThreshold如果设置得过大,原本应该晋升的对象一直停留在 Survivor区,直到 Survivor区溢出,一旦溢出发生,Eden+Survivor中 对象将不再依据年龄全部提升到 Old区,这样对象老化的机制就失效了。
2 、MaxTenuringThreshold 如果设置得过小,过早晋升即对象不能在Young 区充分被回收,大量短期对象被晋升到 Old 区,Old区空间迅速增长,引起频繁的 MajorGC,分代回收失去了意义,严重影响 GC 性能。
解决方案
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 源码分下如下:JVM 1. 8 中,hotspot\src\share\vm\gc_implementation\shared\ageTable.cpp的compute_tenuring_threshold方法中
1.3.6 空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么MinorGC可以确保是安 全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大 于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次MinorGC,尽管这次MinorGC是有风险的,如果担保失败则会进行一次FullGC; 如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次FullGC。
1.3.7 本地线程分配缓冲 (TLAB)
具体见章节分配缓冲
1.4 垃圾回收基础知识
1.4.1 什么是 GC
Java与C++等语言最大的技术区别:自动化的垃圾回收机制(GC) 为什么要了解GC和内存分配策略
- 1 、面试需要
- 2 、GC对应用的性能是有影响的;
- 3 、写代码有好处 栈:栈中的生命周期是跟随线程,所以一般不需要关注 堆:堆中的对象是垃圾回收的重点
方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点
1.4.2 分代回收理论(重点)
当前商业虚拟机的垃圾回收器,大多遵循"分代收集"的理论来进行设计,这个理论大体上是这么描述的:
1 、绝大部分的对象都是朝生夕死。
2 、熬过多次垃圾回收的对象就越难回收。
根据以上两个理论,朝生夕死的对象放一个区域,难回收的对象放另外一个区域,这个就构成了新生代和老年代。
1.4.3 GC 分类
市面上发生垃圾回收的叫法很多,我大体整理了一下:
- 1 、新生代回收(MinorGC/YoungGC):指只是进行新生代的回收。
- 2 、老年代回收(MajorGC/OldGC):指只是进行老年代的回收。目前只有CMS垃圾回收器会有这个单独的回收老年代的行为。(MajorGC定义是比较混乱,有说指是老年代,有的说是做整个堆的收集,这个需要你根据别人的场景来定,没有固定的说法)
- 3 、整堆回收(FullGC):收集整个Java堆和方法区(注意包含方法区)
2 垃圾回收算法(重点)
垃圾回收算法的实现设计到大量的程序细节,并且每一个平台的虚拟机操作内存的方式都有不同,所以不需要去了解算法的实现,我们重点讲解 3 种算法的思想。
2.1 复制算法( Copying )
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,
实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。但是要注意:内存移动是必须实打实的移动(复制),所以对应的引用 ( 直接指针 ) 需要调整。复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。
Appel 式回收
一种更加优化的复制回收分代策略:具体做法是分配一块较大的Eden区和两块较小的Survivor空间(你可以叫做From或者To,也可以叫做Survivor 1和Survivor 2 ) 专门研究表明,新生代中的对象 98 %是"朝生夕死"的,所以并不需要按照 1 : 1 的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[ 1 ]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8 : 1 ,也就是每次新生代中可用内存空间为整个新生代容量的 90 %( 80 %+ 10 %),只有 10 %的内存会被"浪费"。当然, 98 %的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10 %的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(HandlePromotion)
2.2 标记 - 清除算法( Mark-Sweep )
算法分为"标记"和"清除"两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。
2.3 标记 - 整理算法( Mark-Compact )
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用对象的地方都需要更新(直接指针需要调整)。所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。
3 JVM 中常见的垃圾回收器
在新生代中,每次垃圾回收时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成回收。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记---清理"或者"标记---整理"算法来进行回收。
请记住下图的垃圾收集器和之间的连线关系。
Oracle 官 方 也 有 对 应 英 文 解 释 docs.oracle.com/en/java/jav... 13 /gctuning/ergonomics.html#GUID-DB 4 CAE 94 - 2041 - 4 A 16 - 90 EC- 6 AE 3 D 91 EC 1 F 1
(比较难理解,基于java 13 的)
3.1 Serial/Serial Old (了解即可)
JVM刚诞生就只有这种,最古老的,单线程,独占式,成熟,适合单CPU,一般用在客户端模式下。这种垃圾回收器只适合几十兆到一两百兆的堆空间进行垃圾回收(可以控制停顿时间再 100 ms左右),但是对于超过这个大小的内存回收速度很慢,所 以对于现在来说这个垃圾回收器已经是一个鸡肋。
参数设置
- XX:+UseSerialGC 新生代和老年代都用串行收集器
3.2 StopTheWorld ( STW )(重点)
单线程进行垃圾回收时,必须暂停所有的工作线程,直到它回收结束。这个暂停称之为"StopTheWorld",但是这种STW带来了恶劣的用户体验,例如: 应用每运行一个小时就需要暂停响应 5 分。这个也是早期JVM和java被C/C++语言诟病性能差的一个重要原因。所以JVM开发团队一直努力消除或降 低STW的时间。
3.3 Parallel Scavenge ( ParallerGC ) /Parallel Old (重点)
为了提高回收效率,从JDK 1. 3 开始,JVM使用了多线程的垃圾回收机制,关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99 %。 该垃圾回收器适合回收堆空间上百兆~几个G。
参数设置
开启参数
JDK 1. 8 默认就是以下组合
- XX:+UseParallelGC新生代使用ParallelScavenge,老年代使用ParallelOld
收集器提供了两个参数用于精确控制吞吐量,分别控制的停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。 docs.oracle.com/javase/ 8 /docs/technotes/tools/unix/java.html
- XX:MaxGCPauseMillis
不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集 300 MB新生代肯定比收集 500 MB快,但这也直接导致垃圾收集发生得更频繁,原来 10秒收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、 每次停顿 70 毫秒。停顿时间的确在下降,但吞吐量也降下来了。
- XX:GCTimeRatio
- XX:GCTimeRatio参数的值则应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。 例如:把此参数设置为 19 ,那允许的最大垃圾收集时占用总时间的 5 %(即 1 /( 1 + 19 )), 默认值为 99 ,即允许最大 1 %(即 1 /( 1 + 99 ))的垃圾收集时间 由于与吞吐量关系密切,ParallelScavenge是"吞吐量优先垃圾回收器"。
- XX:+UseAdaptiveSizePolicy
- XX:+UseAdaptiveSizePolicy (默认开启)。这是一个开关参数, 当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor 区的比例(-XX:SurvivorRatio)、 晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
动态机制
JVM 的参数中-Xms和-Xmx设置的不一致,在初始化时只会初始-Xms大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC。另外,如果空间剩余很多时也会进行缩容操作,JVM通过-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机。所以动态扩容会引发GC,同时缩容的话JVM也要处理。在高并发应用中,尽量将成对出现的空间大小配置参数设置成固定的,如 -Xms 和 -Xmx,-XX:MaxNewSize 和 -XX:NewSize,
- XX:MetaSpaceSize和-XX:MaxMetaSpaceSize等,保证Java虚拟机的堆是稳定的,确保-Xms和-Xmx设置的是一个值(即初始值和最大值一致),获 得一个稳定的堆。
3.4 ParNew (了解即可)
多线程垃圾回收器,与CMS进行配合,对于CMS(CMS只回收老年代),新生代垃圾回收器只有Serial与ParNew可以选。和Serial基本没区别,唯一的 区别:多线程,多CPU的,停顿时间比Serial少。(在JDK 9 以后,把ParNew合并到了CMS了) 大致了解下搭配关系即可,后续版本已经接近淘汰。
3.5 Concurrent Mark Sweep ( CMS并发垃圾回收器 )
3.5.1 回收过程
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务 的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。 从名字(包含"MarkSweep")上就可以看出,CMS收集器是基于"标记---清除"算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,
整个过程分为 4 个步骤,包括:
初始标记-短暂,仅仅只是标记一下GCRoots能直接关联到的对象,速度很快。
并发标记-和用户的应用程序同时进行,进行GCRoots追踪的过程,标记从GCRoots开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)
重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用 户线程一起并发执行的。
- XX:+UseConcMarkSweepGC,表示新生代使用ParNew,老年代的用CMS
3.5.1.1 预清理与并发可中断预清理
这两个处理都是并发的,所以如果是比较泛的讲的话,都可以说成并发标记阶段,如果是要抓细节,那么并发标记阶段后续还有这两个处理。因为CMS的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理,如果能够在并发阶段处理被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
3.5.1.1.1 预清理
通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用 -----------通过java-XX:+PrintFlagsFinal--version查询:
主要做两件事情:
1 、在并发阶段(并发阶段是不暂停的),在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
2 、在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(其实这里并非使用CardTable,而是一个类似的数据结构, 叫ModUnionTalble)通过扫描这些Table,重新标记那些在并发标记阶段引用被更新的对象。
3.5.1.1.2 并发可中断预清理
该阶段发生的前提是,新生代Eden区的内存使用量大于参数,CMSScheduleRemarkEdenSizeThreshold,默认是 2 M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。
在该阶段,主要循环的做两件事:
1 、处理 From和 To区的对象,标记可达的老年代对象,类似于预处理。
2 、预清理的第二个阶段。
这个逻辑不会一直循环下去,打断这个循环的条件有三个(满足一个即可):
可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是 0 ,意思没有循环次数的限制。
如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是 5 s,会退出循环。
如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认 50 %,会退出循环。
3.5.2 问题
CPU 敏感:CMS对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS对用户的影响较大。
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为"浮动垃圾"。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS收集不能像其它收集器那样等待老年代快满的时候再回收。在 1. 8 的版本中老年代空间使用率阈值( 92 %) 备注:一个复杂的公式。不用管。当然CMS还有参数可以控制触发回收的条件(堆空间达到多少比例触发):CMSInitiatingOccupancyFraction CMSInitiatingOccupancyFraction的值,如果你没设置过就是虚拟机自己的默认值,默认- 1 ,- 1 就是按照 92 %来算。 如果手动设置-XX:CMSInitiatingOccupancyFraction= 70 ,那么就是按照手动的设置来算。 会产生空间碎片:标记 -清除算法会导致产生不连续的空间碎片。
碎片带来了两个问题:
1 、空间分配效率较低:如果是连续的空间JVM可以通过使用指针碰撞的方式来分配,而对于这种有大量碎片的空闲链表则需要逐个访问空闲列表中的项来访问,查找可以存放新建对象的地址。
2 、空间利用效率变低:新生代晋升的对象大小大于了连续空间的大小,即使整个 Old 区的容量是足够的,但由于其不连续,也无法存放新对象。就是内存碎片导致的 PromotionFailed,YoungGC 以为 Old 有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。垃圾回收器退化如果发生了,PromotionFailed,那么CMS会退化,单线程串行 GC 模式,一般会使用SerialOld,因为SerialOld是一个单线程,所以如果内存空间 很大、且对象较多时,CMS发生这样情况会很卡。 Serial使用使用标记整理算法,单线程全暂停的方式,对整个堆进行垃圾收集,暂停时间要长于CMS。
3.5.3 CMS 总结
CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定。但是它毕竟是第一个并发垃圾回收器,对于了解并发垃圾回收具有一定意义,所以我们必须了解。
为什么CMS采用标记-清除,在实现并发的垃圾回收时,如果采用标记整理算法,那么还涉及到对象的移动(对象的移动必定涉及到引用的变化,这个需要暂停业务线程来处理栈信息,这样使得并发收集的暂停时间更长),所以使用简单的标记-清除算法才可以降低CMS的STW的时间。该垃圾回收器适合回收堆空间几个G~ 20 G左右。
在JDK 1. 8 中,配置参数: