扩大内存可以更少的触发gc
内存太大触发gc的时候停顿时间会长
因此要根据你实际的业务场景设置成一个合适的值,并且配合压测和线上环境的实际情况做不断的调优.

吞吐量概念

吞吐量=花费在非Gc停顿上的工作时间/整个运行的总时间

假如说程序一共运行了100秒,其中99秒在处理应用,只有1秒在做gc相关的工作,那么我们的吞吐量就是百分之99.
针对互联网应用来说,我们的吞吐量至少要优化到百分之95以上,甚至优化到百分之98以上,才能给用户一个非常好的体验.因为你永远无法控制程序触发gc的stop the word 会影响什么样的业务,会影响什么样的用户.因此,吞吐量是非常的重要的.

-Xms和-Xmx参数设置问题

-Xms 刚启动JVM时堆内存初始化的大小
-Xmx 堆内存最大限制,在实际的生产环境上,必须要把-Xms 和-Xmx设置成一样的,本质上来说我们server服务器是24小时常驻内存去做用户的一个服务的,因此将-Xms 和-Xmx设置成不一样的话,往往启动时候jvm堆大小会比-Xmx设置的大小会小一些,这个时候我们应用内部的JVM就会扩缩容,去扩到-Xmx设置的参数,当应用服务器闲置下来的话,又去缩容.
这样情况是非常不利于cpu运算的,因为我们在做扩容的时候,往往代表我们的server处理业务在增多,这个时候我们的cpu又要过多的关注对应的jvm内存的扩容,又要去应对我们server处理业务,本质上来说会对我们cpu产生一个干扰,因此,我们要把-Xms和-Xmx的值设置成一样的.

年轻代大小

-XX:NewSize 年轻代的大小
-XX:MaxNewSize 最大年轻代的大小

需要将-XX:NewSize 和-XX:MaxNewSize 这两个参数设置成一样的,防止年轻代大小扩缩容,同时年轻代大小设置的比,老年代大一些.

年轻代的大小参数往往设置会比老年代大小稍微大一些.因为针对互联网的应用来说,本质上是request驱动的,来一个request就处理一个,处理完了就急销毁,如果说我们年轻代内存设置的大一些.就可以保证对象在触发gc次数一定的时候,可以尽可能的减少晋升老年代的时间,因为晋升到老年代之后,我们是尽可能的减少老年代的gc触发

-XX:SurvivorRation Eden Survivor 是 eden和survivor 占比,默认为8,代表假如说年轻代内存设置了100.那么80是给eden的,另外20是给两个survivor区,每个survivor区各占10.
为什么默认值是8,假如说我每次触发young gc 之后,会有百分之90的对象做清除,这个是工程学的假设,我们自然可以去符合业务的场景去调整这个参数,
但是永远要记住,eden区域要比survivor设置的大至少一倍大,否则就没有任何的意义了

元数据区对应参数

jdk1.8之后没有了永久代的概念,而是转化成了元数据区域(Metaspace),

-XX:MetaspaceSize 元空间初始空间大小
-XX:MaxMetaspaceSize=512m 元空间最大空间,默认是没有限制的.

-XX:MetaspaceSize 和-XX:MaxMetaspaceSize 是不建议去设置的

元数据区离开了堆,放到了本地内存区 , 如果我们限制了元数据区最大空间,如果内存超过了-XX:MaxMetaspaceSize的值,我们的程序就会报错,如果说我们不设置-XX:MaxMetaspaceSize 属性,而是我们有预期我们static变量和类分别是多少,这些东西不会随着运行的时候动态的增长许多,这个时候我们其实就可以忽略-XX:MetaspaceSize -XX:MaxMetaspaceSize 这些参数.完全交给我们的jvm自己去分配应该给多少就完事儿了,我们开发人员不需要关心.

Gc优化

1.将进入到老年代的对象尽可能的减少到最低,因为本质上来说要减少老年代的扫描对象的时间,我们需要将真正被定义为永久常驻内存的一些数据给放到老年代当中去,减少放入老年代对象的解决方式是调整新生代的大小

