type
status
date
slug
summary
tags
category
icon
password
这里写文章的前言:
JVM的垃圾回收机制
📝 简介
Java 的对象使用的是堆空间的内存, 方法的执行是栈帧的入栈出栈, 引用和局部变量可以随着入栈出栈直接清理, 但是引用对应所在堆空间的内存则不能通过出栈的结束而回收, 因为堆空间是线程共享的, 并不确定是否有其他线程引用了这块内存, 需要 GC 机制来进行自动的清理
📔垃圾收集
- 程序计数器,虚拟机栈和本地方法栈 这三个区域属于线程私有,只存在于线程私有的,只存在于线程的生命周期内,线程结束后,因此不需要三个区域进行垃圾回收
- 垃圾回收主要针对JVM的 堆空间 和 方法区进行
判断一个对象是否可回收
- 引用计数: 给对象添加一个引用计数器,当对象增加一个引用计数器加1,引用失效时计数器减1。引用计数为0时,可被回收
缺点: 两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收
- 可达性分析
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是都是可用的,不可达的对象可被回收
GC Roots 一般包含:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中的常量引用的对象
- 方法区中类静态属性引用的对象

引用类型
- 强引用
- 使用new一个新对象的方式来创建强引用
- Object obj = new Object();
只要强引用关系存在,垃圾回收器永远不会回收调掉被引用的对象
- 软引用
- 使用SoftReference类来实现软引用
- Object obj = new Object();
- SoftReference<Object> softReference = new SoftReference<>(obj);
一些有用但是并非必须的对象,在系统发生OOM之前,将会对这些对象列进回收范围之中进行第二次回收
- 弱引用
只能生存到下一次垃圾收集之前,当垃圾收集工作时,无论当前内存是否满足,都会被回收
- 虚引用
幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用 取得一个对象实例。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知 业界暂无使用场景, 可能被JVM团队内部用来跟踪JVM的垃圾回收活动
垃圾什么时候回收

完成两次标记的会被回收.
1. 第一次标记是通过gc root 标记是否可达
2. 第二次会看不可达的对象中, 是否实现自己的finalize()方法,有就执行一次,但是未必会等到返回结果, 因为你实现的finalze可能有问题
finalize()
当一个对象可被回收时,会执行该对象的 finalize() 方法,如果通过在该方法中让对象重新被引用,从而有可能而实现自救。
- 该方法在Object对象里面
- 丢到一个 F-QUEUE 队列中, 并且启动一个守护线程(优先级很低) finalizer 来执行, 但是可能CPU轮片切换了,没来得及执行,所以不做保障
HotSpot中GC算法实现

