JVM之垃圾回收机制(GC)
JVM之垃圾回收机制全解(GC)文章底部有思维导图,较为清晰,可参考
导读:垃圾回收是Java体系中最重要的组成部分之一,其提供了一套全自动的内存管理方案,要想掌握这套管理方案,就必须了解垃圾回收器的工作原理。本文介绍了垃圾回收的概念,算法,垃圾回收器及我在工作中遇到的一些关于GC的优化实例。
先来简单了解下JVM:
-------------------------------------------------------------
一、heap内存划分
-------------------------------------------------------------
1.年轻代:分三个区。一个Eden区,两个Survivor区(from Survivor(s0)区和to Survivor(s1)区)。
大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。
2.年老代
在年轻代中经历了N次((ParNew默认15))垃圾回收后仍然存活的对象,就会被放到年老代中。年轻代放不下的大对象直接进入老年代。
tip1:对象动态年龄计算规则
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold(默认15次)才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
3.持久代
用于存放静态文件,如今Java类、方法等
JDK1.8中,永久代已经从java堆中移除,String直接存放在堆中,类的元数据存储在meta space中,meta space占用外部内存,不占用堆内存。
-------------------------------------------------------------
二、GC回收算法
-------------------------------------------------------------
2.1 标记清除算法:标记清除分为两个阶段,标记阶段(标记从根节点开始的所有可达对象,未标记即未被引用)和清除阶段。缺点:两个阶段效率都很低;回收后内存空间不连续,产生碎片多,易导致提前GC。
2.2 复制算法:内存等分两块,相互复制存活的对象后清洗垃圾 缺点:内存利用率低
2.3 标记压缩法:先标记,然后存活的向一段移动,清理存活端标记以外的内存。(老年代使用,无需需要第二块相同的内存) 优缺点:无内存碎片,但是耗时。
2.4 分代算法:复制算法(新生代使用) ,标记压缩法和标记清除法(老年代使用)。卡表(数据结构,一个比特位的集合),用来表示老年代对象是否持有新生代对象的引用,新生代无需再花时间确认对象是否被持有,可以加快新生代回收的速度。
2.5 分区算法:将整个堆空间划分为连续不同的小的空间,独立管理,独立回收。
垃圾回收基本思想在于如何判断对象的可触及性。根据标记清除算法,可以扫描出root节点未触及持有的对象,但一个无法触及持有的对象有可能在某个时间下使自己复活。
对象的可触及性的三种状态:
可触及的
可复活的(finalize()函数)
不可触及的(finalize()函数只能调用一次)
2.6 引用和可触及的强度分为4个级别
强引用:任何时候都不会被系统回收,亦可能会引起OOM。 例:StringBuffer str = new StringBuffer("juejin");
软引用:GC不一定回收,但堆空间不足时会被回收。OOM之前一定会回收,所以软引用不会引起OOM。 使用SoftReference创建的对象。
弱引用:发现即回收。使用WeakReference创建的对象。使用PhantomReference创建的对象。
虚引用:随时可回收。
-------------------------------------------------------------
三、分代垃圾回收
-------------------------------------------------------------
3.1 young代采用复制算法
3.2 old代使用标记清除或者标记清理
3.3 Tip:对象优先在Eden去分配,大的对象直接进入老年代,长期存活对象进入老年代。
-------------------------------------------------------------
四、垃圾回收器
-------------------------------------------------------------
tip:Stop The World(STW)1、为了让垃圾回收器可以正常切高效执行。2、保证了系统某个瞬间的一致性。3、有益于垃圾回收器更好地标记垃圾对象。
4.1 串行回收器
单线程GC,启动时会停止应用,适用于配置小的服务器(1C2G),基本已弃用
4.2 并行回收器PS(吞吐量优先)
JDK1.6~1.8默认使用。垃圾线程并行,启动时应用会等待。
PS的新生代回收器有两个。
(1)、ParNew回收器:多线程执行垃圾回收。PS的线程数量可以用-XX:ParallelGCThreads指定。当CPU<8时,ParallelGCThreads的值=CPU,CPU>8时,ParallelGCThreads的值=3+((5*CPU_count)/8)。适用于交互较弱的场景。JDK1.8以上已经被删除。
(2)、Parallel回收器:与ParNew一样是多线程独占式。但其特点是关注系统的吞吐量(吞吐量:花费在垃圾收集时间和花费在应用时间的占比)。
使用方法:-XX:+UseParallelGC(设置老年代-XX:+UseParallelOidGC)
4.3 并发回收器(响应时间优先)(并行GC前会额外触发新生代的GC)
与并行回收器不相同的是,并发收集器是非独占式,在进行垃圾回收的时候应用程序也可以运行。
主要有Concurrent Mask Sweep(CMS)和G1
JDK1.9默认使用G1。适用于对响应时间有要求的场景。响应时间:花费在应用时间和花费在垃圾收集时间的占比。
CMS(以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现)
过程:1)初始标记(标记root对象) 2)并发标记 3)预清理(准备及控制停顿时间) 4)重新标记 5)并发清除 6)并发重置
优点:并发收集、低停顿。
缺点:
1)CMS对CPU资源敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
2)CMS无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
3)CMS容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
4)老年代垃圾回收过程中,如果出现资源不够用,则会强制进行老年代串行回收,应用暂停时间更长,影响更大。
G1(面向服务端应用的垃圾收集器)
1.7正式使用,且使用了全新的算法,看起来有取代CMS的趋势。
保留了分代的概念,但是从堆结构上看,分代内存并不是连续的。如图:
在并行性和并发性的基础上,可以同时兼顾年轻代和年老代,还可以进行空间整理,每次GC之后会自动进行碎片整理,减少碎片空间。最后还有可预见性,G1可以选取部分区域进行内存回收。
过程:1)初始标记(标记root对象)(eden区会被清空) 2)根区域扫描 3)并发标记 4)重新标记 5)独占清理 (计算各个区域存活对象和GC回收比例)6)并发清理
混合回收:在年轻代满时,触发年轻代收集;随着老年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,G1开始准备收集老年代空间。首先经历并发标记周期,识别出垃圾占比较高的老年代分区。但随后G1并不会马上开始一次混合收集,而是让应用线程先运行一段时间,等待触发一次年轻代收集。在这次STW中,G1将保准整理混合收集周期。接着再次让应用线程运行,当接下来的几次年轻代收集时,将会有老年代分区加入到CSet中,即触发混合收集,这些连续多次的混合收集称为混合收集周期(Mixed Collection Cycle)
特点:
1、并行于并发:G1能充分利用CPU多核,使用多个CPU来缩短stop-The-World停顿时间。
2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
-------------------------------------------------------------
五、调优思路
-------------------------------------------------------------
5.1 前瞻
1、尝试多种垃圾回收器,G1并不是最好的
2、并发不等于并行。垃圾回收的过程实际上有两步,启动GC周期和GC自身运行,这是不同的两件事。并发针对的是GC周期,而并行针对GC算法自身。
3、平均事务时间不是最需要被关注的指标,有可能用户正好经历了那个长时间GC的场景,那将是毁灭性的。
4、GC调优并不能解决所有的事。如果程序修改程度大,那应该优先优化架构及代码。
5、GC日志并不会对性能造成太大的影响,在GC未被优化之前,开启GC日志是有必要的。
6、降低新对象的分配率可以改善GC的运行状况。粗略地把系统中的对象分为三种:长命(long-lived)对象,对它们我们一般做不了什么;中等寿命(mid-lived)对象,最大的问题可能出现在这;短命(short-lived)对象,它们的释放和回收通常都很快,在下个GC周期来临时就会消失
5.2 思路
1、理解应用需求和问题。
2、掌握GC的状态。
3、思考选择的GC是否符合我们的应用特征。
4、分析确认需要调整的参数。
5、验证调优。
5.3 查看设置参数
java -XX:+PrintFlagsInitial <pid>查看初始值
-XX:+PrintFlagsFinal 查看最终值(初始值可能被修改掉)
-Xms 默认情况下堆内存的64分之一
-Xmx 默认情况下对内存的4分之一
-Xmn 默认情况下堆内存的3分之一
-XX:NewRatio 默认为2
-XX:SurvivorRatio 默认为8
-XX:+PrintGCDetails 开启GC详细日志-Xloggc:/cpic/cpicapp/perfma/xowl/../logs/xowl/gc.log -XX:+PrintGCDetails
5.4 GC一般合理表现
分析结果显示GC耗时在0.1-0.3秒以内的话,一般不需要花费额外的时间做GC调优。然而, 如果GC耗时达到1-3秒甚至10秒以上,就需要立即对系统进行GC调优 。
Minor GC执行迅速(50毫秒以内)
Minor GC执行不频繁(间隔10秒左右一次)
Full GC执行迅速(1秒以内)
Full GC执行不频繁(间隔10分钟左右一次)
-------------------------------------------------------------
六、参数调优
-------------------------------------------------------------
6.1 PS
6.1.1 吞吐量:-XX:GCTimeRatio=<N>垃圾收集时间与应用程序时间的比率设置为1/(1+<n>),默认值是99%(垃圾收集时间的1%)。
-Xmx<N>:指定最大堆占用空间。
优先级保证:暂停时间>吞吐量>堆空间。如果不设置初始堆内存和最大堆内存,则初始堆大小为物理内存的1/64,最大内存为1/4,年轻代大小为堆内存的1/3。
-Xms (初始堆内存) and -Xmx (最大堆内存):
如果知道应用程序需要多少堆才能正常工作,那么可以将-Xms和-Xmx设置为相同的值。如果不知道,那么JVM将首先使用初始堆大小,然后自动增长,直到它找到堆使用和性能之间的平衡。
三个重要参数:1、-XX:MaxGCPauseTimeMillis:设置最大垃圾回收时间停顿时间。
2、-XX:GCTimeRatio:设置吞吐量大小。
3、-XX:UseAdaptiveSizePolicy:自适应模式。新生代大小,eden区与survivor区的比例,晋升老年代的对象年龄等参数会被自动调整。
CMS
使用方法:-XX:+UseConcMarkSweepGC
并发线程数:(ParallelGCThreads+3)/4。也可用通过-XX:ConcGCThreads或者-XX:ParallelCMSThreads手工设置。
因为并发性质,所以CMS不会等到堆饱和时才进行垃圾回收。默认值为老年代占用率68%,通过-XX:CMSInitiatingOccupancyFraction设置。
内存压缩:设定多少次之后GC回收之后对内存进行一次压缩。-XX:CMSFullGCsBeforeCompaction。默认0
开启-XX:CMSClassUnloadingEnable,可以在需要时候Perm区的还会触发一次FullGC。
6.2 G1
6.2.1 启用G1(常用):-XX:+UseG1GC
堆内存(常用):-XX:InitialHeapSize(初始堆内存)-XX:MaxHeapSize(最大堆内存)
年轻代设置(常用):-XX:NewSize(最小) -XX:MaxNewSize(最大)
暂停时间(常用):-XX:MaxGCPauseTimeMillis=<N>(默认200ms)
空闲堆占比:-XX:MinHeapFreeRatio=40(GC后,如果发现空闲堆内存占到整个预估堆内存的40%,则放大堆内存的预估最大值,但不超过固定最大值。)-XX:MaxHeapFreeRatio=70
最大暂停间隔时间:-XX:PauseTimeIntervalMillis
GC停顿时候的并行的GC收集线程数:-XX:ParallelGCThreads=< ergo>根据虚拟机所在的主机的可用CPU线程数来计算的:如果CPU少于8个这个值就是cpu的数量,否则,就等于cpu数量*5/8。每个停顿开始的时候,最大的GC线程数还受限于最大的堆内存,G1的内个线程能使用的最大堆内存是由-XX:HeapSizePerGCThread来设置的。
与应用并发执行的GC线程数:-XX:ConcGCThreads=< ergo>:默认是-XX:ParallelGCThreads/4
region的大小:-XX:G1HeapRegionSize=< ergo>整个堆大概有2048个region,region的大小可以在1-32M之间,必须是2的次方。调整之后会影响分配对象的大小及停顿时间。
可分配的最大对象的大小: -XX:G1HeapRegionSize-XX:G1MaxNewSizePercent
-------------------------------------------------------------
七、针对项目经验集
-------------------------------------------------------------
7.1 这几种GC收集器相比之下,只要JDK版本在1.7u4及以上,推荐使用G1收集器。JDK1.7,1.8都默认使用PS(并行收集器)
7.2 尤其注意容器项目,容器设置的JVM配置内存大小不能大于容器内存大小,否则参数配置无效。
7.3 调优实例
7.3.1 实例一(集团2018版XXXXXXXX系统):
压测表现:稳定性压测时结果不稳定且内存消耗一直80%左右,影响时间3天。
内存分析表现:堆内存很大(7G)但年轻代内存非常小,年轻代minGC频繁,老年代内存一直增加直至触发majorGC,且GC暂停时间长,平均200~300ms
调优:调整年轻代内存为3G
export JAVA_OPTS="$JAVA_OPTS -Xmx7g -Xms7g -XX:NewSize=3g -XX:MaxNewSize=3g -XX:+UseG1GC"
优化结果:
复压,内存稳定在60%左右,年轻代GC频繁度减小,GC耗时100ms左右,老年代稳定无majorGC
7.3.2 实例二(寿险2013版XXXXXXXX系统):
压测表现:压测时压不上去,服务器消耗未满载。且压的时间长了TPS会有断崖式下降,TPS和相应时间非常不稳定。影响时间2天。
内存分析表现:heap内存只设置了2G,容器是3.5C7G,老年代一直增长直至触发majorGC,GC频繁且GC暂停时间不稳定。
调优:调整heap内存大小7G,新生代3G。
JAVA_OPTS="-Xmx7000m -Xms7000m -Xmn3072m -XX:PermSize=256M -XX:MaxPermSize=256M
调优结果:
TPS增长40~50且稳定。老年代稳定无majorGC。minorGC频繁度减小,GC暂停时间降低且稳定在几十ms以内。
7.3.3 Tip1:年轻代内存并不是越大越好,虽然会减小GC的频率,但是在GC时会增加回收时间造成GC暂停时间长。
Tip2:docker系统,如果容器内存4G,堆内存设置6G,进程可以启动且显示为6G,但实际只能使用到4G。
Tip3:如果开发和测试都不清楚如何设置堆大小及年轻代大小,可以参考perfma产品 http://xxfox.perfma.com/ ,填写相关参数会给出调优建议