JVM垃圾回收

发布时间:2024年01月18日

1 回收对象判断

①引用计数法:顾名思义,只要对象的引用个数不为零,则不会被回收,因此存在对象间相互引用导致引用个数无法归零,对象无法回收导致的内存泄露问题

②可达性分析法(JVM使用):GC Root对象直接或间接引用的对象,除此外的会被回收,

③如何找到② 中GC Root对象,首先jmap -dump:format=b,live,file=filename pid 命令将GC后的堆占用情况快照转储为二进制文件,再借助eclipse提供的java堆分析工具MAT,分析该文件

5种引用回收条件使用方式
强引用只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
软引用仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用可以配合引用队列来释放软引用自身
弱引用仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象可以配合引用队列来释放弱引用自身
虚引用主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存必须配合引用队列使用
终结器引用垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象无需手动编码,但其内部配合引用队列使用

2 回收算法

算法优点缺点
标记清除速度快,对GC后的内存地址打标记为可覆盖写,即完成清除会造成内存碎片
标记整理没有内存碎片,因为标记完会移动到相邻内存地址速度慢
复制没有内存碎片,因为会复制到相邻内存地址需要占用双倍内存空间

3 分代回收

image-20240111222317029

①对象首先分配在伊甸园区域

②新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to

? minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

③当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)

④当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

⑤当新对象过大直接超过新生代大小,则不走晋升流程,直接分配在老年代区域

⑥多个用户线程的内存占用情况互相之间不影响,比如,主线程里创建的新线程OOM,不会导致整个进程的终止

相关 VM 参数

含义参数
堆初始大小-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

4 垃圾回收器

4.1 三种垃圾回收器对比

回收器范围适用对象特点回收算法开启参数
串行单线程堆内存较小,适合个人电脑不紧不慢新生代采用复制算法,老年代为标记+整理-XX:+UseSerialGC = Serial + SerialOld
吞吐量优先多线程堆内存较大,多核 cpu让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高新生代采用复制算法,老年代为标记+整理-XX:+UseParallelGC ~ -XX:+UseParallelOldGC(开启一个自动开另一个)
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
响应时间优先多线程堆内存较大,多核 cpu尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
跟上面比就是,上面GC占的总时间短,这个是每次GC控制在最短时间完成
新生代采用复制算法,老年代为标记+清除-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld(并发失败则退化为后续方案,需要配置相关参数备用)
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

三种垃圾回收器图解说明如下:

image-20240113190837305

串行:两个要点,①GC在安全点后,为防止对象地址改变等问题,②GC线程运行所有用户线程STW

image-20240113190841516

吞吐量优先:①GC并行,线程数可控,默认为CPU核数(用户线程STW)②动态调整幸存区比例③根据堆的大小合理控制GC的时间占比和最大总时间

image-20240113190845637

响应时间优先(CMS):

①该回收器最大的不同在于,由于不是每个阶段都STW,因此需要考虑用户线程的在GC的同时造成的影响(比如新的对象和浮动垃圾的产生,引用地址的改变等)

②各标记阶段的区别:初始标记阶段的范围为根对象,并发标记扩大到整个堆,重新标记阶段才会STW,对整个堆范围标记,而且考虑到Old里的根对象跨代引用新生代对象,配合开启参数在该阶段之前就minorGC清理新生代垃圾

③关于退化:并发清理阶段用户线程仍运行,导致内存占用仍然上升,因此需要设置百分比参数启动CMS,避免清理阶段OOM,另外,由于该回收器在老年代算法为标记清除,因此会产生大量内存碎片,引发并发失败,所以需要退化为串行GC来整理内存

4.2 G1

4.2.1 阶段划分

很大程度上和CMS相似,但兼顾吞吐量和低延时,不同在于它将内存划分成region来管理,各阶段内容如下:

image-20240116220220153

①新生代GC:还是老规矩,伊甸园中幸存对象拷贝到幸存区region等待晋升,满足阈值则晋升至老年代,整个过程会STW