📔OoPMap
进行可达性分析的第一步,就是要枚举 GC Roots,这就需要虚拟机知道哪些地方存放着对象应用。如果每一次枚举 GC Roots 都需要把整个栈上位置都遍历一遍,那可就费时间了,毕竟并不是所有位置都存放在引用呀。所以为了提高 GC 的效率,HotSpot 使用了一种 OopMap 的数据结构,OopMap 记录了栈上本地变量到堆上对象的引用关系,也就是说,GC 的时候就不用遍历整个栈只遍历每个栈的 OopMap 就行了
在 OopMap 的帮助下,HotSpot 可以快速准确的完成 GC 枚举了,不过,OopMap 也不是万年不变的,它也是需要被更新的,当内存中的对象间的引用关系发生变化时,就需要改变 OopMap 中的相应内容。可是能导致引用关系发生变化的指令非常之多,如果我们执行完一条指令就改下 OopMap,这 GC 成本实在太高了
因此,HotSpot 采用了一种在 “安全点” 更新 OopMap 的方法,安全点的选取既不能让 GC 等待的时间过长,也不能过于频繁增加运行负担,也就是说,我们既要让程序运行一段时间,又不能让这个时间太长。我们知道,JVM 中每条指令执行的是很快的,所以一个超级长的指令流也可能很快就执行完了,所以 真正会出现 “长时间执行” 的一般是指令的复用,例如:方法调用、循环跳转、异常跳转等,虚拟机一般会将这些地方设置为安全点更新 OopMap 并判断是否需要进行 GC 操作
此外,在进行枚举根节点的这个操作时,为了保证准确性,我们需要在一段时间内 “冻结” 整个应用,即 Stop The World(传说中的 GC 停顿),因为如果在我们分析可达性的过程中,对象的引用关系还在变来变去,那是不可能得到正确的分析结果的。即便是在号称几乎不会发生停顿的 CMS 垃圾收集器中,枚举根节点时也是必须要停顿的。这里就涉及到了一个问题
📔安全点SafePoint
Safepoint是垃圾回收器执行停顿的触发点,用于确保所有线程都处于安全状态,以执行特定的操作(如栈帧的扫描和对象标记)
跟引用的变化有关系, 尽量不影响引用的情况下发生安全点
原理:记录一个标记位,业务线程不断的轮训检测这个标志位,发现信号之后,就要跑到最近的一个安全点把自己挂起,等待GC线程执行, 该标记位尽量是引用不太发生变化的位置
- safepoint不能太少,否则GC等待的时间会很久(无法有效的触发停顿,导致垃圾回收停顿时间增加)
- safepoint不能太多,否则将增加运行GC的负担
安全点常见的位置:
- 循环的末尾
- 方法临返回前/调用方法的call指令后
- 可能抛异常的位置
之所以选择这些位置作为safepoint的插入点,主要的考虑是“避免程序长时间运行而不进入safepoint”,比如GC的时候必须要等到Java线程都进入到safepoint的时候VMThread才能开始执行GC,如果程序长时间运行而没有进入safepoint,那么GC也无法开始,JVM可能进入到Freezen假死状态
JVM在很多场景下使用到safepoint, 最常见的场景就是GC的时候。对一个Java线程来说,它要么处在safepoint,要么不在safepoint
安全区
安全点触发不了,就会触发安全区的模式
业务线程如果sleep了,业务线程就检测不到是否需要挂起,不能GC线程工作了;这个时候会风险,这段代码,有那一段是安全的,不会发生引用变化的,那么在这个区域内,都是可以发生GC的

方法区回收
方法区主要存放比较经常使用的对象,所以永久代对象的回收率比新生代差很多(经常用的数据,并且不怎么被回收,所以称之为永久代), 主要是对常量池的回收和对类的卸载,因为方法比较经常被使用到,想收回也不好回收
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGo 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出
在 Java 8 中,方法区仍然需要进行垃圾回收(GC),不过与永久代不同的是,方法区的垃圾回收使用的是基于内存分配担保的方式,即当方法区无法分配新的内存时,会触发垃圾回收来释放一些无用的类信息等元数据,从而腾出更多的空间来存储新的类定义信息和方法信息
垃圾收集算法
复制算法
优点
- 实现简单,运行高效,无需标记和清理操作,只需要复制即可
- 没有内存碎片
缺点
- 需要两倍的内存空间
- 只能使用一半的空间来存储数据,不能达到内存的最大使用率
复制算法常用于新生代的垃圾回收,因为新生代中的对象生命周期短,且对象数量少,适合使用复制算法。

标记-清除算法
优点
- 实现简单
缺点
- 标记和清除的过程效率都不高
- 标记清除之后会产生大量的内存碎片,不利于后续的内存分配,也会导致内存的使用率降低
标记-清除算法常用于老年代的垃圾回收,因为老年代中的对象生命周期长,且对象数量多,适合使用标记-清除算法。

标记-整理算法
优点
- 可以避免标记-清除算法产生的内存碎片问题
缺点
- 标记-整理算法比较耗时
- 需要移动对象,如果存活对象比较多,移动的成本就会比较高
标记-整理算法常用于老年代的垃圾回收,因为老年代中的对象生命周期长,且对象数量多,适合使用标记-整理算法。

