JVM GC算法以及常用参数调优

回收算法

JDK 8 HotSpot Java虚拟机,用-XX:+PrintCommandLineFlags查看参数,发现Java8默认使用的垃圾收集器就是这个-XX:+UseParallelGC (新生代:Paralle Scavenge收集器,老年代:Parallel Old收集器)

新生代

Parallel Scavenge

在这里插入图片描述
主要思想就是,将内存分为eden区和survivor区(from区和to区),默认是8:2(对于eden : survivor1 : survivor2 = 8 : 1 : 1)的比例。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

PartNew

ParNewGC在JDK9中弃用了,JDK10中已经完全移除了,它的理想代替物是G1GC。

G1

G1收集器与之前垃圾收集器的一个显著区别就是——之前收集器都有三个区域,新、老两代和元空间。而G1收集器只有G1区和元空间。而G1区,不像之前的收集器,分为新、老两代,而是一个一个Region,每个Region既可能包含新生代,也可能包含老年代。
G1收集器既可以提高吞吐量,又可以减少GC时间。最重要的是STW可控,增加了预测机制,让用户指定停顿时间。
使用-XX:+UseG1GC开启,还有-XX:G1HeapRegionSize=n、-XX:MaxGCPauseMillis=n等参数可调。

特点
并行和并发:充分利用多核、多线程CPU,尽量缩短STW。
分代收集:虽然还保留着新、老两代的概念,但物理上不再隔离,而是融合在Region中。
空间整合:G1整体上看是标整算法,在局部看又是复制算法,不会产生内存碎片。
可预测停顿:用户可以指定一个GC停顿时间,G1收集器会尽量满足。过程与CMS类似。

初始标记 =》并发标记 =》最终标记 =》 筛选回收

老年代

Parallel Old

Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
复制算法也存在他自己的缺点,比如在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况。所以在老年代一般不直接选用这种算法。

  • 标记出所需要回收的对象
  • 所有存活对象向一端移动
  • 清理掉端边界以外的内存
    在这里插入图片描述
    有几个JVM选项可用于控制Parallel Old收集器的行为和老一代垃圾收集的频率。例如,-XX:MaxHeapFreeRatio选项可用于设置垃圾收集后允许空闲的堆的最大百分比。-XX:MinHeapFreeRatio选项可用于设置垃圾收集后需要空闲的堆的最小百分比。-XX:GCTimeRatio选项可用于控制GC时间和应用程序执行时间之间的平衡。通过调整这些选项,可以影响旧代垃圾收集的频率和触发它们的内存阈值。
CMS (Conc Mark Sweep)

并发标记清除收集器,是一种以获得最短GC停顿为目标的收集器。适用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望停顿时间最短。是G1收集器出来之前的首选收集器。使用标清算法。在GC的时候,会与用户线程并发执行,不会停顿用户线程。但是在标记的时候,仍然会STOP THE WORLD。

使用-XX:+UseConcMarkSweepGC开启。开启过后,新生代默认使用ParNew,同时老年代使用SerialOld作为备用。

在这里插入图片描述
过程

初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,需要STW,很短暂。
并发标记:主要标记过程,标记全部对象,和用户线程一起工作,不需要STW。
重新标记:修正在并发标记阶段出现的变动,需要STW。
并发清除:和用户线程一起,清除垃圾,不需要STW。
优缺点

优点
停顿时间少,响应速度快,用户体验好。

缺点
对CPU资源非常敏感:由于需要并发工作,多少会占用系统线程资源。
无法处理浮动垃圾:由于标记垃圾的时候,用户进程仍然在运行,无法有效处理新产生的垃圾。
产生内存碎片:由于使用标清算法,会产生内存碎片。

G1

后续会单独出一篇文章讨论G1回收器

常用参数

-server
JVM的server模式, 在多CPU服务器中性能可以得到更好地发挥。JDK的64位版本只支持server模式,因此在这种情况下,选项是隐式的。

SurvivorRatio
新生代2个Survivor区和Eden区的比值,默认值为8;即Eden区:From区:To区 = 8: 1:1
调小SurvivorRatio会增大from和to的空间大小,减小Eden空间; 调大SurvivorRatio会减小from和to的空间,增大Eden空间;

-XX:NewRatio
newRatio = 2 代表新生代占 1份,老年代占2份

