JVM调优(其三)

三、JVM的GC系统算法分析

1、GC简介

1.1引言

在理解GC之前,先回顾一下JVM体系结构,如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2何为GC

基于正在使用的对象进行遍历,对存活的对象进行标记,其未标记对象可认为是垃圾对象,然后基于特定算法进行回收,这个过程称之为GC(Garbage Collection),所有的GC系统可从如下几个方面进行实现:

  • GC判断策略(例如引用计数,对象可达性分析)
  • GC收集算法(标记-清除,标记-清除-整理,标记-复制-清除)
  • GC收集器(例如Serial,Parallel,CMS,G1)
    为何要学习GC呢?
    深入理解GC的工作机制,可以帮你写出更好的java应用,同时也是进军大规模应用开发的一个前提。
1.2.1手动GC

手动GC即显式地进行内存分配(allocate)和内存释放(free)。如果忘记释放,则对应的那块内存不能再次使用。内存一直被占着,却不能使用,这种情况就称为内存泄漏(memory leak)。
手动GC时忘记释放内存是相当容易的,这样会直接导致内存泄漏。

1.2.1自动GC

自动GC一般是再JVM系统内存不足时,由JVM系统启动GC对象,自动对内存进行垃圾回收。
引用计数法
在这里插入图片描述
其中:
1)绿色云朵时内存中的根对象,表示程序中正在使用的对象。
2)蓝色圆圈是内存中的活动对象,其中的数字表示其引用对象。
3)灰色圆圈是内存中没有活动对象引用的对象,表示非活动对象。
对于引用计数法,有一个很大的缺陷就是循环引用,例如:

其中红色对象实际上是应用程序不使用的垃圾,但由于引用计数的限制,仍然存在内存泄漏。当然也有一些办法来应对这种情况,例如“弱引用”(weak references)或者使用其他的算法来排查循环引用等。
标记清除
标记清除就是对可达对象进行标记,不可达对象即认为是垃圾,然后进行清除。
标记清除通常有两个步骤:
1)标记所有可到达的对象
2)清除不可到达对象占用的内存地址
在这里插入图片描述
此方法解决了循环依赖问题,单存在短时间的线程暂停,我们一般称这种现象为STW停顿(Stop The World Pause,全线暂停)。
说明:
JVM中包含了多种GC算法收集器,他们虽在实现上略有不同,但理论上都采用了以上两个步骤。

2、GC入门分析

2.1碎片整理

系统GC时每次执行清除(sweeping)操作,JVM都必须保证“不可达对象”占用的内存能被回收然后重用。内存是被回收了,但这有可能会产生大量的内存碎片(类似于磁盘碎片),进而引发两个问题:
1)对象创建时,执行写入操作越来越耗时,因为寻找一块足够大的空闲内存会变得更加麻烦。
2)对象创建时,JVM需要在连续的内存块中为对象分配内存。如果碎片问题很严重,直至没有空闲片段能存放新创建的对象,就会发生内存分配错误(Allocation error)。
为了解决碎片问题,JVM在启动GC执行垃圾收集的过程中,不仅仅是标记和清除,还需要执行“内存碎片整理”。这个过程会让所有可达对象(reachable objects)进行依次移动,进而可以消除(或减少)内存碎片,并为新对象提供更大并且连续的内存空间。示意图如下
在这里插入图片描述
说明:内存整理时会将对象移动到靠近内存地址的起始位置

2.2分代设想

我们知道垃圾收集要停止整个应用程序的运行,那么假如这个收集过程需要的时间很长,就会对应用程序产生很大性能问题,如何解决这个问题呢?通过实验发现内存中的对象通常可以将其分为两大类:
1)存活时间较短(这样的对象比较多)
2)存活时间较长(这样的对象比较少)
基于对如上问题的分析,科学家提出了分代回收思路,将VM中内存分为年轻代(Young Generation)和老年代(Old Generation-老年代有时候也成为老年区)。
在这里插入图片描述
分代设想将内存拆分为两个可单独清理的区域,允许采用不同的算法来大幅提高GC的性能。但这种方法也不是没有问题。例如,在不同分代中的对象可能会互相引用,这样的对象就难以回收。

2.3对象分配