分代回收
分代收集是一种垃圾回收算法,将虚拟机中的内存分为年轻代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation,也称方法区)。这种算法是基于一个事实:不同年龄段的对象有不同的生命周期。
年轻代中存放新生的对象,在这里进行短时间存活的对象都可以被看做是不用收集的垃圾对象,而那些经过多次垃圾回收后仍然存活下来的对象,就可以被视为是生命周期比较长的对象。同样的,老年代中存放着生命周期比较长的对象,而永久代中存放着一些不会被垃圾回收的对象,例如Java类的定义等。
分代收集的优点:
- 年轻代中的对象生命周期短,容易被回收,因此可以使用复制算法进行垃圾回收,这样的效率比较高。
- 老年代中的对象生命周期长,可以使用标记-整理或标记-清除算法进行垃圾回收,这样的效率比较高。
- 分代收集可以很好地处理不同对象的生命周期,提高垃圾回收的效率。
分代收集的缺点:
- 分代收集算法比标记-清除或标记-整理算法更复杂,需要对不同年龄段的对象进行管理,增加了算法的复杂度。
- 在分代收集算法中,需要将内存分为不同年龄段,这样的分配可能会导致一些内存浪费。
卡表(提供效率,空间换时间)
问题
假如我们进行MinorGC,会不会有对象被老年代引用着?进行OldGC会不会又有对象被年轻代引用着?如果是的话,那我们进行MinorGC的时候不光要管GC Roots,还有再去遍历老年代,这个性能问题就很大了,该如何解决效率问题?
存在的问题是:
由于这种情况是极少数的,如果连同老年代一起扫描,老年代比年轻代大2倍,扫描的时间也比单纯的只扫描年轻代多了2倍,而且还可能扫完了并不存在跨代引用的情况,白花了那么多时间
解决方案:
我们不用去扫描整个老年代了,只要在年轻代建立一个数据结构,叫做记忆集Remembered Set,他把老年代划分为N个区域,标志出哪个区域会存在跨代引用。
以后在进行MinorGC的时候,只要把这些包含了跨代引用的内存区域加入GC Roots一起扫描就行了


三色标记法
在并发标记阶段使用的标记算法
- 根一般是黑色的
- 该对象的所有子类对象被扫过了就是黑色
- 该对象的子类被扫了一部分,但是没有完全扫完,就是灰色
- 扫描完成,剩下没被扫到的就是白色

