【JVM】(四) :垃圾回收机制(GC)
垃圾的标准
对象被判定为垃圾的标准:
- 没有被其他对象引用
判断对象是否为垃圾的算法:
- 引用计数算法
- 可达性分析算法
引用计数算法
判断对象的引用数量:
- 通过判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数 为0的对象实例可以被当作垃圾收集
代码示例
public void ReferenceQuoteCounterProblem(){ MyObject object1 = new MyObject(); //(1) count=1 创建对象 MyObject object2=object1; //(2) count=2 object1= null; //(3)count=1 object2=null; //(4) count=0 该对象实例可以被当作垃圾收集 }
如下图所示,每一根指向或剪断堆中的线代表引用计数器+1或-1
优点:执行效率高,程序执行受影响较小
缺点:无法检测出循环引用的情况,导致内存泄漏
代码示例
public void ReferenceQuoteCounterProblem2(){ MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.childNode = object2; object2.childNode = object1; }
注:该算法机制在jvm不常用
可达性分析算法
通过判断对象的引用链是否可达来决定对象是否可以被回收
可以作为GC Root的对象
- 虚拟机栈中引用的对象(栈帧中 的本地变量表)
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程的引用的对象
回收算法
垃圾回收算法
- 标记-清除算法(Mark-Sweep)
- 复制算法(Copying)
- 标记-整理算法(Compacting)
- 分代收集算法(Generational Collector)
注:这里只讲最常用的四种回收算法
标记-清除算法(Mark-Sweep)
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
- 标记:从根集合进行扫描,对存活的对象进行标记
- 清除:对堆内存从头到尾镜像线性遍历,回收不可达对象内存
如下图所示
注:该算法缺点明显,由于标记清除不需要对象的移动,因此会造成多个不连续的碎片。空间碎片太多,当存在分配较大的对象内存(占四个格子)时,没有足够的连续内存,而不得不提前触发另一次GC工作(一直保持clean状态), 导致OOM
复制算法(Copying)
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。(推倒重建,只需要移动堆顶指针,按顺序分配内容,高效简单)
- 分为对象面和空闲面
- 对象在对象面上创建
- 存活的对象被从对象面复制到空闲面
- 将对象面所有对象内存清除
如下图所示
优点:
- 解决碎片化问题
- 顺序分配内存,简单高效
- 适用于对象存活低的场景(分代收集-年轻代)
标记-整理算法(Compacting)
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。(标记加移动地址造成成本更高)
- 标记:从根集合进行扫描,对存活对象进行标记
- 整理-清除:移动所有存活的对象,且按照内存地址依次序依次排列,然后将末端内存地址以后的内存全部回收
如下图所示
优点:
- 避免内存的不连续性
- 不用设置两块内存互换
- 适用于存活率高的场景(分代收集-老年代)
分代收集算法(Generational Collector)
这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。虚拟机生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。这样可以减少复制对象的时间。(这里只演示JDK8中堆)
- 垃圾回收算法的组合拳
- 按照对象生命周期的不同划分区域,每个区域采用不同的垃圾回收算法
- 目的:提高JVM的回收效率
在Java8及以上版本的虚拟机分代垃圾回收机制中,应用程序可用的堆空间可以分为年轻代与老年代,然后年轻代有被分为Eden区,From区与To区,如下图所示:
GC分类
- Minor GC:发生在年轻代中垃圾收集动作,采用的复制算法。
- Full Gc:发生在老年代中。
年轻代
年轻代是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命,具有有朝生夕死的性质。(尽可能快速地收集掉那些生命周期短的对象)
- 一个Eden区和两个Survivor区组成
- Eden与两个Survivor占比为8:1:1
- 年轻代占整个堆的1/3大小
采用算法
- 复制算法
年轻代三次GC回收流程图如下:
- 当系统创建一个对象时,这个对象的年龄也被确定了(0岁),总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区,这时From区的对象增加1(1岁)。
- 这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区(年龄继续加1)。
- 再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。
- 经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(默认阈值15),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。
对象如何晋升到老年代
- 经历一定Minnor次数依然存活的对象
- Survivor区中存放放不下的对象
- 新生成的大对象(-XX:+PretenuerSizeThreshold)
常用的调优参数
- -XX:SurvivorRatio :Eden与两个Survivor的比值,默认 8:1:1
- -XX:NewRatio:老年代和年轻代内存大小比例,默认 2:1
- -XX:MaxTenuringThreshold:对象从年轻代晋升到老年代经过GC次数的最大阈值
老年代
存放生命周期较长的对象
采用算法
- 标记-清理算法
- 标记-整理算法
触发Full GC的条件
- 老年代空间不足
- 永久代空间不足(JDK8之前存在)
- CMS GC时出现 promotion failed ,concurrent mode failure(可能触发)
- Minor GC 晋升到老年代的平均大小大于老年大剩余空间
- 调用System.gc()(可能触发)
- 使用RMI来进行RPC或者管理的JDK应用,每个小时执行一次Full GC
垃圾收集器
介绍收集器之前,我们先了解一下
stop-the-World
顾名思义,“Stop the world”就是 JVM 由于要执行 GC 而停止了其他应用程序的运行,在任何 GC 算法中都可能会发生。假设有这么一个场景,你的程序正在愉快的运行,突然之间 JVM 要清理垃圾了。然后程序就陷入了10分钟的等待,是不是很抓狂?当然一般情况下会让你等待这么久,但是“Stop the world”会在一定程度上影响用户体验这是毋庸置疑的。所以,多数GC优化通过减少 Stop-the-world 时间来提升系统性能。
- JVM由于要执行GC而停止了应用程序的执行(只留下GC线程)
- 任何一种GC算法中都会发生
- 多数GC优化通过减少Stop-the-world发生时间来提高程序性能(提高吞吐量,低停顿)
注:吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
Safepoint(安全点)
- 分析过程中对象引用关系不会发生变化的点
- 产生Safepoint的地方:方法调用;循环跳转;异常跳转等
- 安全点数量得适中(太多会增加程序的负荷 ,太少会饶昂GC等待的时间太长)
JVM运行模式(查看JVM运行模式命令 java -version)
- Server:启动速度较慢,进入稳定期长期运行时,比Client运行快。(重量级虚拟机,采用了更多的优化)
- Client: 启动速度较快(轻量级)
垃圾收集器之间的联系
如上图所示,垃圾收集分为年轻代和老年代的收集器,用线连接表示收集器之间在JVM中是否共存(这里只介绍常用的垃圾收集器)
年轻代收集器
Serial收集器(-XX:+UseSerialGC,复制算法)
Serial收集器是最基本、历史最悠久的垃圾收集器。它是一个单线程收集器,“单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是 它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。 它会在用户不可见的情况下把用户正常工作的线程全部停掉。想象一下,当你结束一天的工作回到家中,喝着冰阔乐刷着副本正要打Boss,突然你的电脑说他要休息5分钟,你会是什么感觉?
存在即合理,当然Serial 收集器也有优于其他垃圾收集器的地方,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器
ParNew收集器(-XX:UseParNewGC,复制算法)
ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为(控制参数、收集算法、分配规则、回收策略等等)和 Serial 收集器完全一样。
除了支持多线程收集,ParNew 相对 Serial 似乎并没有太多改进的地方。但是它却是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。ParNew单核状态下不如Serial,多核线程下才有优势。
- 多线程收集,其余的行为、特点和Serial收集器一样
- 单核执行效率不如Serial(存在线程交互开销),在多核下执行才有优势(默认开启收集线程数与CPU数一样)
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
Parallel Scavenge 收集器是一个新生代收集器,也是采用复制算法+并行。听起来和ParNew差不多对不对,那么它有什么特别之处呢?
Parallel Scavenge 收集器关注点是吞吐量(CPU运行代码的时间与CPU总消耗时间的比值)。 而CMS 等垃圾收集器的关注点更多的是缩短用户线程的停顿时间(提高用户体验)。停顿时间越短就越适合和用户进行交互(响应速度快,可以优化用户体验),而高吞吐量则可以高效的利用CPU时间,尽快完成用户的计算任务。
Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
- 比起关注用户线程停顿时间,更关注系统的吞吐量
- 在多核下执行才有优势,Server模式下默认的年轻代收集器
老年代收集器
Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
- 单线程收集,运行拉阿基收集时,必须咋暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(-XX:+UseParalleOldGC,标记-整理算法)
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
- 多线程,吞吐量优先
CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常重视服务的响应速度,以期给用户最好的体验。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。
- 初始标记:stop-the-world
- 并发标记:并发追溯标记,程序不会停顿
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
CMS一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间 碎片产生。
共用垃圾收集器
G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,开发人员希望在未来可以换掉CMS收集器,它有如下特点
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。这就意味着不会产生大量的内存碎片
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1收集器将整个Java堆内存划分为若干个内存大小相等的Region,年轻代和老年代不再物理隔离,他们都是一部分Region的集合。