Java中堆内存的内存结构如下图所示:
在这里插入图片描述
基于此内存架构,对象内存分配过程如下:
1)编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配。
2)如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配。
3)如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区)。
4)如果Eden区存储对象,则执行Young GC(Minor Collection)。
5)如果Young GC之后Eden区仍然不足以存储对象,则直接分配在老年代。
说明:在对象创建时可能会触发Young GC ,此GC过程的简易原理图分析如下:
在这里插入图片描述
其中:
1)新生代由Eden区和两个幸存区构成(假定s1,s2),任意时刻至少有一个幸存区是空的(empty),用于存放下次GC时未被收录的对象;
2)GC触发时Eden区所有“可达对象”会被复制到一个幸存区,假设为s1,当幸存区s1无法存储这些对象时会直接复制到老年代。
3)GC再次触发时Eden区和s1幸存区中的“可达对象”会被复制到另一个幸存区s2,同时清空eden区和幸存区s1;
4)GC再次触发时Eden区和s2幸存区中的“可达对象”会被复制到另一个幸存区s1,同时清空eden区和幸存区s2,以此类推;
5)当多次GC过程完成后,幸存区中的对象存活时间达到了一定阈值(可以用参数 -XX:+MaxTenuringThreshold来指定上限,默认15),会被看成是“年老”的对象然后直接移动到老年代。

2.4GC模式分析

垃圾收集事件(Garbage Collection Events)通常分为:
1)Minor GC(小型GC):年轻代GC事件,(新对象)分配频率越高,Minor GC的频率就越高
2)Major GC(大型GC):老年代GC事件
3)Full GC(完全GC):整个堆的GC事件
说明:一般情况下可以将Major GC与Full GC看成是同一种GC。

3、GC算法基础

3.1标记可达对象

现在的GC算法,基本都是要从标记“可达对象”开始(Marking Reachable Objects),这些标记为可达的对象即为存活对象。同时我们可以将查找可达对象时的起始位置对象,认为是根对象(Garbage Collection Root),基于根对象标记可访问或可达对象,对于不可达对象,GC会认为是垃圾对象,例如:在下面图示中的绿色云朵为根对象,蓝色圆圈为可达对象,灰色圆圈为垃圾对象。
在这里插入图片描述
首先,GC遍历(traverses)内存中整体的对象关系图(Objects graph)确定根对象,那么什么样的对象可作为根对象呢?GC规范中指出根对象可以是:
1)栈中变量直接引用的对象
2)常量池中引用的对象
3)…
其次,确定了根对象以后,进而从根对象开始进行依赖查找,所有可访问到的对象都认为是存活对象,然后进行标记(mark)。
说明:标记可达对象需要暂停所有应用线程,以确定对象的引用关系。其暂停的时间,与堆内存大小、对象的总数没有直接关系,而是由存活对象(alive objects)的数量来决定。

3.1移除不可达对象

移除不可达对象(Removing Unused Objects)时会因GC算法的不同而不同,但是大部分的GC操作一般都可大致分为三类:清除(Mark-Sweep),整理(Mark-Sweep-Compact),复制(Mark-Copy)
标记-清除(Mark-Sweep)
对于标记清除算法(Mark And Sweep Algorithms)应用相对简单,但内存会产生大量的碎片,这样再创建大对象时,假如内存没有足够连续的内存空间可能会出现OutOfMemoryError。
在这里插入图片描述
标记-清除-整理(Mark-Sweep-Compact)
标记清除整理算法中在清除垃圾对象以后会移动可用对象,对碎片进行压缩,这样会在内存中构建相对比较大的连续空间便于大对象的直接存储,但是会增加GC暂停时间。
在这里插入图片描述
标记-复制(Mark and Copy)
标记复制算法会基于标记清除整理算法,但是会创建新的内存空间用于存储幸存对象,同时复制与标记可以同时并发执行,这样可以减少GC时系统的暂停时间,提高系统性能
在这里插入图片描述

4、GC算法实现简介

4.1GC算法实现简介