多浮动-垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过
(被标记为非垃圾对象),那么本轮GC不会回收这部分的内存.这部分本应该回收的但是没有回到到的内存,被称之为”浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只需要等待下一轮垃圾回收中才被清除
针对并发标记(并发清理)开始产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变成垃圾,这也是浮动垃圾的一部分
漏标问题
导致被引用的对象被当成垃圾误删除,严重bug
处理方式
- 增量更新: 当黑色的插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等待并发结束之后,再将这些记录过的关系引用中黑色对象为根,重新扫描一次。简化理解为:黑色对象一旦新插入了指向白色对象的引用后之后,它就变回了灰色对象
- 原始快照: 当灰色对象要删除指向白色对象的引用关系时,要将这个删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色的对象,将白色的对象直接标记为黑色(目的是让这种对象在本轮gc清理中存活下来,待下一轮gc的时候,重新扫描,这个对象也有可能是浮动垃圾)
以上都是通过写屏障来实现的;所谓写屏障,其实就是在赋值操作前后,加一些处理(参考AOP)
写屏障实现SATB
当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来
写屏障实现增量更新
当对象A的成员变量的引用发生变化时,比如新增(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来
读屏障
以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下
- CMS: 写屏障 + 增量更新
- G1,Shenandoah: 写屏障 + 原始快照
- ZGC: 读屏障
SATB相对增量更新效率会高(当然STAB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次扫描被删除引用对象,而CMS增量引用的根对象会做深度描扫,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度描扫对象的话,G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度描扫
B → D 消失, A再指向D,D现在不是垃圾被当成了垃圾

CMS的处理方式: 通过写屏障(钩子程序,回调这个函数),就是发现黑色对象有新的引用的引用的时候(也就是发现有白色的对象指向了黑色),会加一段操作,把黑色变成了灰色


多线程下不安全:
M1垃圾线程 标记了 1 还没标记2, 这个时候M1的角度是灰色
M2业务线程开始了,M2开始的时候 发生了B-D无了, A指向了D, 这个时候会将A变成灰
M1垃圾线程回来了, 将2标完 A就变成黑色了, 此时A是黑色的。
M2业务线程开始了,本来是灰色的,现在被M1改成了黑色, D就漏标了

垃圾收集器
1. JDK 1.8默认是 PS+PO
- 1. 左边都是分代, 右边是分区

Serial收集器
单线程收集器,不仅意味着只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其它的线程,往往造成过程的等待时间
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率

ParNew收集器
Serial收集器的多线程版本
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作
默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数

Parallel Scavenge收集器
是并行的多线程收集器。 其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称 为“吞吐量优先”收集器。
这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接 设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量 和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代 的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(- XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整 这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。 自适应调节策略也是它与 ParNew 收集器的一个重要区别

Serial Old收集器
Serial Old 是 Serial 收集器的老年代版本,也是Client模式下的虚拟机使用
如果用在 Server 模式 下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用

Parallel Old收集器
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器

CSM收集器


初始标记
仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿
并发标记
进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 (会产生脏页问题)
预清理阶段
此阶段标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。(扫描引用放生了变化的对象, 黑对象会重新变成灰色,重新扫描),是不可中断的, 并发标记时,如果某个对象的引用发生了变化,就标记该对象所在的块为 dirty card。并发预清理阶段就会重新扫描该块,将该对象引用的对象标识为可达
并发可中断预处理
新生代中对象的特点是“朝生夕灭”,这样如果Remark前执行一次Minor GC,大部分对象就会被回收。这样,老年代回收的时候,就会更少的扫描到年轻代的引用指向老年代, CMS就采用了这样的方式,在Remark前增加了一个可中断的并发预清理(CMS-concurrent-abortable-preclean),该阶段主要工作仍然是并发标记对象是否存活,只是这个过程可被中断。此阶段在Eden区使用超过2M时启动,当然2M是默认的阈值,可以通过参数修改。如果此阶段执行时等到了Minor GC,那么上述灰色对象将被回收,Reamark阶段需要扫描的对象就少
CMS 有两个参数:CMSScheduleRemarkEdenSizeThreshold、CMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。两个参数组合起来的意思是预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入remark阶段
总结: 并发可中断预处理,是为了在重新标记前,尽量多做一些事,因为这个期间是属于 "并发标记",不影响用户线程的, 能多干点就多干点, 他的核心思想是,尽量做一次 minor GC,从而清理年轻代的对象,这样扫描老年代的时候,就会降低年轻代指向老年代的引用,从而更快重新标记,
需要达到一些配置条件才能触发
1:eden区使用超过了2MB(可配置)
2:5秒(CMSMaxAbortablePrecleanTime 参数配置)内等不到minor GC就立马中断
3:超过了5秒,还等不到,也可以配置一个参数(CMSScavengeBeforeRemark)强制进行minor GC
重新标记
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到了三色标记里的增量更新算法做重新标记
并发清理
开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理
并发重置
重置本次GC过程中的标记数据
G1收集器
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region
一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式
G1保留了年轻代和老年代的概念,但不再是屋里隔阂了,它们都是(可以不连续)Region的集合。
默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过”-XX:G1MaxNewSizePercent”调整.年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个
G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Reigon叫Humongous区,而不是让大对象直接进入到老年代的Region中。
在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放
入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放


初始标记
暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快
并发标记
同CMS的并发标记
最终标记
对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量 SATB 记录
为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这 段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 (儿子找爸爸就比较好找, 如果不是G1的话,儿子找爸爸可能会白找,找了半天根本没人指向它)
筛选回收
对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成
G1缺点
浪费内存空间(TAMS):因为每一块区域里面 总有5-15%的空间(Rset)记录有哪些区域的引用指向我(方便反向扫描) - 空间换时间
RSet(记忆跨代引用的集合)
存跨代引用(谁指向了我,CSet是划分区域). G1有多少个Eden区,就有多少个,CMS只有一个
有了RSet,标记就不需要全盘扫描,只扫描扫对象以及引用的region集合就可以了

ZGC
垃圾能不能回收,关键看他的引用; 染色指针,内存不超过4TB;短暂的STW,只与GC ROOT大小有关,与堆空间大小无关,所以能保存不管多大的空间都能保证很小的STW时间
特点:不随内存的增大而增加扫描时间(因为根本不扫描内存,之前的GC都在内存堆上处理)
标记在对象头上,
2的42次方 4T内存左右
32和64位是什么意思:能够访问到最大的地址是多少
Object o = new Object
比如o是2位的, 他可以通过 00 01 10 11 4种方式访问, 2的32位就是 4个G, 64位就是18446744073709551616
ZGC支持最大的内存数 2的48次方 16T

七种垃圾收集器比较

Full GC触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,如果要触发fullGC,则
调用 System.gc()
此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它 的内存。可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()
老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做 到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组
空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败, 则会触发 Full GC
JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常 量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC
Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的 浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC
Minor GC、Major GC、FULL GC、mixed gc
1 Minor GC
在年轻代
Young space
(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代2 Major GC
Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候需问清楚它指的是full GC还是old GC
3 Full GC
full gc是对新生代、老年代、永久代【jdk1.8后没有这个概念了】统一的回收。
【知乎R大的回答:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)、元空间(1.8及以上)等所有部分的模式】。
4 mixed GC【g1特有】
混合GC
收集整个young gen以及部分old gen的GC。只有G1有这个模式
GC是怎么判断对象是被标记的
通过枚举根节点的方式,通过jvm提供的一种oopMap的数据结构,简单来说就是不要再通过去遍历内存里的东西,而是通过OOPMap的数据结构去记录该记录的信息,比如说它可以不用去遍历整个栈,而是扫描栈上面引用的信息并记录下来。
总结:通过OOPMap把栈上代表引用的位置全部记录下来,避免全栈扫描,加快枚举根节点的速度,除此之外还有一个极为重要的作用,可以帮HotSpot实现准确式GC【这边的准确关键就是类型,可以根据给定位置的某块数据知道它的准确类型,HotSpot是通过oopMap外部记录下这些信息,存成映射表一样的东西】
📔什么时候触发GC
minor GC(young GC):当年轻代中eden区分配满的时候触发[值得一提的是因为young GC后部分存活的对象会已到老年代(比如对象熬过15轮),所以过后old gen的占用量通常会变高]
Full GC:
①手动调用System.gc()方法 [增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]
②发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间
③老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。
④CMS GC时出现Promotion Faield
⑤统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。
这个比较难理解,这是HotSpot为了避免由于新生代晋升到老年代导致老年代空间不足而触发的FUll GC。
比如程序第一次触发Minor GC后,有5m的对象晋升到老年代,姑且现在平均算5m,那么下次Minor GC发生时,先判断现在老年代剩余空间大小是否超过5m,如果小于5m,则HotSpot则会触发full GC(这点挺智能的)
📔为什么minor gc 比full gc 快?
minor gc 只针对 young 区, full gc 针对所有区,包括young gen、old gen、perm gen.
minor gc 和 full gc 都是从 gc root 开始扫描的.
minor gc root 是指:当前线程stack+ Dirty cards.
full gc root 是指:当前线程stack+ Perm Gen .
full gc 扫描stack 时候会递归扫描整个所有对象以及他们引用,是全量扫描。
minor gc 扫描对象时候和full gc 类似,只不过当遍历的对象是old 区的对象就停止进一步遍历了,这样就就跳过了所有old 对象扫描,扫描数量大大减少(只是young 区的),但这样会产生一个问题:
就是如果young 区的某个对象只被old 应用,那么该对象就扫描不到成了需要被回收的。这时候Dirty cards 排上了用场,dirty card 里面记录了old 区所有引用了young 区的对象,所以扫描
一次dirty card 问题就解决了。
总结一下:就是minor gc 需要扫描的对象很少,扫描后需要复制有效对象也很少,所以速度很快。full gc 需要做全量扫描标记清除,很耗时
🤗 总结归纳
📎 参考文章
- 引用文章
有关文章的问题,欢迎您在底部评论区留言,一起交流~