新生代(Eden + 2*S)与老年代(不包括永久区)的比值;一般比值比较小1:2或1:4(避免较多的fullGC)
4 表示 新生代 :老年代 = 1:4 ,意思是老年代占 4/5

当新生代空间的大小超过一个特定的水平,程序的响应能力会被降低。因为新生代空间的垃圾回收过程,基本上是将数据从一个Survivor Area复制到另外一个(From Space和To Space)。另外,stop-the-world的现象在新生代空间和老年代空间执行垃圾回收时都会发生。如果新生代空间变大,那么Survivor Area的空间也会更大,于是每次复制的数据就更多。基于这样一种特性,我们应该通过指定不同操作系统中HotSpot JVM的NewRatio参数来分配合适大小的新生代空间。

-Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn,需要注意一点是-Xmn和-XX:NewRatio不要同时配置,这两个配置是冲突的,-Xmn会覆盖-XX:NewRatio。
-Xms 堆内存的初始大小,默认为物理内存的1/64
-Xmx 堆内存的最大大小,默认为物理内存的1/4

-Xss

设置栈内存的大小,设置的栈的大小决定了函数调用的最大深度
-Xss 设置的大小决定了函数调用的深度,如果函数调用的深度大于设置的Xss大小,那么将会抛“java.lang.StackOverflowError“ 异常。
一般设置为-Xss256k

-Xss 设置每个线程可使用的内存大小,即栈的大小。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

设置-Xss为512k输出情况如下图,我们可以看到当出count递归调用3707次后将会抛出栈溢出异常。

-XX:MetaspaceSize
指定元空间第一次触发垃圾回收的内存大小的阈值。当元空间内存占用不断增大,直到达到这个阈值时,就会触发一次垃圾回收。所以,适当的增大这个阈值,会减少垃圾回收的次数。默认值根据平台而定,一般情况下大约20.8MB。下面的例子是把元空间第一次触发垃圾回收的内存大小设置为256MB:

-XX:MetaspaceSize=256M

+UseConcMarkSweepGC
开启老年代CMS GC,CMS是一款优秀的收集器,它的最主要优点在名字上已经体现出来了:并发收集、低停顿,Sun的一些官方文档里面也称之为并发低停顿收集器(ConcurrentLowPauseCollector)。但是CMS还远达不到完美的程度。
在这里插入图片描述
三个缺点:
① CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程最多占用不超过25%的CPU资源。但是当CPU不足4个时(譬如2个),那么CMS对用户程序的影响就可能变得很大,如果CPU负载本来就比较大的时候,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,这也很让人受不了。为了解决这种情况,虚拟机提供了一种称为“增量式并发收集器”(IncrementalConcurrentMarkSweep/iCMS)的CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记和并发清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,速度下降也就没有那么明显,但是目前版本中,iCMS已经被声明为“deprecated”,即不再提倡用户使用。·

② CMS收集器无法处理浮动垃圾(FloatingGarbage),可能出现“ConcurrentModeFailure”失败而导致另一次FullGC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序的运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好留待下一次GC时再将其清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即还需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数以获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“ConcurrentModeFailure”失败,这时候虚拟机将启动后备预案:临时启用SerialOld收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量“ConcurrentModeFailure”失败,性能反而降低。

③ 还有最后一个缺点,在本节在开头说过,CMS是一款基于“标记清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会产生大量空间碎片。空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC。为了解决这个问题,CMS收集器提供了一个XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完FullGC服务之后额外免费附送一个碎片整理过程,内存整理的过程是无法并发的。空间碎片问题没有了,但停顿时间不得不变长了。虚拟机设计者们还提供了另外一个参数XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的FullGC后,跟着来一次带压缩的。

三色标记
三色分为黑色、灰色、白色,初始标记时jvm从GCRoots开始扫描直接引用对象,并发标记向下搜索那些被GCRoots直接引用对象的对象。当扫描完一个对象包含的所有引用后,就会将这个对象的对象头中标志位设置黑色标记(注意只是标志位设置了标记);如果某个对象并没有完全扫描完成,那么这个对象被置为灰色标记;还没有被扫描的对象不会标记(所有对象默认为白色)。白色表示GCRoot不可达,会被jvm回收

多标-浮动垃圾