我们知道,JVM系统在运行时,因新对象的创建,可能会触发GC事件,无论哪种GC都可能会暂停应用程序的执行,但如何将暂停时间降到最小,这要看我们使用的GC算法。现在对于JVM中的GC算法无非两大类:一类负责收集年轻代,一类负责收集年老代。假如没有显式指定垃圾回收算法,一般会采用系统平台默认算法,当然也可以自己指定,例如JDK8中基于特定垃圾回收算法的垃圾收集器应用组合如下:
在这里插入图片描述
其中:
1)年轻代和老年代的串行收集器:Serial GC
2)年轻代和老年代的并行收集器:Parallel GC
3)年轻代的并行收集器(Parallel new)+老年代的并发收集器(CMS-Concurrent Mark and Sweep)
4)年轻代和老年代的G1收集器,负责回收年轻代和老年代
说明:除了以上几种组合方式外,其他的组合方式要么现在已经不支持,要么不推荐。如何对这些组合进行选择,要结合系统的特点。例如系统是追求高吞吐量还是响应时间,还是两者都要兼顾,总之,对于GC组合的选择没有最好,只有更好。知己知彼,才能百战不殆。结合当前系统的环境配置,性能指标以及GC器特点,不断进行GC日志分析,定位系统问题,才是一般是选择哪种GC的关键。

4.2GC收集器应用分析

4.2.1Serial收集器应用分析

Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5和JAVA SE6中客户端虚拟机采用的默认配置。
Serial GC(串行收集器)应用特点:
1)内部只使用一个线程执行垃圾回收(不能充分利用CPU的多核特性),无法并行化。
2)GC时所有正在执行的用户线程暂停并且可能会产生较长时间的停顿(Stop the world)
Serial GC(串行收集器)场景应用:
1)一般可工作在JVM的客户端模式
2)适用于CPU个数或核数较少且内存空间较小(越大可能停顿时间越长)的场景
Serial GC(串行收集器)算法应用:
1)新生代使用mark-copy(标记-复制)算法(新生代存活对象较少)
2)老年代使用mark-sweep-compact(标记-清除-整理)算法(老年代对象回收较少,容易产生碎片)
Serial GC(串行收集器)实践应用:
在这里插入图片描述
其应用参数配置:-XX:+UseSerialGC
总之,Serial GC一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,适合单cpu小应用,实时性要求不是那么高场景。一般在JVM的客户端模式下应用比较好

4.2.2GC收集器之CMS应用分析

CMS的官方名称为“Mostly Concurrent mark and Sweep Garbage Collector”,其设计目标是追求更快的响应时间。
CMS(并发收集器)应用特点:
1)使用空闲列表(free-lists)管理内存空间的回收,不对老年代进行碎片整理,减少用户线程暂停时间
2)在标记-清除阶段的大部分工作和用户线程一起并发执行
3)最大优点是可减少停顿时间(可提高服务的响应速度),最大缺陷是老年代的内存碎片
CMS(并发收集器)场景应用:
1)应用于多个或多核处理器,目标降低延迟,缩短停顿时间,响应时间优先
2)CPU受限场景下,因与用户线程竞争cpu,吞吐量会减少
CMS(并发收集器)算法应用:
1)年轻代采用并行方式的mark-copy(标记-复制)算法
2)老年代主要使用并发mark-sweep(标记-清除)算法
CMS(并发收集器)关键步骤分析:
1)初始标记(initial mark)此阶段标记一下GC Roots能直接关联到的对象,速度很快
2)并发标记(concurrent mark)此阶段就是进行GC Roots Tracing的过程,从直接关联对象遍历所有可达对象,然后进行标记
3)重新标记(final remark)此阶段要修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
4)并发清除(concurrent sweep)此阶段与应用程序并发执行,不需要STW停顿。目的是删除未使用的对象,并收回他们占用的空间
5)并发重置(concurrent reset)此阶段与应用程序并发执行,重置CMS算法相关的内部数据,同时GC线程切换到用户线程
CMS(并发收集器)实践应用:
在这里插入图片描述
使用CMS的参数配置:-XX:+UseConcMarkSweepGC,默认开启-XX:+UseParNewGC
其他参数配置:
1)-XX:+UseCMSCompactAtFullCollection 执行Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
2)-XX:+CMSFullGCsBeforeCompcation 设置进行几次Full GC后,进行一次碎片整理
3)-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用cpu数量)
总之,CMS垃圾收集器在减少停顿时间上做了很多给力的工作,大量并发执行的工作并不需要暂停应用线程。如果服务器是多核cpu,并且主要调优目标是降低延迟,那么使用CMS是个很明智的选择。CMS垃圾收集可减少每一次GC停顿的时间,这样会直接影响到终端用户对系统的体验,用户会认为系统非常灵敏。但是因为多数时候都有部分cpu资源被GC消耗,所以在cpu资源受限的情况下,CMS会比并行GC的吞吐量差一些。还有就是老年代内存碎片问题,在某些情况下GC会造成不可预测的暂停时间,特别是堆内存较大的情况下。

