📑本篇内容:Java Virtual Machine Garbage Collection (Java 虚拟机 垃圾回收)~

📘 文章专栏:JVM深入挖掘原理调优

📚 更新周期:2022年4月16日 ~ 2022年4月24日

🙊个人简介:一只二本院校在读的大三程序猿,本着注重基础,打卡算法,分享技术作为个人的经验总结性的博文博主,虽然可能有时会犯懒,但是还是会坚持下去的,如果你很喜欢博文的话,建议看下面一行~(疯狂暗示QwQ)

🌇点赞 👍 收藏 ⭐留言 📝 一键三连 关爱程序猿,从你我做起

首先 在这里 先感谢 黑马程序员相关课程——《JVM完整教程,全网超高评价,全程干货不拖沓》
其次 再次感谢 周志明先生的 《深入理解Java虚拟机》
同时 还要感谢 JDK 官方文档《JAVA 虚拟机规范》
希望大家学有所得 学有所获。

📖 Java Virtual Machine Garbage Collection

📑 如何判断对象可以回收

🔖 ​引用计数法

概念

引用计数法: 当堆中被new 出来的对象被引用时,其引用数量会自增,当这个对象不再被引用时,其对应的计数就会减少。

📚注意点: 循环引用对象,都不能被使用了,但是它们的计数都不为零导致这两个对象不能作为垃圾进行回收,所以会出现内存泄漏问题

在这里插入图片描述

  • 为了避免出现上述情况的发生,通常这种类型的垃圾对象判断是采用的可达性分析算法而非引用计数法

🔖 可达性分析算法 ⭐

根对象概念

根对象: 所谓的根对象就是那些肯定不能被垃圾回收的对象,我们就称之为根对象。

  • 在垃圾回收之前,我们会对堆中的所有对象进行一次扫描,然后看看这些对象是否会被之前定义过的根对象所直接或者间接的引用,那么就说明不能被回收反之该对象就可以被垃圾回收

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root 对象 为起点的引用链找到该对象,找不到,表示可以回收

哪些对象可以作为GC Root ?

这里我们可以通过 Eclipse 公司的 一个软件 Memory Analyzer Tools (MAT)来进行对 Java Heap进行分析的可视化工具

如有需要软件 请自取 ~

链接:https://pan.baidu.com/s/16WKvKlxSFuxtITnh0rJarw?pwd=HHXF
提取码:HHXF
–来自百度网盘超级会员V4的分享

  • 可以找到内存的泄漏 以及 减少内存的耗费

在这里插入图片描述

步骤一 通过 jmap 命令生成指定的 binary 文件

  • 启动程序之后、我们控制台输出1时则说明已经执行完了 list.add()的相关操作
# 展示此时 java 运行进程
$ jps
$ jmap -dump:format=b,live,file=1.bin 8564
  • 通过 jmap dump 指令 生成格式为 b(binary) , live 代表生成 heap dump 之前执行一次垃圾回收, file= 指代的是生成的文件名(默认生成在项目目录下) 最后 8564 代表的是该进程运行的端口

步骤二 通过MAT观察bin文件中不同时间快照时存在的GC ROOT

在这里插入图片描述

GC ROOTS

在这里插入图片描述

  • System Class:启动器加载的类,比较核心的加载类对象,都可以作为GCROOT对象,如java.lang.Class 对象等,所以这些类对象不能被回收。
  • JNI GlobalJava Native Interface ,执行一些系统调用的方法时,是需要本地方法接口来使用用 C / C++写好的一些底层方法,这些需要引用时需要用到的对象通常也可以作为 GCROOT
  • Busy Monitor: Java 对象中有同步锁机制Synchronized关键字,正在加锁的对象(Monitor) 也是可以作为GCROOT的,因为如果我们在加锁之后还未解锁之前,就将其作为垃圾回收,那么就会永远无法解锁,线程卡死状态。
  • Thread: 活动的线程,肯定就不能被当做垃圾进行回收,导致线程无法正常运行。

通过观察得出结论

main 方法中 创建 ArrayList 对象

