本专栏学习内容来自尚硅谷宋红康老师的视频以及《深入理解JVM虚拟机》第三版
有兴趣的小伙伴可以点击视频地址观看,也可以点击下载电子书
有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection(GC),对应的产品(垃圾收集器)称为Garbage Collector(GC)两个都称为GC,本文中的GC理解为垃圾收集器
按线程数分
按工作模式分
按碎片处理方式分
按工作的内存区间分
吞吐量、暂停时间、内存占用三者不可能构成一个三角形,三者的表现会随着技术的进步而越来越好,一款优秀的收集器通常最多同时满足其中两项,主要抓住两点:吞吐量,暂停时间
吞吐量 = 运行用于代码时间 / (运行用户代码时间 + 垃圾手机时间)
如下图所示,在注重吞吐量的情况下,每次GC的暂停时间会较长,在注重低延迟的情况下,吞吐量会较低。
所以,在设计GC算法时,必须确定我们的目标,一个GC算法只可能针对两个目标之一(专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折中
现在的标准是:在最大吞吐量优先的情况下,降低停顿时间。
如下图所示,7款经典的垃圾收集器往往是在不同代之间工作的。
在执行GC的过程中,往往都是多个垃圾收集器组合使用。
两个收集器之间有连线,表面他们可以搭配使用。
方式一:通过-XX:+PrintCommandLineFlags
查看命令行相关参数(包括默认的垃圾收集器)
这里看到JDK8中在新生代中默认使用Parallel GC,那么老年代则是使用Parallel Old GC
方式二:通过命令行指令 jinfo -flag 相关垃圾回收器参数 进程ID
如下图所示,可以看到JDK8默认使用的时Parallel组合,而且并没有使用G1垃圾收集器
如下图所示,这个收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾回收工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
-XX:UseSerialGC
参数可以指定年轻代和老年代都适用串行收集器
如下图所示,对于新生代,回收次数频繁,使用并行方式高效;对于老年代,回收次数少,使用串行方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源)
-XX:+UseParNewGC
手动指定使用ParNew收集器执行内存回收任务,他表示年轻代使用并行收集器,不影响老年代-XX:ParallelGCThreads
可以限制线程数量,默认开启和CPU数据相同的线程数-XX:+UseParallelGC
:手动指定年轻代使用Parallel并行收集器执行内存回收任务-XX:+UseParallelOldGC
:手动指定老年代使用并行回收器
-XX:ParallelGCThreads
:设置年轻代并行收集器的线程数,一般的,最好与CPU数量相同,以避免过多的线程数影响垃圾收集性能
3+5*CPU_Count/8
-XX:MaxGCPauseMillis
:设置垃圾收集器最大停顿时间(即STW时间,单位毫秒)
-XX:GCTimeRedio
:垃圾收集时间占总时间的比例( = 1 / (N + 1))
-XX:MaxGCPauseMillis
参数有一定矛盾性,暂停时间越长,Redio参数就容易超过设定的比例-XX:+UseAdaptiveSizePolicy
:设置Parallel收集器具有自适应调节策略
CMS整个过程分为4个主要阶段
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
因为在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应确保应用程序用户线程有足够的内存可用。因此CMS收集器不能像其他收集器那样等到老年代几乎被完全填满了在进行收集,而是当堆内存使用率达到某一阈值,便开始进行回收,以确保应用程序在CMS工作中依然有足够的空间支持应用程序运行。
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
另外,CMS采用的是标记-清除算法,这意味着每次执行完回收后,不可避免的产生一些内存碎片,那么在为新对象分配内存空间时,只能使用空闲列表执行内存分配。
为什么不把算法替换成标记-压缩算法
因为清理的线程与用户线程是并发的,如果要整理的话必定要将对象的引用地址改变,用户线程中的地址就无法正常使用。
-XX:+UseConcMarkSweepGC
:手动指定使用CMS收集器执行内存回收任务
-XX:+UseParNewGC
打开-XX:CMSInitiatingOccupanyFraction
:设置堆内存使用率的阈值,一旦达到这个阈值,便开始回收
-XX:UseCMSCompactAtFullCollection
:用于指定在执行完Full GC后堆内存空间进行压缩整理,以避免内存碎片的产生,不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变长了
-XX:CMSFullGCsBeforeCompaction
:设置在执行多少次Full GC后对内存空间进行压缩整理
-XX:ParallelCMSThreads
:设置CMS的线程数量
如何选择三组回收器?
在JDK9以后,G1就是默认的垃圾回收器,去带哦了CMS回收器一起Parallel + Parallel Old组合
与其他GC收集器相比,G1使用全新的分区算法,其特点如下
并行与并发
分代收集
空间整合
可预测的停顿时间模型
这是G1相对于CMS的另一大优势,G1除了追求低停顿之外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。也就是说可以控制程序吞吐量
相较于CMS,G1还不具备全方位、压倒性优势,比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时额外执行负载都要比CMS要高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间
-XX:+UseG1GC
:手动指定使用G1收集器执行内存回收任务-XX:G1HeapRegionSize
:设置每个Region的大小。值是2的幂次方,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000-XX:MaxGCPauseMillis
:设置期望达到最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200ms-XX:ParallelGCThreads
:设置STW工作线程数的值,最多设置为8-XX:ComcGCThreads
:设置并发标记的线程数,将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右-XX:InitiatingHeapOccupancyPercent
:设置触发并发GC周期的Java堆占用阈值,超过此值,就会触发GC,默认值是45.在介绍G1的垃圾回收过程之前,先来了解一下Region的相关知识。
所有的Region大小相同,且在JVM生命周期内不会被改变。
为什么设置humongous区域?
对于堆中的大对象,默认直接分配到老年代,但如果他是一个短期存在的大对象,就会对垃圾收集器造成负面影响,而humongous区就是专门用来存放大对象。**如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。**为了能找到连续的H区,有时候不得不启动Full GC,G1大多数行为都把H区当作老年代的一部分看待。
这是一个对象被不同区域引用的问题
一个region不可能是孤立的,一个region中的对象可能被其他region中的对象引用,那么判断对象存货是,就需要扫描整个Java堆才能保证准确,这样会降低Minor GC的效率
解决方案
如下图所示,Region1和Region3分别引用了Region2中的两个对象,那么Region2对应的记忆集就会保存两个引用记录信息,当对Region2进行垃圾回收时,只需要遍历Region2和对应的结果集即可。
G1的垃圾回收过程主要包括以下三个环节
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden区空间耗尽时,G1会启动一次念你清代垃圾回收过程。年轻代垃圾回收只会回收Eden区和Survivor区。
YGC时,首先G1停止应用程序的执行(STW),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。然后开始如下回收过程:
扫描根
根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet(结果集)记录的外部引用作为扫描存活对象的入口
更新RSet
处理dirty card queue中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用。
在年轻代回收时,G1对会对dirty card queue中的所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。
处理RSet
识别被老年代对象指向的Eden区中的对象,这些被指向的Eden中的对象被认为是存活的对象
复制对象
此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达到阈值,年龄会加1,达到阈值会被福州道Old区中空的内存分段,如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代
处理引用
处理Soft、Weak、Phantom、Final、JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
初始标记阶段
标记从根节点直接可达的对象,这个阶段是STW的,并且会触发一次年轻代GC
根区域扫描
G1扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在young GC之前完成
并发标记
在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)
再次标记
由于应用程序持续进行,需要修正上一次的标记结果,是STW的。G1中采用比CMS更快的初始快照算法
独占清理
计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,为下阶段做铺垫,是STW的(这个阶段并不会实际上去做垃圾收集)
并发清理阶段
识别并清理完全空闲的区域
当越来越多的对象晋升到老年代region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个年轻代region,还会回收一部分的老年代region。这里需要注意,是一部分老年代,而不是全部老年代。可以选择哪些老年代region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。
-XX:G1MixedGCLiveThresholdPercent
,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间-XX:G1HeapWastePercent
,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不进行混合回收,因为GC会花费更多的时间但是回收道德内存却很少G1的初衷就是避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序执行,使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
导致G1Full GC的原因可能有两个
-Xmn
或-XX:NewRatio
等相关选项显式设置年轻代大小