4.2.3GC收集器之Parallel应用分析

Parallel收集器为并行收集器,它可利用多个或多核cpu优势实现多线程并行gc操作,其目标是减少停顿时间,实现更高的吞吐量(Throughput)。
Parallel GC(并行收集器)应用特点:
1)可利用cpu的多核特性执行多线程下的并行化GC操作
2)GC期间,所有CPU内核都在并行清理垃圾,所以暂停时间较短
3)最大优势是可实现可控的吞吐量与停顿时间
Parallel GC(并行收集器)场景应用:
1)GC操作仍需暂停应用程序(也有可能暂停时间较长,因为GC阶段不能被打断),所以不适合要求低延迟的场景
2)因其高吞吐GC(throughput GC)量的特性,适用于后台计算、后台处理的弱交互场景而不是web交互场景
Parallel GC(并行收集器)算法应用:
1)在年轻代使用标记-复制(mark-copy)算法,对应的是Parallel Scavenge收集器
2)在老年代使用标记-清除-整理(mark-sweep-compact)算法,对应的是Parallel Old收集器
Parallel GC(并行收集器)实践应用:
在这里插入图片描述
使用Parallel GC的参数配置:-XX:+UseParallelGC,默认开启-XX:+UseParallelOldGC
其他参数配置:
1)-XX:ParallelGCThread=20:设置并行收集器的线程数:即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等
2)-XX:MaxGCPauseMills=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值,
3)-XX:+UseAdaptiveSizePolicy设置并行收集器自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开
4)-XX:GCTimeRatio=99,设置吞吐量大小,默认值就是99,也就是将垃圾回收的时间设置成了总时间的1%。它的值是一个0-100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾回收。
总之,Parallel GC是一种并行收集器,可利用多CPU优势,执行并行GC操作,吞吐量较高,并可有效降低工作线程暂停时长,但是因为垃圾收集的所有阶段都不能被打断,所以Parallel GC还是有可能导致长时间的应用暂停,所以Parallel GC适合于需要高吞吐量而对暂停时间不敏感的场合,比如批处理任务。

4.2.4、G1收集器应用分析

