知识养成了思想,思想同时又在融化知识。这篇文章主要讲述JVM升级篇九(GC篇)相关的知识,希望能为你提供帮助。
1、为什么要有GC?
- 本质上就是内存资源的有限性(收集垃圾)
- 有引用,计数器 +1
- 无引用,计数器 -1
- 循环依赖(跟事务,线程死锁一个道理)
public class ReferenceCountingGc
Object instance = null;
public static void main(String[] args)
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
如何解决?
- 引用追踪 => 标记清除算法
上面的引用的类型有
- 强引用:普通的变量引用
public static User user = new User();
- 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference user = new SoftReference(new User());
- 使用场景:浏览器的后退按钮
- 为什么?
- 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
- 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
- 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
public static WeakReference user = new WeakReference(new User());
- 虚引用
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用3、垃圾收集的算法有什么呢?
- 标记清除算法(Mark and Sweep)
- 定义
- Marking(标记): 遍历所有的可达对象,并在本地内存(native)中分门别类记下。
- Sweeping(清除): 这一步保证了,不可达对象所占用的内存,在之后进行内存分配时可以重用。
- 用处
- 并行 GC 和 CMS 的基本原理
- 优势(优点):
- 可以处理循环依赖,只扫描部分对象
- 缺点
- 效率问题 (如果需要标记的对象太多,效率不高)
- 空间问题(标记清除后会产生大量不连续的碎片)
- 如何解决?
- 压缩,STW 标记和清除大量对象!!!!
- 标记复制算法
- 定义
- 以将内存分为大小相同的两块,每次使用其中的一块。当这一块的 内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对 内存区间的一半进行回收。
- 标记整理算法
- 根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回到收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法
- 为什么会有分代?
- 分代假设:大部分新生对象很快无用;存活较长时间的对象,可能存活更长时间。
- 所以JVM会有内存池划分
- 不同类型对象不同区域,不同策略处理。
- 分代收集
- 对象分配在新生代的 Eden 区,标记阶段 Eden 区存活的对象就会复制到存活区;
- 注意:为什么是复制,不是移动???
- 两个存活区 from 和 to,互换角色。对象存活到一定周期会提升到老年代。
- 由如下参数控制提升阈值-XX:+MaxTenuringThreshold=15
- 老年代默认都是存活对象,采用移动方式:
- 1. 标记所有通过 GC roots 可达的对象;
- 2. 删除所有不可达对象;
- 3. 整理老年代空间中的内容,方法是将所有的存活对象复制,从老年代空间开始的地方依次存放。
- 持久代/元数据区
- 1.8 之前 -XX:MaxPermSize=256m
- 1.8 之后 -XX:MaxMetaspaceSize=256m
如:java-jar -XX:+UseSerialGCmicroservice-eureka-server.jar算法: 年轻代:mark-copy(标记-复制)算法 老年代:mark-sweep-compact(标记-清除-整理)算法共同点:
- 都是单线程的垃圾收集器,不能并行处理,会触发STW,停止所有线程
- 不能充分利用多核CPU,只能用单核
- 单CPU利用高,暂停时间长,易卡死
- 只适合几百MB的堆内存,并且单核的CPU
- 串行 GC中的串行,跟我们实际的队列是一样的,先进先出,所以就有个问题,容易阻塞,并且不能充分利用多核,所以单核最好,
- 因此,只适合几百MB的堆内存,并且单核的CPU
4.2、并行 GC(Parallel GC)=> eden:标记复制old:标记整理(Java 8默认GC)应用:
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:+UseParallelGC
-XX:+UseParallelOldGC
算法:
年轻代:mark-copy(标记-复制)算法
老年代:mark-sweep-compact(标记-清除-整理)算法
-XX:ParallelGCThreads=N 来指定 GC 线程数, 其默认值为 CPU 核心数。
- 优点:
- 适用于多核服务器,主要目标增加吞吐量。
- 对系统资源的有效使用,达到最高吞吐量
- 在GC期间所有CPU内核都在并行清理垃圾,总暂停时间更短
- 在两次GC周期的间隔期,没有GC在运行,不会消耗内存
解释:
- 阶段 1: Initial Mark(初始标记)
- 暂停所有的其他线程(STW),并记录下gc roots标记所有的根对象(包括根对象直接引用的对象,以及被年轻代中所有存活对象所引用的对象(老年代单独回收)),速度很快
- 阶段 2: Concurrent Mark(并发标记)
- CMS GC 遍历老年代,标记所有的存活对象,从前一阶段 “Initial Mark” 找到的根对象开始算起。 “并发标记”阶段,就是与应用程序同时运行,不用暂停的阶段
- 阶段 3: Concurrent Preclean(并发预清理)
- 此阶段同样是与应用线程并发执行的,不需要停止应用线程。 因为前一阶段【并发标记】与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化,JVM 会通过“Card(卡片)”的方式将发生了改变的区域标记为“脏”区,这就是所谓的 卡片标记(Card Marking)。
- (漏标解决:写屏障 + 增量更新)
- 写屏障+增量更新:当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D 记录下来。
- 示例:
void post_write_barrier(oop* field, oop new_value)
remark_set.add(new_value); // 记录新引用的对象
【JVM升级篇九(GC篇)】
- 阶段 4: Final Remark(最终标记)
- 最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW 停顿。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次 STW 暂停来处理各种复杂的情况。通常 CMS 会尝试在年轻代尽可能空的情况下执行 Final Remark 阶段,以免连续触发多次 STW 事件
- 阶段 5: Concurrent Sweep(并发清除)
- 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
- 阶段 6: Concurrent Reset(并发重置)
- 重置本次GC过程中的标记数据
-XX:+UseConcMarkSweepGC
4.3.3、算法:
年轻代:mark-copy(标记-复制)算法 老年代:mark-sweep(标记-清除)算法
4.3.4、优缺点
优点: 并发收集、低停顿 缺点:- 对CPU资源敏感(会和服务抢资源); - 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了); -它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理 - 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并 发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收
4.3.5、三色标记法(CMS中Concurrent Sweep(并发清除)的底层实现)
在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。 这里我们引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以 下三种颜色:
- 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过 灰色对象) 指向某个白色对象。
- 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
- 白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
- -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
- -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
- G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
- G1 GC 最主要的设计目标是:将 STW 停顿的时间和分布,变成可预期且可配置的。
- 事实上,G1 GC 是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。为了达成可预期停顿时间的指标,G1 GC 有一些 独特的实现。
- 首先,堆不再分成年轻代和老年代,而是划分为多个(通常是2048个)可以存放对象的小块堆区域(smaller heap regions)。 每个小块,可能一会被定义成 Eden 区,一会被指定为 Survivor区或者Old 区。在逻辑上,所有的 Eden 区和 Survivor 区合起来就是年轻代,所有的 Old 区拼在一起那就是老年代。
- -XX:+UseG1GC -XX:MaxGCPauseMillis=50
- 这样划分之后,使得 G1 不必每次都去收集整
void pre_write_barrier(oop* field)
oop old_value = https://www.songbingjia.com/android/*field; // 获取旧值
remark_set.add(old_value); // 记录原来的引用对象
- G1 的另一项创新是,在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是: 垃 圾最多的小块会被优先收集。这也是 G1 名称的由来。
- -XX:+UseG1GC:启用 G1 GC;
- -XX:G1NewSizePercent:初始年轻代占整个 Java Heap 的大小,默认值为 5%;
- -XX:G1MaxNewSizePercent:最大年轻代占整个 Java Heap 的大小,默认值为 60%;
- -XX:G1HeapRegionSize:设置每个 Region 的大小,单位 MB,需要为 1、2、4、8、16、32 中的某个值,默认是堆内存的1/2000。如果这个值设置比较大,那么大对象就可以进入 Region 了;
- -XX:ConcGCThreads:与 Java 应用一起执行的 GC 线程数量,默认是 Java 线程的 1/4,减少这个参数的数值可能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回收机制耗时加长;
- -XX:+InitiatingHeapOccupancyPercent(简称 IHOP):G1 内部并行回收循环启动的阈值,默认为 Java Heap的 45%。这个可以理解为老年代使用大于等于 45% 的时候,JVM 会启动垃圾回收。这个值非常重要,它决定了在什么时间启动老年代的并行回收;
- -XX:G1HeapWastePercent:G1停止回收的最小内存大小,默认是堆大小的 5%。GC 会收集所有的 Region 中的对象,但是如果下降到了 5%,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以遗留少量的下次处理,这样也降低了单次消耗的时间;
- -XX:G1MixedGCCountTarget:设置并行循环之后需要有多少个混合 GC 启动,默认值是 8 个。老年代 Regions的回收时间通常比年轻代的收集时间要长一些。所以如果混合收集器比较多,可以允许 G1 延长老年代的收集时间。
- -XX:+G1PrintRegionLivenessInfo:这个参数需要和 -XX:+UnlockDiagnosticVMOptions 配合启动,打印 JVM 的调试信息,每个Region 里的对象存活信息。
- -XX:G1ReservePercent:G1 为了保留一些空间用于年代之间的提升,默认值是堆空间的 10%。因为大量执行回收的地方在年轻代(存活时间较短),所以如果你的应用里面有比较大的堆内存空间、比较多的大对象存活,这里需要保留一些内存。
- -XX:+G1SummarizeRSetStats:这也是一个 VM 的调试信息。如果启用,会在 VM 退出的时候打印出 Rsets 的详细总结信息。
- -XX:+G1TraceConcRefinement:这个也是一个 VM 的调试信息,如果启用,并行回收阶段的日志就会被详细打印出来。
- -XX:+GCTimeRatio:这个参数就是计算花在 Java 应用线程上和花在 GC 线程上的时间比率,默认是 9,跟新生代内存的分配比例一致。这个参数主要的目的是让用户可以控制花在应用上的时间,G1 的计算公式是 100/(1+GCTimeRatio)。这样如果参数设置为9,则最多 10% 的时间会花在 GC 工作上面。Parallel GC 的默认值是 99,表示 1% 的时间被用在 GC 上面,这是因为 Parallel GC 贯穿整个 GC,而 G1 则根据 Region 来进行划分,不需要全局性扫描整个内存堆。
- -XX:+UseStringDeduplication:手动开启 Java String 对象的去重工作,这个是 JDK8u20 版本之后新增的参数,主要用于相同String 避免重复申请内存,节约 Region 的使用。
- -XX:MaxGCPauseMills:预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在这个范围内。
1、年轻代模式转移暂停(Evacuation Pause)
G1 GC 会通过前面一段时间的运行情况来不断的调整自己的回收策略和行为,以此来比较稳定地控制暂 停时间。在应用程序刚启动时,G1 还没有采集到什么足够的信息,这时候就处于初始的 fully-young 模式。当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。如果还 没有存活区,则任意选择一部分空闲的内存块作为存活区。 拷贝的过程称为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理。
2、并发标记(Concurrent Marking)
同时我们也可以看到,G1 GC 的很多概念建立在 CMS 的基础上,所以下面的内容需要对 CMS 有一定的理解。 G1 并发标记的过程与 CMS 基本上是一样的。G1 的并发标记通过 Snapshot-At-The-Beginning(起始快 照)的方式,在标记阶段开始时记下所有的存活对象。即使在标记的同时又有一些变成了垃圾。通过对象的存 活信息,可以构建出每个小堆块的存活状态,以便回收集能高效地进行选择。 这些信息在接下来的阶段会用来执行老年代区域的垃圾收集。 有两种情况是可以完全并发执行的:
- 一、如果在标记阶段确定某个小堆块中没有存活对象,只包含垃圾;
- 二、在 STW 转移暂停期间,同时包含垃圾和存活对象的老年代小堆块。
- 阶段 1: Initial Mark(初始标记)
- 此阶段标记所有从 GC 根对象直接可达的对象。
- 阶段 2: Root Region Scan(Root区扫描)
- 此阶段标记所有从 "根区域" 可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。
- 阶段 3: Concurrent Mark(并发标记)
- 此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。
- 阶段 4: Remark(再次标记)
- 和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 G1 收集器会短暂地停止应用线程, 停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。
- 阶段 5: Cleanup(清理)
- 最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC 的效率,维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的: 例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的 STW 暂停。
并发标记完成之后,G1将执行一次混合收集(mixed collection),就是不只清理年轻代,还将一部 分老年代区域也加入到回收集中。混合模式的转移暂停不一定紧跟并发标记阶段。有很多规则和历史数 据会影响混合模式的启动时机。比如,假若在老年代中可以并发地腾出很多的小堆块,就没有必要启动 混合模式。 因此,在并发标记与混合转移暂停之间,很可能会存在多次 young 模式的转移暂停。 具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时 性能指标,存活性,以及在并发标记期间收集的 GC 效率等数据,外加一些可配置的 JVM 选项。混合收 集的过程,很大程度上和前面的 fully-young gc 是一样的。
4.4.3、注意事项
特别需要注意的是,某些情况下 G1 触发了 Full GC,这时 G1 会退化使用 Serial 收集器来完成垃圾的清理工作,它仅仅使用单 线程来完成 GC 工作,GC 暂停时间将达到秒级别的。
- 并发模式失败
- G1 启动标记周期,但在 Mix GC 之前,老年代就被填满,这时候 G1 会放弃标记周期。
- 晋升失败
- 没有足够的内存供存活对象或晋升对象使用,由此触发了 Full GC(to-space exhausted/to-spaceoverflow)。
- 巨型对象分配失败
- 当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。
- 解决办法:增加内存或者增大 -XX:G1HeapRegionSize
4.6、常见GC组合(重要)
常用的组合为:
(1)Serial+Serial Old 实现单线程的低延迟 垃圾回收机制;
(2)ParNew+CMS,实现多线程的低延迟垃 圾回收机制;
(3)Parallel Scavenge和Parallel Scavenge Old,实现多线程的高吞吐量垃圾 回收机制。
4.7、GC 如何选择(如何选择垃圾收集器?)选择正确的 GC 算法,唯一可行的方式就是去尝试,一般性的指导原则:
- 如果系统考虑吞吐优先,CPU 资源都用来最大程度处理业务,用 Parallel GC;
- 如果系统考虑低延迟有限,每次 GC 时间尽量短,用 CMS GC;
- 如果系统内存堆较大,同时希望整体来看平均 GC 时间可控,使用 G1 GC。
- 一般 4G 以上,算是比较大,用 G1 的性价比较高。
- 一般超过 8G,比如 16G-64G 内存,非常推荐使用 G1 GC。
使用:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx16gZGC 最主要的特点包括:
- GC 最大停顿时间不超过 10ms
- 堆内存支持范围广,小至几百 MB 的堆空间,大至 4TB 的超大堆内存(JDK13 升至 16TB)
- 与 G1 相比,应用吞吐量下降不超过 15%
- 当前只支持 Linux/x64 位平台,JDK15 后支持 MacOS 和Windows 系统
-XX:+UnlockExperimentalVMOptions - XX:+UseShenandoahGC -Xmx16g Shenandoah GC 立项比 ZGC 更早,设计为 GC 线程与应用线程并发执行的方式,通过实 现垃圾回收过程的并发处理,改善停顿时间, 使得 GC 执行线程能够在业务处理线程运行 过程中进行堆压缩、标记和整理,从而消除 了绝大部分的暂停时间。 Shenandoah 团队对外宣称 Shenandoah GC 的暂停时间与堆大小无关,无论是 200MB还是 200 GB的堆内存,都可以保障具有 很低的暂停时间(注意:并不像 ZGC 那样保 是 200 GB的堆内存,都可以保障具有 很低的暂停时间(注意:并不像 ZGC 那样保 证暂停时间在 10ms 以内)。
4.9.1、ShennandoahGC 与其他 GC 的 STW 比较
4.10、GC总结Java 目前支持的所有 GC 算法,一共有 7 类:
- 串行 GC(Serial GC): 单线程执行,应用需要暂停;
- 并行 GC(ParNew、Parallel Scavenge、Parallel Old): 多线程并行地执行垃圾回收,关注与高吞吐;
- CMS(Concurrent Mark-Sweep): 多线程并发标记和清除,关注与降低延迟;
- G1(G First): 通过划分多个内存区域做增量整理和回收,进一步降低延迟;
- ZGC(Z Garbage Collector): 通过着色指针和读屏障,实现几乎全部的并发执行,几毫秒级别的延迟,线性可扩展;
- Epsilon: 实验性的 GC,供性能分析使用;
- Shenandoah: G1 的改进版本,跟 ZGC 类似。
- 串行 -> 并行: 重复利用多核 CPU 的优势,大幅降低 GC 暂停时间,提升吞吐量。
- 并行 -> 并发: 不只开多个 GC 线程并行回收,还将 GC 操作拆分为多个步骤,让很多繁重的任务和应用线程一起并发执行,减少了单次 GC 暂停持续的时间,这能有效降低业务系统的延迟。
- CMS -> G1: G1 可以说是在 CMS 基础上进行迭代和优化开发出来的,划分为多个小堆块进行增量回收,这样就更进一步地降低了单次 GC 暂停的时间
- G1 -> ZGC::ZGC 号称无停顿垃圾收集器,这又是一次极大的改进。ZGC 和 G1 有一些相似的地方,但是底层的算法 和思想又有了全新的突破。
推荐阅读
- String 既然能做性能调优,我直呼内行
- JAVA SE——包继承多态抽象类接口 ( 巨细!总结 )
- 谷粒商城学习日记(18)——Vue语法入门
- 4万字50余图3个实战示例一网打尽Transformer
- 谷粒商城学习日记(21)——Vue生命周期
- 带你认识FusionInsight Flink(既能批处理,又能流处理)
- 谷粒商城学习日记(20)——Vue语法入门
- YYDS|不得不看的Spark内存管理机制
- Spring 专场「IOC 容器」不看源码就带你认识核心流程以及运作原理