JVM垃圾回收,Java虚拟机,Java GC
2024-04-16 09:48:02 4 举报
AI智能生成
JVM(Java虚拟机)垃圾回收是一种内存管理机制,用于自动管理Java应用程序中的内存分配和回收。垃圾回收器的主要任务是检测和释放不再使用的对象,以便内存可以被重用。这有助于防止内存泄漏,提高应用程序的性能和稳定性。 垃圾回收器的工作原理是基于以下核心概念: 1. 引用计数:每个对象都有一个引用计数器,当有一个引用指向该对象时,计数器加1,当引用失效时,计数器减1。当计数器为0时,对象被视为垃圾。 2. 可达性分析:从对象的根节点(如静态变量、局部变量等)开始,沿着引用链进行搜索,能够到达的对象被视为存活对象,其余对象被视为垃圾。 3. 垃圾回收算法:常见的垃圾回收算法包括标记-清除算法、复制算法、标记-整理算法等。这些算法在不同场景下有不同的应用。 垃圾回收通常涉及到的文件类型主要是日志文件,如GC日志。这些日志文件记录了垃圾回收的活动,包括垃圾回收的时间、内存使用情况等信息,对于性能分析和问题定位非常有用。 在编写Java代码时,可以通过不同的修饰语来影响对象的垃圾回收,例如finalize()方法和@Override注解。finalize()方法在对象被垃圾回收之前被调用,可以用于资源的清理。@Override注解用于标记子类中的方法覆盖了父类中的方法,这在实现继承关系中的垃圾回收时有用。
作者其他创作
大纲/内容
垃圾回收概念
System.gc()的理解
System.gc或Runtime.getRuntime().gc()的调用,会显示触发FullGC,同时会对老年代和新生代进行回收,尝试释放被丢对象占用的内存
然后System.gc调用无法保证对垃圾收集器的调用
一些特殊情况下,比如编写性能基准,我们可以在运行之间调用System.gc
内存溢出与内存泄露
OOM
java 虚拟机的堆内存设置不够
代码创建大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
内存泄露
只有对象不再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露
实际情况有一些疏忽导致对象的生命周期变的很长甚至OOM,宽泛意义上的内存泄露
举例
单例的生命周期和程序是一样长,如果单例程序中,持有对外部对象的引用的话,那么这个外部对象是不能被回收的,导致内存泄露
一些提供close的资源未关闭导致内存泄露,如数据库链接,网络链接,和IO
StopTheWorld
垃圾回收的并行与并发
并发
同一时间段内,几个程序都在同一个处理器上运行
CPU切换
并行
一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互相不抢占资源,可以同时进行,我们称之为并行
并行因素取决于CPU的核心数量
并发的多个任务之间抢占资源
并行多个任务之间不互相抢占资源
垃圾回收的并发与并行
并行
多条垃圾收集器并行工作,用户线程处于等待状态
串行
单线程执行。
安全点与安全区域
安全点
程序执行并非在所有地方都能停顿下来开始GC,只有特定的位置才能停顿下来开始GC,这些位置称为安全点
如果太少,导致GC等待时间长,如果太多导致运行时性能问题,,大部分指令执行都比较短,通常会根据是否具有让程序长时间执行的特征为标准选择一些执行时间较长的指令作为安全点,比如方法调用,循环跳转和异常跳转等
抢先式中断
中断所有线程,如果还有线程不在安全点,就恢复线程,让线程跑到安全点
没有虚拟机采用
主动式中断
设置一个中断标志,各个线程运行到安全点的时候,主动轮询这个标志,如果标志为真,则将自己进行中断挂起
安全区域
如果线程处于sleep或者blocked状态,这时候线程无法响应jvm中断请求,走到安全点去中断挂起。对于这种情况,就需要安全区域来解决
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何位置开始GC都是安全的。
当线程运行到安全区域代码时,首先标志已经进入了安全区域,如果GC,JVM会忽略标识为安全区域状态的线程
当线程即将离开安全区域时,会检查JVM是否已经完成GC,如果完成了,则继续运行。否则线程必须等待直到收到可以安全离开安全区域的信号为止
强引用
最传统的引用定义,程序代码中普遍存在的引用赋值,类似new Object这种引用关系,无论任何情况下,强引用存在,垃圾收集器永远不会回收掉被引用的对象
强引用是造成java内存泄露的主要原因之一
强引用可以直接访问目标对象
软引用
系统将要发生内存溢出之前,会将这些对象列入回收范围之中进行第二次回收,如果这些回收后还没有足够内存,才会抛出内存溢出异常
软引用通常用来实现内存敏感的缓存,高速缓存就有用到软引用
垃圾回收器在某个时间决定回收软可达的对象的时候,会清理软引用,并可选的把引用存放到一个引用队列
弱引用
只被弱引用关联的对象只能够生生存到下一次垃圾收集器之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象
虚引用
一个对象是否有虚引用存在,完全不会对其生存时间构成影响。唯一目的就是在这个对象被收集器回收时收到一个系统通知
他不能单独使用,也无法通过虚引用获取被引用的对象。
终结器引用
用以实现对象的finalize方法,所以被称为终结器引用
无需手动编码,其内部配合引用队列使用
GC时,终结器引用入队,由finalize线程通过终结器引用找到被引用对象并调用 他的finalize方法,第二次GC时才能回收被引用对象
垃圾回收算法
标记阶段:引用计数算法
对每个对象保存一个整型的引用计数器属性,用于记录被对象引用的情况
被对象引用了就+1,引用失效就-1,0表示不可能再被使用,可进行回收
优点:实现简单,垃圾便于辨识,判断效率高,回收没有延迟性
缺点
需要单独的字段存储计数器,增加了存储空间的开销
每次赋值需要更新计数器,伴随加减法操作,增加了时间开销
无法处理循环引用的情况,致命缺陷,导致JAVA的垃圾回收器中没有使用这类算法
小结
引用计数算法,是很多语言的资源回收选择,例如python,它更是同时支持引用计数和垃圾回收机制
Python如何解决循环引用
手动解除
使用弱引用,weakref,python提供的标准库,旨在解决循环引用
标记阶段:可达性分析算法
基本思路
是以根对象(GCRoots)为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达
使用可达性分析算法后,内存中存活的对象都被被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活的对象
GC Roots包括
虚拟机栈中引用的对象
比如各个线程被调用的方法中使用到的参数、局部变量
本地方法栈内JNI,引用的对象
方法区中静态属性引用的对象
比如:java类的引用类型静态变量
方法区中常量引用的对象
比如字符串常量池里的引用
所有被同步锁synchronized持有的对象
Java虚拟机内部的引用
基本数据类型对应的class对象,一些常驻的异常对象,如nullpointerException,OOMerror,系统类加载器
反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等
除了固定的GC Roots集合之外,根据用户选择的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性的加入,共同构成完整GCRoots集合,比如分代收集和局部回收
如果只针对Java堆中某一块内存区域进行垃圾回收,必须要考虑这个区域的对象可能被其他区域对象所引用,这是需要一并将关联的区域对象加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
小技巧
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那么它就是一个Root
如果需要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话,分析结果的准确性就无法保证。
这也是GC进行时必须STW的一个重要原因,即使是号称几乎不会发生停顿的CMS收集器中,枚举根节点也是必须要停顿的。
对象的finalization机制
Java语言提供了对象终止finaliztion机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
当垃圾回收器发现没有引用指向一个对象,即垃圾回收此对象之前,总会先调用这个对象的finalize()方法
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放,通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库链接等
定义虚拟机的对象可能的三种状态
可触及的
从根节点开始,可以到达这个对象
可复活的
对象的所有引用都被释放了,但是对象有可能在finalize()中复活
不可触及的
对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次
只有对象再不可触及时才可以被回收
具体过程
判断一个对象ObjA是否可以被回收,至少需要经历两次标记过程
1、如果对象到GCRoots没有引用链,则进行第一次标记
2、进行筛选,判断此对象是否有必要执行finalize()方法
如果对象A没有重写finalize方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为没有必要执行,对象A被判定为不可触及的
如果对象A重写finalize()方法,且还未执行过,那么A会被插入到F-queue队列中,有一个虚拟机自动创建的,低优先级的Finalizer线程触发其finalize()方法执行
finalize方法是对象逃脱死亡的最后机会,稍后GC会对F-queue队列中的对象进行第二次标记,如果A在finalize方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,A会被移除即将回收集合。之后,对象会再次出现没有引用存在的情况下,finalize方法不会再被调用,对象直接变为不可触及状态
MAT与JProfiler的GC Roots溯源
MAT是Memory Analyzer的简称,是一款功能强大的Java堆内存分析器。用于查找内存泄露以及查看内存消耗情况,基于Eclipse开发的一款免费性能分析工具
清除阶段:标记-清除算法
标记
从引用根节点开始遍历,标记所有被引用的对象,一般是在对象Header中记录为可达对象
注意标记引用对象,不是垃圾对象
清除
对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
缺点
效率不算高
在GC的时候,,需要停止整个应用程序,导致用户体验差。
这种方式清理出来的空闲内存不连续,产生内存碎片,需要维护一个空闲列表
何为清除?
所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放。
清除阶段:复制算法
将或者的内存空间分为两块,每次使用其中一块。在垃圾回收时,将正在使用的内存中的存活的对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有的对象,交换两个内存的角色,最后完成垃圾回收
优点
没有标记和清除的过程,实现简单高效
复制过去以后的保证空间的连续性,不会出现碎片的问题
缺点
需要两倍的内存空间
对于G1这种拆分为大量region的GC,复制而不是移动,意味着GC需要维护region之间的引用关系,不管是内存占用或者时间开销也不小。
如果系统中的垃圾对象很多,需要复制的存活对象数量并不会太大,或者非常低才行
清除阶段:标记-压缩算法
也叫标记整理算法
第一个阶段和标记清除算法一样,从根节点开始标记所有被引用的对象
第二阶段将所有的存货对象压缩在内存的一端,按照顺序排放
之后清理边界外所有的空间
最终效果等同于标记清除算法执行完成后,再进行一次内存碎片整理。
与标记清除算法本质区别,标记清除算法是非移动式的算法,标记压缩是移动式的
是否移动回收后的存活对象时一项优缺点并存的风险决策
优点
消除了标记清除算法内存区域分散的缺点,
消除了复制算法中,内存减半代价
缺点
从效率上来讲,标记整理算法要低于复制算法
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
移动的过程中,需要全程暂停用户应用程序,即STW
分代收集算法
不同生命周期的对象可以采取不同额收集方式,以便提高回收效率
几乎所有的GC都采用分代收集算法执行垃圾回收的
HotSpot中
年轻代
生命周期短,存活率低,回收频繁
老年代
区域较大,生命周期长,存活率高,回收不及年轻代频繁
增量收集算法、分区算法
增量收集算法思想
每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成
通过对线程间冲突的妥善管理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
缺点
线程和上下文切换导致系统吞吐量的下降
分区算法
为了控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的时间
分代算法是将对象按照生命周期长短划分为两个部分,分区算法是将整个堆划分为连续的不同的小区间
每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个小区间
垃圾回收器
GC分类与性能指标
垃圾回收器分类
按垃圾回收线程数
可以分为串行垃圾回收器
串行回收指同一个时间段内,只允许一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直到垃圾收集工作结束
在单CPU处理器或者较小应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以串行回收默认被应用在客户端的client模式下的JVM中
在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器
并行垃圾回收器
和串行相反,并行收集可以运用在多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了STW机制
按照工作模式分
并发式
垃圾回收器与应用程序交替工作,以尽可能减少应用程序的停顿时间
独占式
一旦运行,就停止应用程序中所有的用户线程,直到垃圾回收过程完全结束
按照碎片处理方式
压缩式
非压缩式
按个工作内存区间分
年轻代
老年代
性能指标
吞吐量
运行用户代码的时间占总运行时间的比例
总运行时间:程序的运行时间+内存回收的时间
吞吐量优先,意味着单位时间内,STW的时间最短
垃圾收集开销
吞吐量的补数,垃圾收集所占用的时间与总运行时间的比例
暂停时间
执行垃圾收集时,程序的工作线程被暂停的时间
暂停时间优先,意味着单次STW的时间最短,但是频率可能增加
收集频率
相对于应用程序的执行,收集操作发生的频率
内存占用
Java堆区所占的内存大小
快速
一个对象从诞生到被回收经历的时间
不可能三角
简单来说抓住两点,吞吐量和暂停时间
高吞吐量与低暂停时间,是一对互相竞争的。因为如果高吞吐量优先,必然需要降低内存回收的执行频率,导致GC需要更长的暂停时间来执行内存回收。
如果选择低延迟优先为原则,也只能频繁的执行内存回收,引起程序吞吐量的下降
现在的标准,在最大吞吐量优先的情况下,降低停顿时间
不同的垃圾回收器概述
垃圾回收器的发展迭代史
Serial GC
1999年jdk1.3.1
第一款GC
ParNew
是SerialGC收集器的多线程版本
Parallel GC和Concurrent Mark SweepGC
jdk1.4.2
2002年2月26日
ParallelGC在JDK1.6之后称为HotSpot默认GC
G1
2012年
jdk1.7u4
2017年JDK9中G1变成默认的垃圾收集器,以替代CMS
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性改善最坏情况下的延迟
Epsilon 垃圾回收器、ZGC,可伸缩低延迟垃圾回收器
2018年9月JDK11
Shenandoah GC:低停顿时间的GC,实验版
2019年3月JDK12
增强ZGC
2019年9月JDK13
删除CMS垃圾回收器,扩展ZGC在macOS和Windows上的应用
2020年3月JDK14
7款经典垃圾收集器和垃圾分代之间的关系
垃圾收集器的组合关系
jdk8之前,可以用虚线参考关系
CMS下面的实线,是CMS回收失败的后备方案
JDK8中取消了红线的组合,标记为废弃的。如果要用也可以用。
JDK9中将红线做了remove
jdk14中弃用了绿线组合
jdk14中删除了CMSGC
JDK9默认G1
JDK8默认Parallel Scavenge 和Parallel old Gc
新生代用了Parallel Scavenge 则老年代自动触发用Parallel old
Parallel底层与ParNew底层不同,所以不能和CMS组合
如何查看默认的垃圾收集器
-XX:+PrintCommandLineFlags
jinfo -flag 相关垃圾回收器参数 进程ID
Serial回收器:串行回收
Serial收集器采用复制算法,串行回收和STW机制的方式执行内存回收
除了年轻代,还有用于执行老年代的Serial old收集器,同样采取了串行回收,但是用标记压缩算法
使用一个CPU或者一条收集线程去完成垃圾收集工作,在进行垃圾收集时,必须暂停其他所有工作线程
优势
简单而高效,对于限定单个CPU的环境来说,由于没有线程交互的开销,可以获取最高的单线程收集效率
HotSpot虚拟机中,使用-XX:+UseSerialGC指定年轻代和老年代使用串行收集器
对于交互强的应用而言,不会采取串行垃圾收集器
ParNew回收器:并行回收
除了采用并行回收,其他方面和Serial之间几乎没有任何区别
-XX:UseParNewGC手工指定ParNew收集器执行内存回收任务,它表示年轻代使用,不影响老年代
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数
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%
与上一个参数矛盾,暂停时间越长,Ratio参数就容易超过设定比例
-XX:+UseAdaptiveSizePolicy
开启自适应调节策略
这种模式下,年轻代大小,Eden和Survivor的比例,晋升老年底对象年龄参数都会被自动调整
为了达到堆大小,吞吐量和停顿时间之间的平衡点
在手动调优比较困难的场景下,可以直接用自适应方式,仅指定虚拟机最大堆,目标吞吐量和停顿时间,让虚拟机自己完成调优工作
CMS回收器:低延迟
jdk1.5推出Concurrent Mark Sweep 并发的标记清除,第一次实现了让垃圾收集线程与用户线程同时工作
初始标记:STW,仅仅只是标记处GC Roots能直接关联的对象,一旦标记完成后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里速度非常快
并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程。可以与垃圾收集线程一起并发运行
重新标记:为了修正并发标记期间,因用户程序继续运作导致标记产生变动的那一部分对象的标记记录
并发清除:清理删除标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也可以与用户线程同时并发
初始标记和重新标记阶段仍然需要STW机制
由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此CMS收集器不能像其他收集器那样等到老年代几乎填满再进行回收,而是当堆内存使用率达到某一阈值时,便开始进行回收。
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,这时虚拟机启用备用方案,临时启用Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就长了。
CMS采取标记清除算法,会产生内存碎片,只能够选择空闲列表执行内存分配
为什么不采取标记压缩呢?
因为并发清除时,如果用压缩整理内存,原来的用户线程使用的内存就无法使用了。标记压缩更适合STW场景下使用
优点
并发收集
低延迟
缺点
会产生内存碎片
对CPU资源非常敏感
在并发阶段会占用一部分线程导致应用程序变慢
无法处理浮动垃圾
并发标记阶段是与工作线程同时运行,如果并发阶段产生垃圾对象,CMS无法进行标记,导致新产生的垃圾对象没有被及时回收,只能在下一次执行GC时释放空间
参数
-XX:+UseConcMarkSweepGC
手工指定CMS收集器执行内存回收任务
开启后,自动将-XX:UseParNewGC打开,即ParNew(Young区)+CMS(old区)+Serial GC组合
-XX:CMSlnitiatingOccupanyFraction
设置堆内存使用率的阈值
一旦达到该阈值,则开始进行回收
jdk5及之前默认68,即老年代的空间使用率达到68%时会执行一次CMS回收
JDK6及以上默认值为92%
如果内存增长缓慢,可以设置一个稍大的值,有效降低CMS的触发频率,减少老年代回收的次数
如果应用程序内存使用率增加很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。
-XX:+UseCMSCompactAtFullCollection
用于执行完Full GC后对内存空间进行压缩整理
不过内存压缩无法并发执行,会带来停顿时间更长的问题
-XX:CMSFullGCsBeforeCompaction
设置执行多少次FullGC后对内存空间进行压缩整理
-XX:ParallelCMSThreads
设置CMS的线程数量
默认启动的线程数是(ParallelGCThreads+3)/4
ParallelGCThreads是年轻代并行收集器的线程数
小结
如果想要最小化使用内存和并行开销,选择Serial GC
如果最大化应用程序的吞吐量,选择ParallelGC
如果想要最小化的GC的中断或停顿时间,选择CMS GC
jdk9标记为废弃的,jdk14已经删除了
G1回收器:区域化分代式
官方给G1设定的目标
就是在延迟可控的情况下,获得尽可能高的吞吐量,所以才担当起全功能收集器的重任和期望
Garbage First
G1是一个并行回收器,他把堆内存分割为很多不相关的区域(Region)(物理上不连续)
使用不同的region表示Eden,s0,s1,老年代等
G1跟踪各个region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
JDK1.7版本正式启用,jdk9以后默认垃圾回收器
JDK8还不是默认的,需要用-XX:+UseG1GC来启用
优势
并行与并发
分代收集
同时兼顾年轻代与老年代
空间整合
region之间用复制算法,整体可以看做是标记压缩算法。
两种算法都避免内存碎片,有利于程序长时间运行,分配大对象不会因为无法找到连续空间提前触发下一次GC,尤其当Java堆非常大的时候,G1优势更加明显
可预测的停顿时间模型
能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不能超过N毫秒
缺点
相较于CMS,G1不具备全方位,压倒性优势。比如用户程序运行中,G1无论是为了垃圾收集产生的内存占用,还是程序运行时的额外执行负载都要比CMS要高
经验上来说,小内存应用CMS表现大概率优于G1,在大内存上G1优势发挥更多,平衡点再6-8GB
参数设置
-XX:+UseG1GC
-XX:G1HeapRegionSize
设置每个Region大小,值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆划分出约2048个区域,默认是堆内存的1/2000
-XX:MaxGCPauseMillis
设置期望达到的最大GC停顿时间指标,JVM尽力但不保证,默认200ms
-XX:ParallelGCThread
设置STW工作线程数的值,最多设置8
-XX:ConcGCThreads
设置并发标记的线程数,将N设置为并行垃圾回收线程数(parallelGCThreads)的1/4左右
-XX:InitiatingHeapOccupancyPercent
设置触发并发GC周期的Java堆占用率阈值,超过此值就触发GC,默认是45
常见调优
第一步开启G1垃圾收集器
第二步,设置堆的最大内存
第三步,设置最大的停顿时间
G1提供了三种垃圾回收模式在不同的条件下触发
YoungGC
MixedGC
FullGC
适用场景
面向服务器端应用,针对具有大内存,多处理器的机器
最主要应用是需要低GC延迟
如:在堆大小约6GB或更大,可预测的暂停时间可以低于0.5s,G1每次清理一部分region来保证每次GC停顿时间不会过长
用来替换1.5中的CMS
超过50%的Java堆被活动数据占用
对象分配频率或年代提升频率变化很大
GC停顿时间过长,长于0.5~1秒
region
所有region大小相同,且在JVM生命周期内不会改变
region可以充当多个角色
垃圾回收过程
年轻代GC
当年轻代eden区用尽时
并行独占式收集器
老年代并发标记过程
当堆内存使用到一定值,默认45%
混合回收
标记完成马上开始混合回收
G1老年代回收器不需要整个老年底都被回收,一次只需要扫描回收一小部分老年代的region就可以了。
同时这个老年代回收是和年轻代一起被回收的。
有可能fullGC
记忆集
每个region对应一个记忆集
通过记忆集避免全局扫描
每次引用类型数据写操作时,会产生一个写屏障暂时中断操作
然后检查将要希尔的引用指向的对象是否和该引用对象类型数据在不同的region,如果不同就通过CardTable把相关的引用信息记录到引用指向对象所在的Region对应的记忆集中
当进行垃圾收集时,在GC根节点枚举范围加入记忆集,就可以保证不进行全局扫描,也不会有遗漏
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回收过程二、并发标记过程
初始标记阶段STW
标记从根节点直接可达的对象,并且触发一次年轻代GC
根区域扫描阶段
扫描Survivor区直接可达老年代区域对象,并标记被引用的对象,这个过程在youngGC之前完成
并发标记
和应用程序并发执行,并发标记阶段若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。
并发标记过程中,会计算每个区域的对象活性,存活对象的比例
再次标记
由于应用程序持续进行,需要修正上次标记结果,STW,G1采取比CMS更快的初始快照算法
独占清理
计算各个区域存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下个阶段做铺垫,STW,
这个阶段并不会实际上去做垃圾的收集
并发清理阶段
识别并清理完全空闲的区域
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等相关选项显式设置年轻代大小
固定的年轻代大小会覆盖暂停时间目标
暂停时间目标不要太苛刻,太苛刻会影响吞吐量
垃圾回收器总结
收藏
0 条评论
下一页