G1(Garbage-First)收集器是一种工作于服务端模式的垃圾回收器,主要面向多核,大内存的服务器。G1在实现高吞吐的同时,也最大限度满足了GC停顿时间可控的目标。在Oracle JDK7 update 4后续的版本中已全面支持G1回收器功能。G1收集器主要为有如下需求的程序设计。
1)可以像CMS收集器一样能同时和应用线程一起并发的执行;
2)减少整理内存空间时的停顿时间
3)要满足可预测的GC停顿时间需求
4)不能牺牲太多的吞吐性能
未来G1计划要全面取代CMS。G1相比CMS有更多的优势,G1时压缩型收集器,可以实现更有效的空间压缩,消除大部分潜在的内存碎片问题。G1提供了更精准的可预测的垃圾停顿时间设置,可满足用户在指定垃圾回收时间上的需求。
在G1中,堆不再分成连续的年轻代和老年代空间,而是划分为多个(通常是2048个)可以存放对象的小堆区(Smaller heap regions)。每个小堆区都可能是Eden区,Survivor区或者Old区。在逻辑上,所有的Eden区和Survivor区合起来就是年轻代,所有的Old区拼在一起那就是老年代,如下图所示:
在这里插入图片描述
这样的划分使得GC不必每次都去收集整个堆空间,而是以增量的方式来处理。GC时每次只处理一部分小堆区,称为此次的回收集(collection set),GC时间的每次暂停都会收集所有年轻代的小堆区,同时也可能包含一部分老年代小堆区,如下图所示:
在这里插入图片描述
G1在并发阶段估算每个小堆区存活对象的总数,垃圾最多的小堆区会被优先收集,这也是G1名称的由来。
G1以一种和CMS相似的方式执行垃圾回收,G1在并发标记阶段估算每个小堆区存活对象的总数,垃圾最多的小堆区会被优先收集,这也是G1名称的又来。顾名思义,G1将其收集和压缩活动集中在堆中可能充满可回收对象(即垃圾)的区域上。G1通过用停顿预测模型来满足用户自定义的停顿时间目标,它基于设定的停顿时间来选择要回收的regions数量。
G1基于标记,清理对应的regions时,会将对象从一个或多个region里复制到另一个region里,在这个过程中会伴随着压缩和释放内存。清理过程在多核机器上都采用并行执行,来降低停顿时间,增加吞吐量,因此G1在持续的运行中能减少碎片,满足用户自定义停顿时间需求。这种能力是以往的回收期所不具备的(例如CMS回收期不能进行碎片压缩,ParallelOld只能进行整堆的压缩,会导致较长的停顿时间)。
再次强调:G1不是一个实时的收集器,它只是最大可能的来满足设定的停顿时间,G1会基于以往的收集数据,来评估用户指定的停顿时间可以回收多少regions,需要花费的时间,然后确定停顿时间内可以回收多少个regions。
G1收集器特点:
1)将java堆均分成大小相同的多个区域(region,1M-32M,最多2000个,最大支持堆内存64G)
2)内存应用具备极大地弹性(一个或多个不连续的区域共同组成eden、survivor或old区,但大小不再固定)
3)相对CMS有着更加可控的暂停时间(pause time)和更大的吞吐量(throughput)以及更少的碎片(标记整理)
4)支持并行与并发,可充分利用多cpu,多核优势,降低延迟,提高响应速度。
G1场景应用分析:
1)FullGC发生相对比较频繁或消耗的总时长过长
2)对象分配率或对象升级至老年代的比例波动较大
3)较长时间的内存整理停顿
说明:如果你现在用CMS或者ParallelOldGC,ing且你的程序运行很好,没有经历长时间垃圾回收停顿,建议就不用迁移
G1算法应用分析:
1)年轻代标记复制算法
2)老年代标记清除整理算法
G1关键步骤应用分析:(Old Generation)
1)初始标记(Initial Mark):属于Young GC范畴,是Stop-the-world活动。对持有老年代对象引用的Survivor区(Root区)进行标记;
2)根区扫描(Root Region Scan):并发执行,扫描那些对old区有引用的survivor区,在young gc发生之前该阶段必须完成;
3)并发标记(Concurrent Mark):并发执行,找出整个堆中存活的对象,将空区域标记为“X”,此阶段也可能会被Young GC中断;
4)再次标记(Remark):完全完成对heap存活对象的标记。采用snapshot-at-the-beginning(SATB)算法完成,比CMS用的算法更快
5)清理(cleanup):并发执行,统计小堆区中所有存活的对象,并对小堆区进行排序,优先清理垃圾多的小堆区,释放内存
6)复制/清理(copy/clean):对小堆区未被清理对象进行复制,然后再清理。
G1实践应用分析:
在这里插入图片描述
其应用参数配置:-XX:+UseG1GC表示启用GC收集器
其他参数配置:
1)-XX:MaxGCPauseMills=200 -设置成最大GC停顿时间(GC pause time)指标(target),这是一个软性指标(soft goal),JVM会尽力去达成这个目标,所以有时候这个目标并不能达成,默认值为200毫秒
2)-XX:InitiatingHeapOccupancyPercent=45 -启动并发GC时的堆内存占用百分比,G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为0则表示“一直执行GC循环”,默认值为45(表示堆使用了45%)。
总之:
G1是HotSpot中最先进的准产品级(production-ready)垃圾收集器,重要的是,HotSpot工程师的主要精力都放在不断改进G1上面,在新的java版本中,将会带来新的功能和优化。
上一篇:JVM调优(其二)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值