jvm方面的优化往往是系统优化的最后一步,万不得已时才会基于JVM去优化系统,在对mqtt系统进行优化时,也是仅仅对jvm层面进行了少量的修改。下面总结记录一下
设置合适的堆的大小:主要涉及三个参数:
-Xms:设置JVM的初始化内存的大小,JVM启动时就会分配这么大的内存
-Xmx:设置JVM最大可用内存的大小,JVM在运行期间如果内存超过-Xms设置的大小会自动扩容,如果超过了-Xmx的大小就会抛出OOM异常。
-Xmn:新生代大小,堆按代分有:新生代,老年代,持久带(方法区),持久带主要存放静态数据,类加载数据等,大小一般固定为64M,所以增大新生代大小就会减小老年代大小,发生在新生代的gc成为minor GC,而发生在老年代的通常称为major GC或full GC,而minor GC相对于full GC,性能消耗要低很多,所以尽量减少full GC的次数,那么该参数影响gc也就比较大了,一般推荐配置新生代大小为整个堆大小的3/8。
当然还有一个配置线程栈大小的配置:-Xss,该参数是设置每个线程堆栈的大小,jdk8默认为1024k,在linux系统中,对于线程栈的大小的分配是固定的,所以减小-Xss的参数,可以增加线程数。
注意:如果用到了直接内存,那么分配JVM大小的时候,JVM大小+直接内存大小不要超过了主机最大物理内存,配置JVM初始化内存和JVM最大可用内存最好相等,这样JVM的堆内存就不会再自动扩展, 这个动作也是会消耗性能的。
首先看下垃圾回收算法主要有:
标记-清除:效率不高,会产生大量的不连续的内存碎片
复制算法:会将内存分为两块,然后将存活的对象从这一块复制到另一块,所以相对来说对内存利用不高,对于对象存活率高的话,该算法效率会变低
标记-整理:与标记-清除第一阶段标记相同,但是第二阶段是将存活的对象向一端移动,然后清理掉边界以外的内存。
分带收集算法:就是根据JVM中对象的存活周期的不同将内存分为几块,一般分为新生代和老年代,持久带一般不会进行GC,对于新生代,对象一般“朝生夕死”,所以新生代一般采用复制算法,对于老年代,对象存活率一般较高,所以一般采用标记清除或标记整理算法。
总结了下垃圾收集算法的特点,下面看下我们一般使用的垃圾收集器(按新生代和老年代来分):
对于新生代:
Serial收集器:串行收集器,新生代采用复制算法。
ParNew收集器,并行收集器,新生代采用复制算法。
Parallel Scavenge收集器,并行收集齐,新生代采用采用复制算法。
对于老年代:
Serial Old收集器,串行收集器,老年代采用标记-整理算法。
Parallel Old收集器,Parallel Scavenge的老年代版本,采用标记-整理算法。
当然还有CMS收集器:以获取最短回收停顿时间为目标的收集器,G1收集器:在jdk8以后一般都使用该收集器。
在使用时,在jdk7之前,对于并发高,对吞吐量有要求的一般使用CMS收集器,jdk8后推荐使用G1收集器。因为CMS收集器的老年代是采用的标记-清除算法,所以要设置UseCMSCompactAtFullCollection,即在完成垃圾收集后是否进行内存碎片整理。对于消息中间件这种要求高并发,高吞吐量的系统,一般就使用CMS或G1收集器
对于垃圾收集而言,发生在新生代的minor GC远远比full GC效率高的多,我们希望尽量减少Full GC的次数,除了在开发时候注意以外,我们还可以动态对JVM进行一些配置来调整,例如:
MaxTenuringThreshold:经过多少次minorGC,对象才会进入老年代,默认是15,可以动态配置
UseAdaptiveSizePolicy:自动选择年轻代区大小和相应的Survivor区比例,在使用并行收集器时可以使用
PretenureSizeThreshold:对象超过多大是直接在旧生代分配,默认为0,总是会先分配在新生代,因为新生代采用复制算法,所以内存占用相对比较紧张,如果JVM发现不能再分配内存时候就会提前进行一次minorGC,所以如果有大对象的话,设置该值可以减少minorGC的次数
对于开发时,例如可以用内存池,重用对象,来减少gc的次数,例如spring的控制反转,单例的作用域,又比如netty的内存池(Netty4以后默认使用了内存池,这也是我们为什么每次都要手动释放buffer对象的原因),也可以使用直接内存来减少gc的次数,ps:直接内存的回收不受gc控制,在jvm分配时,堆中的对象实际上可以看成只是一个指向直接内存的指针,在使用直接内存时,一定要注意在使用完后一定要释放,否则可能导致堆内存还有很多空闲,然后直接内存已经占满的情况(堆中回收了该指针,却没有释放直接内存的数据)。
在系统运行时,最好打印出运行时的堆栈情况,特别是异常时需要打印出来,这样方便系统出现异常宕机时进行分析,打印GC堆栈信息:
-verbose:gc -Xloggc:/dev/shm/mq_gc_%p.log
其它参数参考下面的资料即可。
这里只总结了在进行JVM调优方面,我用到的一些知识,在mqtt项目优化实践上对于tps和连接数有一定的提升,个人经验有限,这方面还要继续总结和学习,当然还有一些参数可能对于高并发系统非常相关,例如:UseBiasedLocking,该参数是使用偏向锁,jdk6以后自动启用,所以在高并发而锁竞争激烈下禁用偏向锁比较好,如果在分配JVM内存时,分配的初始化内存和最大内存一样大,那么要开启AlwaysPreTouch,这是因为JAVA进程启动的时候,虽然我们可以为JVM指定合适的内存大小,但是这些内存操作系统并没有真正的分配给JVM,而是等JVM访问这些内存的时候,才真正分配。在进行JVM调优时,需要进行大量的测试才能找到较优的方案,还需要借助一些工具进行分析。
开发环境推荐:jvisualvm,jconsole可以进行可视化的分析。
对于测试和生产环境,推荐使用jdk自带的:jstat,jstack等命令进行观察,利用阿里开源的tprofiler也可以很方便在测试环境中进行性能的观察(不过只支持jdk8以下)。
参考:
https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html