深入理解Java虚拟机之【垃圾回收器】
-
垃圾回收器分类
- 按垃圾回收线程数,可以分为串行垃圾回收器和并行垃圾回收器
串行回收指同一个时间段内,只允许一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直到垃圾收集工作结束 并行收集可以运用在多个CPU同时执行垃圾回收,因此提升了应用的吞吐量 不过并行回收仍然与串行回收一样,采用独占式,使用了STW机制
- 按照工作模式分,可分为并发式和独占式
并发式:垃圾回收器与应用程序交替工作,以尽可能减少应用程序的停顿时间 独占式:一旦运行,就停止应用程序中所有的用户线程,直到垃圾回收过程完全结束
- 按照碎片处理方式,可分为压缩式和非压缩式
- 按个工作内存区间分,可分为年轻代和老年代
-
性能指标
- 吞吐量
- 暂停时间
- 内存占用
高吞吐量与低暂停时间,是一对互相竞争的。
因为如果高吞吐量优先,必然需要降低内存回收的执行频率,导致GC需要更长的暂停时间来执行内存回收。
如果选择低延迟优先为原则,也只能频繁的执行内存回收,引起程序吞吐量的下降
现在的标准,在最大吞吐量优先的情况下,降低停顿时间
一、Serial回收器:串行回收
-
Serial收集器采用复制算法,串行回收和STW机制的方式执行内存回收
-
除了年轻代,还有用于执行老年代的Serial old收集器,同样采取了串行回收,但是用标记压缩算法
-
优势:简单而高效,对于限定单个CPU的环境来说,由于没有线程交互的开销,可以获取最高的单线程收集效率
-
但是,对于交互强的应用而言,不会采取串行垃圾收集器
-
HotSpot虚拟机中,使用-XX:+UseSerialGC指定年轻代和老年代使用串行收集器
二、ParNew回收器:并行回收
- 除了采用并行回收,其他方面和Serial之间几乎没有任何区别
-
-XX:UseParNewGC手工指定ParNew收集器执行内存回收任务,它表示年轻代使用,不影响老年代
-
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数
三、Parallel回收器:吞吐量优先
-
Parallel回收器也是并行回收,和ParNew不同的是,它的目标是达到一个可控制的吞吐量,自适应调节策略也是Parallel 与ParNew的一个重要区别。
-
适合后台运算不需要太多交互的任务,例如执行批量处理,订单处理,工资支付,科学计算的应用程序
-
Parallel old采取标记压缩算法,同样基于并行回收和STW机制
-XX:+UseParallelGC 手动指定年轻代使用此收集器执行内存回收任务
-XX:+UseParallelOldGC 手工指定老年代使用并行回收收集器,分别适用于新生代和老年代,默认jdk8是开启的
-XX:ParallelGCThreads 设置年轻代并行收集器的线程数,一般与CPU数量相同,如果CPU数量大于8个,则值=3+(5*N/8)
-XX:MaxGCPauseMillis 设置收集器最大停顿时间,单位毫秒
-XX:GCTimeRatio 垃圾收集占总时间比,用于衡量吞吐量大小,默认99,取值范围0-100,也就是垃圾回收时间不超过1%
-XX:+UseAdaptiveSizePolicy 开启自适应调节策略
四、CMS回收器:低延迟
-
jdk1.5推出Concurrent Mark Sweep 并发的标记清除,第一次实现了让垃圾收集线程与用户线程同时工作
-
初始标记:STW,仅仅只是标记GC Roots能直接关联的对象,一旦标记完成后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里速度非常快
-
并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程。可以与垃圾收集线程一起并发运行
-
重新标记:SWT,为了修正并发标记期间,因用户程序继续运作导致标记产生变动的那一部分对象的标记记录
-
并发清除:清理删除标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也可以与用户线程同时并发
-
-
由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此CMS收集器不能像其他收集器那样等到老年代几乎填满再进行回收,而是当堆内存使用率达到某一阈值时,便开始进行回收。
-
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,这时虚拟机启用备用方案,临时启用Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就长了。
-
CMS采取标记清除算法,会产生内存碎片,只能够选择空闲列表执行内存分配
-
为什么不采取标记压缩呢?因为并发清除时,如果用压缩整理内存,原来的用户线程使用的内存就无法使用了。标记压缩更适合STW场景下使用。
优点:并发收集,低延迟
缺点:会产生内存碎片
对CPU资源非常敏感,在并发阶段会占用一部分线程导致应用程序变慢
无法处理浮动垃圾,并发标记阶段是与工作线程同时运行,如果并发阶段产生垃圾对象,CMS无法进行标记,
导致新产生的垃圾对象没有被及时回收,只能在下一次执行GC时释放空间
-
如果想要最小化使用内存和并行开销,选择Serial GC
-
如果最大化应用程序的吞吐量,选择ParallelGC
-
如果想要最小化的GC的中断或停顿时间,选择CMS GC
-
jdk9标记为废弃的,jdk14已经删除了
五、G1回收器:区域化分代式
-
G1是一个并行回收器,他把堆内存分割为很多不相关的区域(Region)(物理上不连续),使用不同的region表示Eden,s0,s1,老年代等
-
G1跟踪各个region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
-
JDK1.7版本正式启用,jdk9以后默认垃圾回收器
所有region大小相同,且在JVM生命周期内不会改变
优点
并行与并发
分代收集,同时兼顾年轻代与老年代
空间整合,region之间用复制算法,整体可以看做是标记压缩算法。
两种算法都避免内存碎片,有利于程序长时间运行
分配大对象不会因为无法找到连续空间提前触发下一次GC,尤其当Java堆非常大的时候,G1优势更加明显
可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不能超过N毫秒
缺点
相较于CMS,G1不具备全方位,压倒性优势。
比如用户程序运行中,G1无论是为了垃圾收集产生的内存占用,还是程序运行时的额外执行负载都要比CMS要高
经验上来说,小内存应用CMS表现大概率优于G1,在大内存上G1优势发挥更多,平衡点再6-8GB
常见调优
第一步开启G1垃圾收集器
第二步,设置堆的最大内存
第三步,设置最大的停顿时间
G1提供了三种垃圾回收模式在不同的条件下触发
适用场景
面向服务器端应用,针对具有大内存,多处理器的机器
最主要应用是需要低GC延迟
如:在堆大小约6GB或更大,可预测的暂停时间可以低于0.5s,G1每次清理一部分region来保证每次GC停顿时间不会过长
记忆集
每个region对应一个记忆集,通过记忆集避免全局扫描
每次引用类型数据写操作时,会产生一个写屏障暂时中断操作
然后检查将要希尔的引用指向的对象是否和该引用对象类型数据在不同的region,
如果不同就通过CardTable把相关的引用信息记录到引用指向对象所在的Region对应的记忆集中
当进行垃圾收集时,在GC根节点枚举范围加入记忆集,就可以保证不进行全局扫描,也不会有遗漏
垃圾回收过程
-
年轻代GC,当年轻代 eden 区用尽时,并行独占式收集器
-
当堆内存使用到一定值,默认45%,老年代并发标记
-
混合回收
- 标记完成马上开始混合回收
- G1老年代回收器不需要整个老年底都被回收,一次只需要扫描回收一小部分老年代的region就可以了。
- 同时这个老年代回收是和年轻代一起被回收的。
-
有可能fullGC
详细过程
-
G1回收过程一,年轻代GC
-
1、扫描根,根是指static变量指向的对象,正在执行的方法调用链上的局部变量等。根引用连同Rset记录的外部引用作为扫描存活对象的入口
-
2、更新Rset,处理dirty card queue中的card,更新Rset,此阶段完成后,Rset可以准确的反应老年代所在的内存分段中对象的引用
-
3、处理Rset,识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象
-
4、复制对象,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor去中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,会加一,达到阈值会被复制到old区中空的内存分段,如果Survivor区空间不够,Eden空间的部分数据会直接晋升到老年代空间
-
5、处理引用,处理强软弱虚,终结器引用,本地方法接口引用等,最后eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
-
-
G1回收过程二、并发标记过程
-
1、初始标记阶段STW,标记从根节点直接可达的对象,并且触发一次年轻代GC
-
2、根区域扫描阶段,扫描Survivor区直接可达老年代区域对象,并标记被引用的对象,这个过程在youngGC之前完成
-
3、并发标记,和应用程序并发执行,并发标记阶段若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。并发标记过程中,会计算每个区域的对象活性,存活对象的比例
-
4、再次标记,由于应用程序持续进行,需要修正上次标记结果,STW,G1采取比CMS更快的初始快照算法
-
5、独占清理,计算各个区域存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下个阶段做铺垫,STW,这个阶段并不会实际上去做垃圾的收集
-
6、并发清理阶段,识别并清理完全空闲的区域
-
-
G1回收过程三:混合回收
-
当越来越多的对象晋升到老年代old region时,为了避免内存被耗尽,虚拟机会触发一次混合的垃圾收集器,该算法除了回收整个young region,还会回收一部分的old region。也要注意Mixed gc并不是fullgc
-
并发标记结束后,老年代中百分百为垃圾的内存分段被回收了。部分为垃圾的内存分段被计算出来了,默认情况下,这些老年代的内存分段会分8次被回收-XX:G1MixedGCCountTarget设置
-
混合回收的回收集包括八分之一的老年代,Eden区内存分段,Survivor区内存分段。
-
由于老年代中内存分段默认分8次回收,G1会优先回收垃圾多的内存分段,并且有一个阈值会决定内存分段是否被回收。-XX:G1MixedGCLiveThresholdPercent,默认为65%。意思是垃圾占比达到65%才会被回收。如果垃圾占比比较低,意味存活对象较高,复制的时候花更多时间。
-
混合回收不一定要进行8次,有一个阈值:-XX:G1HeapWastePercent,默认值是10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存比例低于10%,则不再进行混合回收,因为GC花费更多的时间,但是回收到的内存却很少。
-
-
G1可选过程四:fullGC
-
G1初衷就是要避免FULLGC,如果上述方式不能正常工作,G1会停止应用程序的执行。使用单线程的内存回收算法进行垃圾回收,性能非常差。应用程序停顿时间长
-
比如堆太小,当G1复制存活对象的时候没有空的内存分段可用,则会回退到FullGC
-
导致FullGC原因可能有两个:回收阶段的时候没有足够的to-space存放晋升的对象;并发处理过程完成之前空间耗尽了。
-
优化建议
避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小
固定的年轻代大小会覆盖暂停时间目标
暂停时间目标不要太苛刻,太苛刻会影响吞吐量
总结
OVER(∩_∩)O~