young gc如果40ms内就能完成垃圾回收,那么说明你的机器young gc已经优化到比较好的一个状态了

major gc: major gc触发 stop the world时间总和在100ms内是非常好的一个状态,实际生产环境下,如果major gc 如果触发的不是特别的频繁的话,那么stop the world时间总和可以放宽到200ms~500ms内都是可以的

full gc : 尽可能的少,实在没有办法触发了full gc,也要将时间控制在1秒内,不然许多应用服务的超时机制就会收到严重的影响.

除了cms和g1垃圾回收器之外,其余串行的老年代垃圾回收器或者并行的老年代垃圾回收器的major gc和full gc 是等价的.它们老年代满了,都会同时触发young gc和 major gc的运作,
而cms和g1由于采取了一个对应的初始标记,并发标记以及最终标记,外加清理的一个状态,它们两个(young gc 和 major gc )和full gc是不等同的,只有都等到一个特殊的条件,我们的cms 或者 g1 才会被晋升成为full gc .

垃圾回收器选择

jdk1.9默认是g1垃圾回收器, jdk1.8建议还是cms垃圾回收器,不要使用g1,因为g1在jdk1.8版本问题非常的多.
现在大多数公司都是jdk1.8版本,还是用cms垃圾回收器比较稳妥,当然如果你们公司jdk升级到了1.9甚至1.9以后,那么还是推荐使用g1 垃圾回收器

cms优化条件和能力

触发cms的major gc其实并不可怕,可怕的是由于major gc 中间过程的一些失败,或者发生的一些问题,导致了cms变成了一个full gc 的执行.

什么时候cms会变成full gc, 主要有两种状态:
1.promotion failure: 由于内存碎片导致的晋升空间不足,当我们的年轻代在survivor区域当中存活过15个周期之后,我们对应的年轻代就会往年老代中去晋升对象,年老代的cms名义上来说是通过标记整理的方式,不会产生内存碎片,但是整理的动作是通过参数开启的,如果我们没有开启参数,或者是开启了也导致我们晋升过来的对象在老年代当中是没有足够的连续空间去做存放的,这个时候就会发生promotion failure ,由于内存碎片导致晋升空间不足,发生promotion failure会触发full gc

2.concurrent mode failed: 还未完成cms又触发下一次的major gc,由于我们的cms是经过初始标记,并发标记,最终标记,清理 这四个大阶段,因此周期本质上来说是相对有点长的,如果说我们的内存分配的速度比cms回收内存的速度更短的话,那么就会出现cms垃圾回收还没回收完毕后又触发了下一次major gc ,这个时候就会出现concurrent mode failed,这个时候major gc 就会变成full gc ,full gc 是没有办法去做并发收集的,有可能我们就变成了串行收集器或者并行收集器会变成stop the world阶段,因此我们必须想办法尽可能减少full gc的一个运行

cms调优参数

-XX:ParallelGcThreads= N 设置年轻代的并行收集线程数:
明明我的cms是做老年代垃圾回收,为什么还要设置年轻代的并行收集线程数?这是一个踩坑的问题,比如说docker容器,如果我们jvm不配置-XX:ParallelGcThreads参数的话,那么jvm在启动的时候就会获取当台机器上的CPU的核心数去决策放多少个年轻代的并行收集线程数,但是在jdk早起版本当中,在docker里面读取是物理机的cpu核心数,并非是docker配置的核心数,试想一下, 如果我们是64核心的物理机,以4核为单位架起一个docker,一共16个docker,每个docker都会用满这64核心的物理机cpu核心做-XX:ParallelGcThreads参数分配的线程数做收集,那这样是非常坑的. 所以还是要自己设置-XX:ParallelGcThreads 是非常好的习惯.

-XX:ParallelCMSThreads= N 设置cms老年代的并行收集线程数,也是需要手动的去设置,如果我们指定了-XX:ParallelGcThreads参数,那么-XX:ParallelCMSThreads如果你不设置的话会自动根据-XX:ParallelGcThreads的参数自动调整,那么也就是说-XX:ParallelCMSThreads 参数可以不设置