List<Integer> list = new ArrayList<>();
  • List<Integer> list 实际是栈帧中的局部变量名,list是所处于栈帧当中的代表是一种引用。
  • new ArrayList<>() 实际代表的是在 Heap(堆) 中创建了一个 ArrayList对象。
  • List<Integer> list = new ArrayList<>();这句话代表了栈帧中局部变量list这个引用指向了堆中新创建好的ArrayList对象。

list = null

  • 这里指代的是栈帧中的引用指向为null,而且此时的main栈帧空间中并没有任何引用指向了之前创建好的ArrayList对象,所以此时之前创建好的ArrayList对象,就不会再和根对象有任何直接或者间接的联系此时ArrayList对象就可以被视作为垃圾对象,进行回收。

🔖 四种引用 ⭐

在这里插入图片描述

图中的 实现代表强引用 虚线代表对应连接名称的引用

🍼 强引用
  • GCROOT 对象都对该对象的强引用断开时,此时的 A1 对象,就会被当成垃圾被回收。
🍼 弱引用
  • 弱引用的对象没有GCROOT对其引用的对象,进行强引用时,只要发生了GC垃圾回收时,这些对象也可能被垃圾回收。

📚注意点 :弱引用所引用的对象被当做垃圾进行回收时弱引用对象就会进入有弱引用队列的引用队列当中

🍼 软引用
  • 软引用的对象没有GCROOT对其引用的对象,进行强引用时,在GC垃圾回收且发现内存不够时,这些对象就会被当做垃圾回收。

📚注意点:软引用所引用的对象被当做垃圾进行回收时弱引用对象就会进入有软引用队列的引用队列当中

🍼 虚引用 (必须配合引用队列)

在这里插入图片描述

  • 这里举出之前 ByteBuffer 直接内存使用来举例,当我们的GCROOT对象对 ByteBuffer 的强引用消失之后ByteBuffer 可以被 Java 当成垃圾直接进行回收,而实际直接内存占用的空间并不能被 Java 直接进行回收。
  • 此时没有强引用所引用的对象的虚引用Cleaner对象所保存的直接内存地址,就会进入到引用队列当中,引用队列会被一个叫做 ReferenceHandler 的线程实时监控,当检测到虚引用对象进入到了引用队列当中,就会调用执行 Cleaner 中的clean()方法 即(unsafe.freeMemory())释放掉直接内存,防止内存泄漏。
🍼 终结器引用 (必须配合引用队列)
  • 当对应的对象没有GCROOT对象强引用时,该对象重写后的终结方法(finalize())会创建一个终结器引用对象。该对象被垃圾回收时终结器引用对象就会进入引用队列,此时的对象并不会直接被垃圾回收,反而会等待一个优先级很低的线程Finalizer会去检查引用队列中是否包含引用对象,如果存在,则会调用之前对象的finalize()方法然后在下一次垃圾回收时,会将其作为垃圾进行回收。

📚注意点: 一般不会调用 finalize() 方法进行垃圾回收,因为效率太慢,原因如上述所示,以及底层实现较为复杂为由。

🔖 引用实际应用

🍼 SoftReferenceApplication

SoftReference

SoftReferenceApplication.java

在这里插入图片描述

运行结果

在这里插入图片描述

📚注意点 :之前会因为堆内存溢出而无法运行的程序此时在软应用的试用下是可以运行的,这样就保证了,对于一系列不重要的数据信息就是可以采用软引用进行关联,当内存紧张时,这些不重要的数据信息就会被垃圾回收。

引用队列的使用

在这里插入图片描述

🍼 WeakReferenceApplication

WeakReference

WeakReferenceApplication.java

在这里插入图片描述

运行结果

在这里插入图片描述

📚 注意点:WeakReference自身对象占用的内存已经达到了快要到临界值,且无法继续装载对象时,则会执行一次 Full GC 此时会清除 list 中所有的对象,而不是像之前一样只会对前一个对象进行垃圾回收

📑 垃圾回收算法 ⭐

🔖 标记清除 (Mark Sweep)

在这里插入图片描述

算法原理

  • 第一阶段: 先对堆中的对象进行标记查看哪些对象并没有与GCROOT有直接或间接的引用关系,标记为可回收垃圾。
  • 第二阶段:将被标记好的可回收垃圾对象所占用的空间进行释放。无需释放清零整个内存空间,只需要记录每个垃圾回收对象起始、结束地址置入与空闲内存列表里即可。