②新生代GC+并发标记: Young GC 时会进行 GC Root 的初始标记,老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

? -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

③混合收集:会对 伊甸园、幸存区、老年代 进行全面垃圾回收,整个过程包含最终标记、拷贝存活两个阶段,会STW,为了实现最短的GC暂停时间,在老年代GC时会优先收集垃圾多的region,点题Garbage First

? -XX:MaxGCPauseMillis=ms

④不难理解,回收算法为整体上是 标记+整理 算法,两个区域之间是 复制 算法

开启参数:

-XX:+UseG1GC

-XX:G1HeapRegionSize=size

-XX:MaxGCPauseMillis=time

4.2.2 跨代引用

image-20240117220623665

老年代中根对象引用了新生代对象,成为跨代引用

老年代region进一步细分为卡表(card Table),其中引用新生代对象的标记为脏卡,以此在做GC Root遍历时,减少搜索范围

对应的新生代region有Remembered Set来记录外部引用(脏卡)

对象的引用发生变更时需要更新脏卡,异步线程从写屏障和脏卡队列中获取变更信息来更新脏卡

4.2.3 版本特性

版本功能开启参数特点
JDK 8u20字符串去重-XX:+UseStringDeduplication将所有新分配的字符串放入一个队列
当新生代回收时,G1并发检查是否有字符串重复
如果它们值一样,让它们引用同一个 char[]
这么做节省内存,但占用CPU回收young的时间
JDK 8u40并发标记类卸载-XX:+ClassUnloadingWithConcurrentMark所有对象都经过并发标记后,就能知道哪些类不再被使用,
当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
JDK 8u60回收巨型对象一个对象大于 region 的一半时,称之为巨型对象
G1 不会对巨型对象进行拷贝
回收时被优先考虑
G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
JDK 9并发标记起始时间的调整-XX:InitiatingHeapOccupancyPercent 用来设置初始值,进行数据采样并动态调整并发标记必须在堆空间占满前完成,否则退化为 FullGC

4.3 Remark理解

由于并发标记阶段并未STW,因此用户线程可能对已经标记过的对象改变其它对象对其的引用,比如被标记为待回收的对象加上新的引用,结果导致被引用的对象会被回收,因此为了防止该现象,当对象的引用将要发生变化,在此之前,写屏障(pre-writer barrier)指令将其放入队列(satb_mark_queue),等到Remark阶段再将其更新回收标记防止误回收

4.4 Full GC

问题:单纯的老年代空间不足就会发生Full GC吗?

ANS:以四种垃圾回收器说明,确实新生代不足会minor GC,但是,只有串行GC和并行GC时老年代内存不足会Full GC,CMS和G1则不是,G1的old占比到达45%则会触发并发标记和后续的混合收集阶段,CMS也是到达阈值则触发GC,但是还不能达到Full GC标准,只有并发清理的速度跟不上垃圾产生的速度,才会触发Full GC

5 GC调优

工具:

java官网查看参数

查看虚拟机参数:

“jdk安装目录\bin\java” -XX:+PrintFlagsFinal -version | findstr “GC”

手段:

缓存数据使用软引用/弱引用,在内存吃紧时回收,或第三方缓存实现如redis

新生代特点:

TLAB 线程私有可分配内存,避免多个线程创建对象时互相干扰

采用复制算法,死亡对象回收代价为零,大部分对象用过即死,少量存活,因此minor GC时间远低于Full GC

新生代内存占比并非越大越好,占比1/4~1/2,否则old太小触发full GC

整体思想为:尽量多给老年代分配更多内存(观察Full GC时old内存占比,然后调大 1/4 ~ 1/3 ),毕竟full GC代价太高,新生代的各区域分配大小可以粗略计算得出,方法如下:

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

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

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

文章来源:https://blog.csdn.net/jason_bone_/article/details/135663240
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。