在并发标记时,垃圾回收线程和业务线程并发执行,在方法运行结束后,方法内部存在栈中局部变量会被释放,此前初始标记时他们已经被标记为非垃圾对象,这些对象称为浮动垃圾,本轮Full GC不会回收他们,下一次full gc会清除他们。

另外并发标记过程产生的新对象会被标记为黑色,不会被jvm回收掉。

漏标-读写屏障(可以理解为AOP,在做某种操作时前后可以做额外处理

在并发标记时,业务线程可能会修改某些对象的引用,比如对象引用被删除后,又重新被其他已经扫描过的对象(标记为黑色)所引用,而这个引用已经无法被扫描了,这就产生了漏标。如果不做特殊处理,这些漏标的对象会被当成垃圾对象回收掉。jvm有两种解决方式:增量更新和原始快照

增量更新:将新增的黑色对象指向白色对象的引用通过集合记录下来,等并发标记结束后,将这些新增引用的黑色对象重新扫描一次

原始快照:当灰色对象要删除指向白色对象的引用时,将这些要删除的引用记录下来,等并发标记结束后,灰色对象指向的白色对象设置为黑色对象,标记为非垃圾对象,本次gc不回收,等下一次回收。

记忆集和卡表
新生代在做GCRoot可达性分析时,可能会碰到跨代引用的情况(如老年代引用新生代的对象),这种情况如果去扫描老年代效率会很低,也违背了新生代和老年代的设计初衷。通过在新生代引入记忆集的数据结构(从非收集区到收集去的指针集合) 解决跨代引用。

hotspot使用一种叫卡表的方式实现记忆集,卡表是一个字节数组的数据结构,每个元素对应某一内存区域,判断某一特定大小的内存区域是否存在跨代引用就是通过这个字节数组内某一元素的是否存指向收集区的指针即可。

CMS核心调优参数:

  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设
    定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引
    用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

-XX:+UseCMSCompactAtFullCollection
CMS收集器提供了一个XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完FullGC服务之后额外免费附送一个碎片整理过程,内存整理的过程是无法并发的。空间碎片问题没有了,但停顿时间不得不变长了。虚拟机设计者们还提供了另外一个参数XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的FullGC后,跟着来一次带压缩的。

-XX:+UseParallelGC
指 定在 New Generation 使用 parallel collector, 并行收集 , 暂停 app threads, 同时启动多个垃圾回收 thread, 不能和 CMS gc 一起使用 . 系统吨吐量优先 , 但是会有较长长时间的 app pause, 后台系统任务可以使用此 gc

-XX:MaxGCPauseMiilis:单位为ms,适用于高用户体验的场景,虚拟机将尽力保证每次MinorGC耗时不超过所设时长,但并不是该时间越小越好,因为GC耗时缩短是用调小年轻代获取的,回收500m的对象肯定要比回收2000m的对象耗时更短,但是回收频率也大大增大了,吞吐量也随之下去了。使用该参数的理论效果:MaxGCPauseMillis越小,单次MinorGC的时间越短,MinorGC次数增多,吞吐量降低。

-XX:GCTimeRatio:从字面意思上理解是花费在GC上的时间占比,但是实际含义并非如此,GC耗时的计算公式为1/(1+n),n为GCTimeRatio,因此,GCTimeRatio的实际用途是直接指定吞吐量。GCTimeRatio的默认值为99,因此,GC耗时的占比应为1/(1+99)=1%。使用参数的理论效果:GCTimeRatio越大,吞吐量越大,GC的总耗时越小。有可能导致单次MinorGC耗时变长。适用于高运算场景。

-XX:+UseParNewGC
并发串行收集器,它是工作在新生代的垃圾收集器,它只是将串行收集器多线程化,除了这个并没有太多创新之处,而且它们共用了相当多的代码。它与串行收集器一样,也是独占式收集器,在收集过程中,应用程序会全部暂停。但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
在这里插入图片描述

差异:
-XX:+UseParallelGC
指 定在 New Generation 使用 parallel collector, 并行收集 , 暂停 app threads, 同时启动多个垃圾回收 thread, 不能和 CMS gc 一起使用 . 系统吨吐量优先 , 但是会有较长长时间的 app pause, 后台系统任务可以使用此 gc

-XX:+UseParNewGC
指定在 New Generation 使用 parallel collector, 是 UseParallelGC 的 gc 的升级版本 , 有更好的性能或者优点 , 可以和 CMS gc 一起使用

-XX:MaxMetaspaceSize
指定元空间所分配内存的最大值,默认是没有限制,取决于系统的可用内存量,理论上可以占满整个系统的内存。为了避免这种惨剧,影响系统上的其他应用,需要适当设置它的大小。下面的例子是把元空间所分配内存的最大值设置为512MB。

-XX:MaxMetaspaceSize=512M

-XX:SurvivorRatio

2个Survivor区和Eden区的比值
8 表示 两个Survivor : Eden = 2: 8 ,每个Survivor占 1/10

XX:-UseAdaptiveSizePolicy

-XX:SurvivorRatio默认为8 -XX:NewRatio默认为2
-XX:SurvivorRatio默认为8,单独使用初始时生效,但是由于-XX:+UseAdaptiveSizePolicy默认开启,后期会自动调节Survivor区的大小,取消自适应即可使得-XX:SurvivorRatio生效!

-XX:SurvivorRatio=8

该参数作用:Eden区与每一个Survivor区的比值
-XX:SurvivorRatio=8,这是该参数的默认值,所以Eden:S0:S1=8:1:1。
-XX:SurvivorRatio=4,Eden:S0:S1=4:1:1,千万不要以为新生代是被分成10份,Eden:S0:S1会是4:3:3,这是错误的。
-XX:SurvivorRatio=5,Eden:S0:S1=5:1:1。

-Duser.timezone
JVM用户启动实例的默认时间,logback

如果不配置JVM 指定内存大小会怎么样?

当运行一个Spring Boot项目时,如果未设置JVM内存参数,Spring Boot默认会采用JVM自身默认的配置策略。在资源比较充足的情况下,开发者倒是不太用关心内存的设置。但一旦涉及到资源不足,JVM优化,那么就需要了解默认的JVM内存配置策略。

关于JVM内存最常见的设置为初始堆大小(-Xms)和最大堆内存(-Xmx)。很多人懒得去设置,而是采用JVM的默认值。特别是在开发环境下,如果启动的微服务比较多,内存会被撑爆。

而JVM默认内存配置策略分两种场景,大内存空间场景和小内存空间场景(小于192M)。

默认情况下,最大堆内存占用物理内存的1/4,如果应用程序超过该上限,则会抛出OutOfMemoryError异常。初始堆内存大小为物理内存的1/64。

如果应用程序运行在手机上或物理内存小于192M时,JVM默认的初始堆内存大小和最大堆内存大。

最大堆内存为物理内存的1/2,初始堆内存大小为物理内存的1/64,但当初始堆内存最小为8MB,则为8MB。

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此,服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。

其中最大堆内存是JVM使用内存的上限,实际运行过程中使用多少便是多少。默认,分配给年轻代的最大空间量是堆总大小的三分之一。

针对最开始的问题,如果每个程序都按照默认配置启动,一台服务器上部署多个应用时,就会出现内存吃紧的情况,造成一定的浪费。最简单的操作就是在执行java -jar启动时添加上对应的jvm内存设置参数。

java -Xms64m -Xmx128m -jar xxx.jar

切记参数要防止-jar参数之前。否则会被当做系统参数而无效。
当然在排查JVM的使用情况时,还会用到以下相关操作。

查看系统默认内存设置

通过上面的描述我们可以看到,不同的系统配置,JVM使用的内存是不同的。我们可以通过Java命令自带的功能来查看默认的内存设置。

在Linux操作系统下,输入如下命令:

java -XX:+PrintFlagsFinal -version | grep HeapSize

在Windows操作系统下,输入如下命令:

java -XX:+PrintFlagsFinal -version | findstr HeapSize

查看运行时内存情况

当应用程序运行时,如果我们想查看程序的运行情况,可通过以下几种方式来查询不同维度的数据。

查看正在运行的jvm服务

可以通过jps命令查看正在运行的jvm服务。

appledeMacBook-Pro:~ apple$ jps
51972 EurekaApplication
51973 Launcher
51976 Jps

参考文章:
面试官:怎么做JDK8的内存调优?
GC - UseParallelGC和UseParallelOldGC的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

澄风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值