算法优缺点

  • 优点:速度快(只需要记录垃圾对象的起始、结束地址,无需额外处理就完成了垃圾回收处理)
  • 缺点:容易产生内存碎片。

🔖 标记整理 (Mark Compact)

在这里插入图片描述

算法原理

  • 第一阶段: 先对堆中的对象进行标记查看哪些对象并没有与GCROOT有直接或间接的引用关系,标记为可回收垃圾。
  • 第二阶段:进行扫描,当堆中的当前被标记了可回收垃圾标记时,则将其作为垃圾回收的同时,后续对象进行紧凑整理,这样就避免了出现大量的碎片内存问题。

算法优缺点

  • 优点:减少了内存碎片的产生,使得内存空间中能时刻有较大的连续内存存储空间。
  • 缺点:由于整理操作,牵扯到了对象的移动过程,引用地址改变,所以效率会降低。

🔖 复制 (Copy)

第一阶段

在这里插入图片描述

第二阶段

在这里插入图片描述

算法原理

  • 第一阶段:准备两块大小一致的内存空间 FROM 与 TO ,TO 内存空间初始一般都是为空,通过对FROM 进行扫描并且标记出将被垃圾回收的对象,随后将不会被垃圾回收的对象复制到 TO 内存当中,随后清理FROM内存空间。
  • 第二阶段:将此时的 FROM 内存空间与 TO 内存空间进行交换,构建出新的 FROMTO 内存空间,以此保证 TO 内存空间初始情况下一般为空。

算法优缺点

  • 优点:减少了内存碎片的产生,使得内存空间中能时刻有较大的连续内存存储空间。
  • 缺点:需要占用双倍的内存空间。

📑 分代垃圾回收 ⭐

在这里插入图片描述

分代垃圾回收分类

  • 新生代: 频繁需要进行垃圾回收的对象所处的区域,新生代包括了伊甸园、幸存区FROM、幸存区TO

  • 老年代: 经历过多次 MinorGC 都没有被当做垃圾回收的对象,则一般会认为该对象无需进行垃圾回收处理,我们就会将其置于老年代这种进行回收频率少的老年代区中,当其经历MinorGC 次数超过阈值时,则会将该对象转移到老年代当中

分代垃圾回收过程

  • 当我们新创建好对象时,该对象会直接进入到 新生代的伊甸园中 ,占用新生代的伊甸园中的内存空间,随着对象不断的创建并置入,当 新生代的伊甸园内存空间不足,不足以置入下一个对象时 ,此时就会发生 复制 垃圾回收算法(通常被称之为 Minor GC),此时会扫描并标记出伊甸园中将被垃圾回收的对象将不会被垃圾回收的对象置于幸存区TO 中并且修改该对象经历过MinorGC次数+1交换此时的 幸存区FROM 与 幸存区TO,此时就算完成了一次 MinorGC,再有对象创建后就会进入清空内存后的伊甸园当中,重复上述操作。

📚注意点一:当非第一次执行MinorGC时,幸存区FROM中的对象也会参与到垃圾回收算法当中,将伊甸园与幸存区FROM中存活的对象 通过 copy算法复制到幸存区 TO 中,然后交换FROM 与 TO。

  • 当出现了 幸存区FROM 中的对象经历过 MinorGC 的次数超过默认给定的阈值次数时,就会将该对象置于老年代中
  • 如果此时创建的一个新的对象在加入过程中出现了新生代的伊甸园、幸存区FROM的内存区域已无法添加该对象,并且老年代中也没有该对象的容身之处时,就会发生一次 FULL GC

📚注意点二:当执行Minor GC 时会引发 stop the world ,暂停其他用户的线程,等到垃圾回收结束,用户线程才恢复。

📚注意点三:当对象寿命超过阈值时,会晋升为老年代,最大寿命是15(4bit)

🍼 相关VM OPTIONS
含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

演示

GCAnalysis.java

在这里插入图片描述

执行结果

在这里插入图片描述

示例1

public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
}

运行结果

在这里插入图片描述

示例2

public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    list.add(new byte[_512KB]);
}