-XX:+UseCMSCompactAtFullCollection 在FullGC情况下initial remark or final remark都整理内存碎片
-XX:+CMSFullGCsBeforeCompaction=4 这个参数要配合-XX:+UseCMSCompactAtFullCollection 使用的,设置4之后,我们在两次full gc的情况下,会各自触发两次Initial remark 或者final remark,这个时候,我们会经过4次之后才会去整理内存碎片,这两个参数放在一起用的意思,如果我开启了-XX:+UseCMSCompactAtFullCollection,不开启-XX:+CMSFullGCsBeforeCompaction=4 ,那么每次的full gc 都会去触发整理内存碎片的动作,整理内存碎片的动作是比较耗时的,而且一旦触发了full gc 对应就会产生 stop the world ,那么内存碎片整理动作就会把stop the world 时间拉长的,我们设置了XX:+CMSFullGCsBeforeCompaction=4 这个阈值之后,这样代表我们触发两次full gc才去做一次内存碎片的整理. 这样我们就能更好的去权衡性能和内存碎片之间的关系

-XX:CMSInitiationOccupancyFraction=70: 代表老年代占满百分之70内存才触发cms垃圾回收,参数的设置非常的重要,如果参数设置太低的话,我们的cms触发会非常的频繁,设置的太高,可能根本来不及触执行完当前的cms垃圾回收,就触发 concurrent mode failed 效果.因此推荐设置参数为70是一个比较合理.

-XX:+UseCmsInitiationOccupancyOnly: 开启了-XX:CMSInitiationOccupancyFraction=70之后也需要开启-XX:+UseCmsInitiationOccupancyOnly这个参数, 如果说我们不配置这个参数的话. jvm只会遵从一次-XX:CMSInitiationOccupancyFraction=70的配置, 第一次当老年代占满百分之70的内存的时候触发gc,那么当第二次的时候jvm就不遵从-XX:CMSInitiationOccupancyFraction=70的配置了,jvm会自己去判断当前系统情况来决定触发cms,为了防止jvm自己瞎搞,我们肯定需要设置-XX:+UseCmsInitiationOccupancyOnly

-XX:+CMSScavengeBeforeRemark : remark前先做一次minor gc

-XX:+CMSParallelRemarkEnable : 在final remark的阶段需要做并行remark, 如果配置了-XX:+CMSScavengeBeforeRemark 之后,那么在final remark的阶段需要做并行remark,
然后再做并行的remark之前,我们先去做一次minor gc,
由于我们的final remark阶段是会做重新标记的,由于年轻代的数据是一直发生变化的,如果说我们再做final remark(重新标记) 发现会有脏数据,这些脏数据又会溯洄年轻代,我们还需要遍历年轻带的gc roots所关联的递归树,如果递归树嵌套的非常深,这个final remark重新标记的stop the world 的时间又会非常的长,于是我们开启-XX:+CMSParallelRemarkEnable参数的本质是为了做final remark(重新标记) 之前我们先尝试着去做一次minor gc ,将年轻代里面的无用对象释放掉一部分.当然这个参数是否开启还是需要实际的业务场景,如果实际业务场景大部分对象都是常驻内存,被分配到了老年代上,那么开启了-XX:+CMSParallelRemarkEnable这个参数有可能效果只会更差,因为你的年轻代垃圾回收根本释放不了多少度内存空间.

G1参数调优

-XX:+UseG1GC :开启G1,注意,G1在jdk1.9之后是默认的垃圾回收器,不需要指定这个参数

-XX:MaxGcPauseMillis=n : GC最大停顿时间,软性参数,jvm会尽可能满足用户配置的这个参数

-XX:G1HeapRegionSize=n: 每个Region的大小,Region大小需要通过不断实践去找到一个最佳的Region值

调优最佳实践

1.多分析线上case,并设置不同的内存大小观察gc日志,寻找最佳策略
2.通过改善参数避免common类型的问题