导图社区 JVM-GC回收机制
这是一篇关于JVM-GC回收机制的思维导图,包括新生代回收算法、新生代进入老年代情况、老年代回收等内容。
编辑于2021-09-01 10:22:28从 0 开始带你成为JVM实战高手
主题
主题
JVM垃圾回收
新生代回收算法
触发Minor GC
复制算法
新生代分成两个内存区域;两个内存区域重复使用 缺点:造成内存浪费,两个内存区域只有一个在使用, 只有50%内存在使用,50%在闲置
复制算法优化
新生代分成三个内存区域:1个Eden,两个Survivor Eden占用80%,Survivor每个占用10%
-XX:SurvivorRatio=8"这个参数,默认是说Eden区比例为80%, 也可以降低Eden区的比例
平时使用的是:Eden和一个Survivor,一共90%可用
1.开始对象都是存在Eden中,当Eden快满的时候,触发一次Minor GC,把Eden中存活的对象,复制到一个Survivor 1中,然后清空Eden 2.继续在Eden中创建对象,等Eden快要满了,触发Minor GC,把Eden中和Survivor 1中存活的对象复制到另一个Survivor 2中,然后清空Eden和Survivor 1 3.同上,把存活对象复制到Survivor 1,清空Eden和Survivor 2
有点:90%的内存在使用,10%的内存闲置
新生代进入老年代情况:
1.躲过15次GC后,15岁 JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁
2.动态对象年龄判断: 当前存放对象的Survivor里有一批对象大于当前Survivor内存大小50%时,那么大于等于这批对象年龄的对象直接进入老年代 实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代
动态年龄判断的规则,这条规则也会让一些新生代的对象进入老年代。
3.大对象直接进入老年代 JVM参数“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数 要是创造了一个大于这个参数的对象,直接放入老年代,根本不经过新生代 防止大对象在内存中来回复制,减少耗时
4.Minor GC后存活的对象大于Survivor内存时,直接放入老年代
老年代空间分配担保规则
1.每一次执行Minor GC时,都会检查一下老年代可用的可用空间,是否大于新生代所有的对象的大小:极端情况新生代对象全部存活,老年代可用空间放不下
2.1.如果大于,则进行Minor GC,即使新生代全部对象存活,老年代也可以放下
2.2.如果小于,看看JVM的 -XX:-HandlePromotionFailure参数是否设置
1.如果设置,则判断老年代剩余的可用内存是否大于之前所有的Minor GC的平均大小 例如之前Minor GC的平均大小是10M,老年代内存剩余大于10M可以以进行一次Minor GC试试,会有3种情况。
1.Minor GC后存活对象小于Survivor 内存大小,直接放进Survivor
2.Minor GC后存活对象大于Survivor 内存大小,小于老年代可用内存空间,直接放入老年代
3.Minor GC后存活对象大于Survivor 内存大小,大于老年代可用内存空间,则进行一次Full GC
Full GC 会对老年代进行回收,也会对新生代回收 如果Full GC后,老年代仍然放不下Minor GC存活的对象,就会发生OOM
2.如果小于每次Minor GC的大小,或-XX:- HandlePromotionFailure没有设置 则进行Full GC 对老年代进行回收
老年代回收
老年进行回收的情况
1.在Minor GC之前,发现要进入老年代的对象太多,老年代放不下
2.在Minor GC之后,存活对象太多,老年代放不下
老年代回收算法,标记整理算法
1.再回收时先将存活的对象标记,然后把它们在内存中移动到一起,尽量挪动到一起 2.把其他的垃圾对象一起回收掉 3.老年代的垃圾回收算法至少比新生代的垃圾回收算法慢10倍
如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况
垃圾回收器分类
Serial 和 Serial Old垃圾回收器
新生代和老年代垃圾回收器
单线程运行,会导致系统停止,一般不用
ParNew和CMS垃圾回收器
ParNew 一般是新生代垃圾回收
1.-XX:+UseParNewGC 参数,在JVM启动的时候确定GC回收方式
2.ParNew 的线程数量一般和CPU核心数是一样的,不需要特别设置 -XX:ParallelGCThreads参数可以调整ParNew的线程数量
CMS一般是老年代垃圾回收器
如果Stop the World 然后采用 标记-清理的算法去回收垃圾会先停止一切工作线程,慢慢执行标记-清理算法会导致系统卡死时间过长,很多响应无法处理。
CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的。
CMS是如何实现系统一边工作的同时进行垃圾回收
CMS执行一次垃圾回收的过程分为4个阶段
1.初始标记
这个阶段会让系统的工作线程全部停止,进入 Stop the World 状态,标记出来所有GC Roots直接引用的对象,不会管直接引用对象引用的对象。 方法的局部变量和类的静态变量是GC Roots,而类的实例变量不是GC Roots。
初始标记,虽然说要造成“Stop the World”暂停一切工作线程,但是其实影响不大,因为他的速度很快,仅仅标记GC Roots直接引用的那些对象罢了
3.重新标记
这个阶段,要继续让系统程序停下来,再次进入“Stop the World”阶段。然后重新标记下在第二阶段里新创建的一些对象,还有一些已有对象可能失去引用变成垃圾的情况。
这个重新标记的阶段,是速度很快的,他其实就是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快。
接着重新恢复系统程序的运行
4.并发清理
这个阶段就是让系统程序随意运行,然后他来清理掉之前标记为垃圾的对象即可。
这个阶段其实是很耗时的,因为需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行
2.并发标记
这个阶段会让系统线程可以随意创建各种新对象,继续运行。运行期间可能会创建新的对象,也可能让部分存活对象失去引用,变成垃圾对象。
这个过程,垃圾回收线程会尽可能对已有的对象进行GC Roots的追踪。查看引用对象是否有其他的引用对象,其他的引用对象是否被引用,是否可以回收。
第二个阶段,就是对老年代所有对象进行GC Roots追踪,其实是最耗时的。他需要追踪所有对象是否从根源上被GC Roots引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的。
总结:1.停止系统,进行标记 2.恢复系统,查找标记的内容是否有其他引用,标记其他引用状态 3.停止系统,再标记2中标记的引用是否变化和系统运行间新创建对象的状态 4.恢复系统,清理标记内容
CMS的垃圾回收机制,已经尽可能的进行性能优化。
因为最耗时的,其实就是对老年代全部对相关进行GC Roots追踪,标记出来到底哪些可以回收,然后就是对各种垃圾对象从内存里清理掉
的第二阶段和第四阶段,都是和系统程序并发执行的,所以基本这两个最耗时的阶段对性能影响不大
第一个阶段和第三个阶段是需要“Stop the World”的,但是这两个阶段都是简单的标记而已,速度非常的快,所以基本上对系统运行响应也不大
CMS垃圾回收器会产生的问题
并发回收垃圾导致CPU资源紧张
最大的问题:在垃圾回收的同时让系统同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分。并发标记和并发清理两个最耗时的阶段
并发标记的时候,需要对GC Roots进行深度追踪,是因为老年代里存活对象是比较多的,这个过程会追踪大量的对象,所以耗时较高。
并发清理,需要把垃圾对象从各种随机的内存位置清理掉,也是比较耗时的
以在这两个阶段,CMS的垃圾回收线程是比较耗费CPU资源的。 CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4
Concurrent Mode Failure问题
老年代的“浮动垃圾”
在并发清理阶段,CMS是回收之前标记好的垃圾对象。系统同时运行期间可能会有新的对象通过Minor GC进入老年代,同时还变成垃圾对象。
但是CMS只处理以前标记的垃圾对象,新的垃圾对象要等下次GC的时候才能回收。
为了保证在CMS垃圾回收期间,一般会预留一些空间让一些对象可以进入老年代。
CMS垃圾回收的触发时机,有一个是当老年代内存占用达到一定比例了,就自动执行GC
“-XX:CMSInitiatingOccupancyFaction”参数来设置老年代达到多大比例进行自动GC。JDK 1.6 默认是92%
MS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,会发生Concurrent Mode Failure,并发回收失败。
此时会自动用“Serial Old”垃圾回收器替代CMS,直接强行把系统程序“Stop the World”,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生
然后一次性把垃圾对象都回收掉,恢复系统线程。
内存碎片问题
CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Full GC
CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了。 在Full GC之后会进行Stop the World,停止系统工作,整理碎片,把存活对象挪到一起,空出大片连续内存,防止碎片化。
还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,默认零。进行多少次Full GC后进行碎片整理。默认每次后都进行。
多线程运行,效率更高,现在生产标配
G1垃圾回收器
统一收集新生代 和老年代,采用了更加优秀的算法和设计机制
G1思想:主要是把内存拆分为很多个小的Region,然后新生代和老年代各自对应一 些Region,回收的时候尽可能挑选停顿时间最短以及回收对象最多的Region,尽量保证达到我们指定的垃圾回收系统停顿时间。
G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时 间内尽量回收尽可能多的垃圾对象。
G1最大的一个特点,就是可以让我们设置一个垃圾回收的预期停顿时间
分析Region的回收价值
G1也会有新生代和老年代的概念,是逻辑上的概念
G1深度理解
设定G1对应的内存大小
G1对应的一大堆Region,每个Region大小一致
JVM最多可以有2048个Region,没给Region必须是2的倍数
用“-XX:+UseG1GC”指定G1垃圾回收器,此时自动用堆大小除2048(默认)
例如4G,4096/2048=2M,每个Region就是2M
可以用“-XX:G1HeapRegionSize”指定每个Region大小
默认新生代对堆内存的占比是5%,系统运行中,JVM其实会不停的给新生代增加更多的Region,最多不超过60%;用“-XX:G1NewSizePercent”来设置新生代初始占比;且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态的。
新生代还是有Eden和Survivor的概念
“-XX:SurvivorRatio=8”这个参数还可以划分Eden和Survivor的大小;例如10个Region,Eden大概8个,S0和S1大概各占一个
随着系统运行,属于Eden和S0,S1的Region会增加
G1的新生代垃圾回收
一旦新生代达到了设定的占据堆内存的最大大小60%(默认),G1就会用制算法来进行垃圾回收,进入一个“Stop the World”状态,然后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden对应的Region中的垃圾对象。
区别:G1是可以设定目标GC停顿时间的,“-XX:MaxGCPauseMills”参数来设定,默认值是200ms。对每个Region追踪回收他需要多少时间,可以回收多少对象来选择回收一部分的Region,保证GC停顿时间控制在指定范围内,尽可能多的回收掉一些对象。
对象什么时候进入老年代
跟之前几乎是一样的
对象在新生代躲过了很多次的垃圾回收,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,达到年龄
动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%。
,比如年龄为1岁,2岁,3岁,4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象全部会进入老年代,这就是动态年龄判定规则
大对象Region的独自存放和回收
G1提供了专门的Region来存放大对象——大对象不再进入老年代
大对象的判定规则就是一个大对象超过了一个Region大小的50%,一个大对象如果太大,可能会横跨多个Region来存放
新生代、老年代在回收的时候,会顺带带着大对象Region一起回收,这就是在G1内存模型下对大对象的分配和回收的策略。
发新生代+老年代的混合垃圾回收
触发混合回收的参数:“-XX:InitiatingHeapOccupancyPercent”,默认45%。
,如果老年代占据了堆内存的45%的Region的时候,会尝试触发一个新生代+老年代一起回收的混合回收阶段
G1垃圾回收过程
1.初始标记
“Stop the World”的,仅仅只是标记一下GC Roots直接能引用的对象
这个过程速度是很快
GC Roots
各个线程栈内存中的局部变量
方法区类静态变量
2.并发标记
允许程序运行
同时进行GC Roots 追踪,从GC Roots追踪所有的存活变量
这个过程耗时,要追踪全部的存活对象;并发运行,对系统程序影响小
并发标记阶段会对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用
3.最终标记
会进入“Stop the World”,根据并发标记 阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象
4.混合回收
计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率
停止系统程序,选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内
会从新生代、老年代、大对象里各自挑选一些Region,保证用指定的时间(比如200ms)回收尽可能多的垃圾,这就是所谓的混合回收
G1是允许执行多次混合回收
比如先停止工作,执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。
-XX:G1MixedGCCountTarget参数决定混合回收的次数,默认8次。
-XX:G1HeapWastePercent参数,回收过程不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,会 立即停止混合回收,意味着本次混合回收就结束了。
-XX:G1MixedGCLiveThresholdPercent参数,默认85%,要回收的Region,必须是存活对象低于85%的Region才可以进行回收
使用复制算法,把一个Region中存活的对象复制到新的Region中
回收失败时的Full GC
年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去
如果没有空闲Region可以承载自己的存活对象了,就会触发 一次失败
一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。
Stop the World
JVM进行Full GC的时候是暂停java程序的,只有等Full GC完成后,才会继续让java程序运行。这期间会造成系统暂停运行。不在让程序继续创建新的对象,让垃圾回收器尽快完成垃圾回收工作。