执行结果

在这里插入图片描述

示例3

发生两次垃圾回收将之前的内存对象晋升为老年代中

public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    list.add(new byte[_512KB]);
    list.add(new byte[_512KB]);
}

执行结果

在这里插入图片描述

案例4

大对象直接晋升老年代(不会进行垃圾回收)

public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_8MB]);
}

执行结果

在这里插入图片描述

📑 垃圾回收器 ⭐

🔖 串行

在这里插入图片描述

  • 单线程
  • 堆内存较小,适合个人电脑

通过 -XX:+UseSerialGC 就可以开启串行垃圾回收器

其中 Serial 采用的是 复制算法

而 SerialOld 采用的是 标记+整理算法

  • 触发垃圾回收时,所有的线程都会先到达一个安全点 stop (防止对象地址改变引起的干扰变换) ,因为串行垃圾回收器是单线程的,所以只会有一个垃圾回收线程其它的用户线程都会阻塞,等待垃圾回收线程的结束。

🔖 吞吐量优先 (并行垃圾回收器)

在这里插入图片描述

  • 多线程
  • 堆内存较大,多核CPU来支持
  • 让单位时间内,stop the world 的时间最短

通过 -XX:+UseParallelGC ~ -XX:+UseParallelOldGC 这两个来开启吞吐量优先垃圾处理器,在JDK1.8中已经默认开启了

如果开启了二者中的其中一个另外一个也会自动开启

UseParallelGC 新生代复制算法

UseParallelOldGC 老年代的标记+整理算法

-XX:+UseAdaptiveSizePolicy 开启自适应的新生代大小调整策略

-XX:ParallelGCThreads 表示的是垃圾回收发生时的并行处理线程数

两个目标

-XX:GCTimeRatio 调整吞吐量的目标 默认ratio = 99 (计算公式为 1 / (1 + ratio) ) 也就是 100 分钟 中 只能有 1 分钟时间在进行垃圾回收,而这个目标通常无法达到,所以 ratio 一般设置为 19。

-XX:MaxGCPauseMillis 最大暂停毫秒数,默认值为 200ms 一般如果堆调整变大了,那么每次最大暂停毫秒数也会增大,若最大暂停毫秒数变小,那说明吞吐量也会下降,所以需要折中选择。

🔖 响应时间优先(CMS 垃圾回收器)

在这里插入图片描述

  • 多线程
  • 堆内存较大,多核CPU来支持
  • 尽可能的让单次的 stop the world 的时间最短

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

UseConcMarkSweepGC 是工作于老年代上的基于标记清除算法的

UseParNewGC 是工作于新生代上的基于复制算法的

这是采用标记+清除垃圾回收算法的垃圾回收器

-XX:ParallelGCThreads 代表的并行执行的线程数,通常设置为计算机的核数

-XX:ConcGCThreads 一般设置为并行线程数的四分之一,也就是说四分之一的线程数去控制垃圾回收,而剩下的四分之三的线程是用于留给用户线程执行工作

当垃圾回收线程并发清理时,其他的用户线程又产生了垃圾,这种垃圾被称之为浮动垃圾,

-XX:CMSInitiatingOccupancyFraction=percent 老年代占用到达percent时就进行垃圾回收

-XX:+CMSScavengeBeforeRemark 在重新标记的过程中,新生代的对象会引用老年代的对象,为了避免无用的查找工作,就要在重新标记之前对新生代做一次垃圾回收操作。

📚 注意点一:如果当CMS垃圾回收器发生了并发失败的问题,就会产生一个补救的措施,此时CMS垃圾回收器会退化到SerialOld单线程串行工作与老年代的垃圾回收器。

📚注意点二:当用户线程初始为总的线程控制CPU满负荷计算同一任务,时间为固定的,而在经历垃圾回收时,需要从总的线程中找出四分之一的线程去并发执行垃圾回收操作,可进行计算的用户线程数量就减少了,如此一来完成之前的同一任务时间就会增加。

📚注意点三:在并发标记的过程中,由于cms的并发标记过程是在其他线程也工作的时候运行的,因此它需要预留一部分内存空间来保证程序分配新对象的需要。而当这部分内存空间不足的时候,就会出错,导致并发标记失败。这时候原本的cms收集器对于老年代的垃圾收集方案执行没有成功,就需要启动后备方案,而这个后备方案就是Serialold。

📚 注意点四:连续空间不足不会full gc,只会开启内存碎片的整理;默认情况为每次full gc之前都会内存整理 书 P98

📚 注意点五:标记清除过程中,用户线程产生的垃圾会被下一次垃圾收集处理掉,而要触发FullGC需要在标记清除的过程中,堆中CMS收集器预留的内存空间装不下用户线程产生的对象垃圾会造成并发失败,从而会导致FullGC

📚 注意点六:标记-整理这一部分最后说了CMS采用的就是“和稀泥”的解决方案,虚拟机平时大多数使用标记-清除算法,暂时容忍内存碎片的存在,等到内存碎片太小以至于容纳不下对象的时候会采用标记-整理算法收集一次。可能CMS这两种算法都用了,毕竟两种垃圾收集算法有利有弊,比如标记-清除会产生内存碎片,标记-整理算法会产生较长的stop the world 书 P81

Concurrent(并发)垃圾回收器工作的同时其他用户线程也是可以运行的。

Parallel(并行)指的是多个垃圾回收器并行运行,而在垃圾回收期间是不允许用户线程运行的。

📑 G1 垃圾回收器 ⭐

定义

G1 (Garbage First) : JDK 9 默认设置 G1 GC替代了 CMS 垃圾回收器

适用场景

  • 同时注重吞吐量与低延迟,默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等Region
  • 整体上是 标记 + 整理 算法,两个区间之间是 复制 算法

JVM 相关VM参数

含义 参数
是否开启 G1 GC -XX:+UseG1GC
设置Region区域大小 -XX:G1HeapRegionSize=size
设置暂停目标 -XX:MaxGCPauseMillis=time

🔖 G1 垃圾回收阶段

在这里插入图片描述

🍼 YoungCollection ⭐

新生代回收阶段

第一时间段:当有对象创建过后首先会占用伊甸园的内存空间

  • 会 STW(Stop The World)

在这里插入图片描述

E :代表 伊甸园的内存区域。

第二时间段:当工作一段时间后,发现新生代内存紧张时,就会将伊甸园中幸存的的内存数据对象,通过复制算法将其放入 S(幸存区)中

在这里插入图片描述

S:代表 的是其中一个幸存区

第三时间段:当再次工作一段时间后,发现此时的幸存区中有部分内存数据对象的存活年龄超过所设置的阈值时,便会进行一次垃圾回收,将存活年龄超过阈值的对象其晋升为老年代,而另一部分不满足晋升的且无需进行垃圾回收的对象会通过复制算法复制到另一个幸存区当中。

在这里插入图片描述

O: 代表老年代的内存区域

🍼 YoungCollection + CM ⭐

新生代回收 + 并发标记 阶段

  • Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定。
含义 参数
初始化老年代占用堆空间比例阈值 -XX:InitiatingHeapOccupancyPercent=percent

📚 注意点:初始标记指的是找到那些 GC Root 并进行标记,而并发标记是指从 GC Root 对象出发 ,顺着引用链找到其它的对象。

在这里插入图片描述

🍼 Mixed Collection ⭐

混合回收

会对伊甸园内存区域、幸存区内存区域、老年代内存区域进行过全面垃圾回收

在这里插入图片描述

  • 最终标记(Remark)会STW
  • 拷贝存活(Evacuation)会STW
含义 参数
最大暂停时间 -XX:MaxGCPauseMillis=ms

Mixed Collection 阶段 :G1 会根据最大暂停时间去有选择的进行回收来以此满足最大暂停时间的这个目标,通常 G1 会从老年代中找到回收价值最高的几个老年代区,去进行垃圾回收,复制到新的老年区之中。若无需进行选择也可以满足目标条件则会对所有的内存区域进行垃圾回收,也是通过复制算法整理内存碎片,调整内存结构。

🍼 Full GC
  • SerialGC
    • 新生代 内存不足 发生的垃圾收集 Minor GC
    • 老年代 内存不足 发生的垃圾收集 Full GC
  • ParallelGC
    • 新生代 内存不足 发生的垃圾收集 Minor GC
    • 老年代 内存不足 发生的垃圾收集 Full GC
  • CMS (并发标记清除)
    • 新生代 内存不足 发生的垃圾收集 Minor GC
    • 老年代 内存不足
  • G1
    • 新生代 内存不足 发生垃圾收集 Minor GC
    • 老年代 内存不足
🍼 Young Collection 跨代引用 ⭐

前要知识

JVM——OopMap And RememberSet (HotSpot 中的OopMap数据结构 与 GC 垃圾回收 避免重复扫描的优化 记忆集)

在这里插入图片描述

  • 卡表 与 Remembered Set
  • 在 引用变量时 通过 post -write barrier +dirty card queue
  • concurrent refinement threads 更新 Remembered Set

在这里插入图片描述

如图所示

新生代中会有 Remembered Set 用于记录外部对当前内存区的引用。

粉色区块 : 如果有对象引用了新生代的对象,对应的卡就会标记为 脏卡 为 粉色。

当对第一行伊甸园区域进行垃圾回收时,可以通过该Region的RememberSet来知道 老年代 卡表中的 脏卡,然后根据这些脏卡去遍历 GCROOTS 减少了 遍历扫描的时间。

标记脏卡是通过 write barrier (写屏障) 每次对象引用发生变更时都要去更新卡表中的卡标记为脏卡,异步操作。

🍼 Remark (重标记) ⭐
  • pre-write barrier(提前写屏障技术) + satb_mark_queue (队列名称)

在这里插入图片描述

处于处理中阶段的各颜色区块所示含义

  • 黑色: 代表已经进行并发标记处理完成的对象状态,表示有引用在引用它们,并发标记结束之后,会被保留下来的对象。
  • 灰色: 代表正在进行并发标记的处理阶段的对象状态。
  • 白色: 代表还未进行并发标记处理的对象状态。

处理完成之后

  • 灰色最终会变为黑色
  • 而有引用的白色区块也会变成黑色
  • 而无任何引用的孤立白色区块会被垃圾回收

处理过程中 重标记 工作流程原理

在这里插入图片描述

因为这是一个并发标记阶段 ,所以如果当 B 进行并发标记完成之后,发现其含有所引用时,该 B 内存对象将会转化为黑色,也正因为 是并发标记阶段 如果此时,其他线程 对 C 对象中的引用进行了修改,此时C与B没有引用关系了。而此时已经判断其C为白色区块,说明最后C区块会被垃圾回收,但是并发标记过程还没有结束之前,又有用户线程对 C 进行了引用,比如这里使得 C 又作为了 A 的引用对象,那么 A 区块作为已经标记完成后的黑色区块,后面是不会在进行标记处理的。这也就是为什么后续标记完成之后,可能会导致 C 这个作为 A 的引用对象被垃圾回收

在这里插入图片描述

为了解决上述出现的问题 这也就是为什么后续还需要进行一次Remark重新标记

Remark 工作原理 与 流程如下:

  • 当一个对象引用发生改变时,JVM 就会给该对象加入一个写屏障(当对象引用发生改变,写屏障代码就会被执行)。
  • 写屏障指令干了些啥? 会把该引用对象加入一个队列当中,并将其区块转换为灰色,说明还未处理完。等到整个并发标记过程结束后,就会进入重新标记阶段。
  • 而重新标记过程中,会STW,此时重新标记的线程会从队列中的对象一个个取出,再次进行检查,如果是灰色,就进行判断处理。
🍼 JDK 8u20 字符串去重

在这里插入图片描述

🍼 JDK 8u40 并发标记类卸载

在这里插入图片描述

🍼 JDK 8u60 回收巨型对象

在这里插入图片描述

🍼 JDK 9 并发标记起始时间的调整

在这里插入图片描述

📑 垃圾回收调优 ⭐

预备知识

  • 掌握 GC 相关的 VM 参数,会基本的空间调整
  • 掌握相关工具
  • 调优跟应用、环境有关,没有放之四海而皆准的法则

官方文档 VM 参数设置

  1. java VM Options (oracle.com)

  2. Java HotSpot VM Options (oracle.com)

查看虚拟机运行参数:

$ java -XX:+PrintFlagsFinal -version | findstr "GC"

🔖 最快的 GC 是不发生 GC

  • 查看 Full GC 前后的内存占用,考虑下面几个问题
    • 数据是不是太多了?
      • resultSet = statement.executeQuery("select * from 大表 limit n")
    • 数据表示是否太过于臃肿?
      • 对象图
      • 对象大小 16Byte Integer 16 Byte int 4Byte
    • 是否存在内存泄漏问题?
      • static Map map
      • 第三方缓存实现

🔖 新生代调优

新生代的特点

  • 新生代的特点
    • 所有的 new 操作的内存分配非常廉价
      • TLAB thread-local allocation buffer(每个线程中都会在伊甸园中给他分配一块私有的区域,是每个线程的局部的私有的缓冲区)
    • 死亡对象的回收代价是零(复制算法)
    • 大部分对象用过即死
    • Minor GC 的时间远远低于 Full GC

新生代越大越好么?

在这里插入图片描述

  • 设置新生代的初始堆内存与最大堆内存大小(以字节为单位),垃圾回收在此区域执行的频率远高于其他区域。如果新生代所占堆内存设置过小,则会执行多次Minor GC ,如果该堆内存设置过大,则只会执行Full GC ,这样需要很长的时间来完成垃圾回收。Oracle 建议 新生代堆内存大小大于总堆内存的 四分之一,小于总堆的二分之一。

📚 注意点补充:新生代调大,垃圾回收时间也会增长,因为垃圾回收时是采用的复制算法, 复制算法 也是有两阶段 ,第一阶段是 标记, 第二阶段是复制(更耗时)。

新生代设置为多大比较合适 ?

  • 新生代能容纳所有【并发量 * (请求 - 响应)】的数据

幸存区设置为多大比较合适 ?

  • 幸存区大到能保留【当前活跃对象 + 需要晋升对象】

  • 晋升阈值设置得当,让长时间存活对象尽快晋升

-XX:MaxTenuringThreshold=threshold

-XX:+PrintTenuringDistribution

🔖 老年代调优

以 CMS(并发标记清除)为例

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经… ,否则先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用 , 将老年代内存预设调大 1/4 ~ 1/3

-XX:CMSInitiatingOccupancyFraction=percent

📑 垃圾回收案例

🔖 案例一:Full GC 和 Minor GC 频繁

发生的原因 ?

  • 请求高发峰期可能高达每分钟上百次 MinorGC ,这很大程度上是因为新生代内存空间紧张,当大量的对象涌入新生代伊甸园内存当中,当新生代伊甸园内存不足时就会发生 Minor GC此时大量对象又会涌入幸存区当中,如果新生代内存较小,幸存区中很多本不该晋升为老年代对象会因为晋升阈值的自动适应调整而提前将这些对象晋升为老年代对象,老年代中的对象不断增多,进而会导致老年代的 Full GC 发生。

解决方案

  • 适当调整新生代的内存空间 ,调整晋升阈值

🔖 案例二:请求高峰期发生 Full GC 单次暂停时间特别长 CMS

发生的原因?

  • 首先已知是采用的是 CMS 垃圾回收器,那么单次暂停时间特别长可能是因为 重新标记阶段 的时间较长既然是重新标记阶段引起的,那我们需要进行考虑 重新标记阶段 干了些啥才会导致时间较长,因为之前说到 重新标记阶段 会STW的同时会扫描整个堆内存空间不仅需要扫描老年代对象还需要扫描新生代对象,而高峰期期间 新生代内存中对象会非常多,这就是一个非常耗时的操作了,这也就间接导致了为什么高峰期发生了 Full GC 单次暂停时间特别长的原因。

解决方案

  • 利用参数调优 -XX:+CMSScavengeBeforeRemark 来开启在 Remark 之前进行一次新生代的垃圾回收,以此来减少不必扫描作为垃圾的新生代对象所耗费的时间。

🔖 案例三:老年代充裕情况下 发生Full GC JDK1.7

解决方案

因为 JDK 是 1.7 其方法区是通过永久代来实现的1.8 其方法区转换为元空间来进行实现的永久代的内存空间不足也是会触发整个堆的 Full GC 发生。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