美文网首页
JVM实战优化篇

JVM实战优化篇

作者: 会飞的蜗牛F | 来源:发表于2020-07-12 18:50 被阅读0次

    在前文《JVM原理解惑篇》中已经梳理了主要的JVM的理论基础:Java程序运行在Java虚拟机之上,JVM管理了整个程序的内存分配和使用,负责对象的整个生命周期。这部分内容是广大Java开发者都比较熟知的内容,在面试或者看书博客过程中多少有过印象的,但是转到项目实战中,例如核心的JVM参数有哪些,如何根据项目评估一套合理的JVM参数,在项目上线后如何根据业务优化JVM配置,碰到GC频繁该如何分析解决等等实际问题,就基本没有章法了,如果盲目上网去找JVM优化的文章大部分只说参数调优不基于背景,基本是盲人摸象无法把握住关键问题,这篇文章即是对JVM实战和优化进行一个系统化的梳理,也是近日对JVM整个学习使用的一个总结,首先上篇章大纲图:

    image

    一、JVM内存相关参数


    1.JVM内存参数的权衡

    首先,JVM最重要最核心的参数是去评估内存和分配,第一步需要指定堆内存的大小,这个是系统上线必须要做的,-Xms 初始堆大小,-Xmx 最大堆大小,后台Java服务中一般都指定为系统内存的一半,过大会占用服务器的系统资源,过小则无法发挥JVM的最佳性能。

    其次需要指定-Xmn新生代的大小,这个参数非常关键,灵活度很大,虽然sun官方推荐为3/8大小,但是要根据业务场景来定,针对于无状态或者轻状态服务(现在最常见的业务系统如Web应用)来说,一般新生代甚至可以给到堆内存的3/4大小;而对于有状态服务(常见如IM服务、网关接入层等系统)新生代可以按照默认比例1/3来设置。服务有状态,则意味着会有更多的本地缓存和会话状态信息常驻内存,应为要给老年代设置更大的空间来存放这些对象。

    最后是设置-Xss栈内存大小,设置单个线程栈大小,默认值和JDK版本、系统有关,一般默认512~1024kb。一个后台服务如果常驻线程有几百个,那么栈内存这边也会占用了几百M的大小。

    image

    2.如何设置JVM参数

    如果在IDEA中调试JVM参数,只需要打开项目的Configuration中,对VM Options进行设置即可:

    image

    如果在Linux环境中调试JVM参数,需要在启动Java进程的时候,加入到启动命令中:

    [root@LOCAL~]#java -Xmx512m -Xms512m -Xmn256m -Xss1m –jar hello.jar
    
    

    服务启动后,在日志的第一行就会打印jvm参数相关信息:可以验证启动后的jvm参数是否设置成功!

    二、新系统上线如何规划容量

    1.套路总结

    任何新的业务系统在上线以前都需要去估算服务器配置和JVM的内存参数,这个容量与资源规划并不仅仅是系统架构师的随意估算的,需要根据系统所在业务场景去估算,推断出来一个系统运行模型,评估JVM性能和GC频率等等指标。以下是我结合大牛经验以及自身实践来总结出来的一个建模步骤:

    • 计算业务系统每秒钟创建的对象会占用多大的内存空间,然后计算集群下的每个系统每秒的内存占用空间(对象创建速度)
    • 设置一个机器配置,估算新生代的空间,比较不同新生代大小之下,多久触发一次MinorGC。
    • 为了避免频繁GC,就可以重新估算需要多少机器配置,部署多少台机器,给JVM多大内存空间,新生代多大空间。
    • 根据这套配置,基本可以推算出整个系统的运行模型,每秒创建多少对象,1s以后成为垃圾,系统运行多久新生代会触发一次GC,频率多高。

    2.套路实战——以登录系统为例

    有些同学看到这些步骤还是发憷,说的好像是那么回事,一到实际项目中到底怎么做我还是不知道!光说不练假把式,这里就以登录系统为例模拟一下推演过程:

    • 假设每天100w次登陆请求,登陆峰值在早上,预估峰值时期每秒100次登陆请求。
    • 假设部署3台服务器,每台机器每秒处理30次登陆请求,假设一个登陆请求需要处理1秒钟,JVM新生代里每秒就要生成30个登陆对象,1s之后请求完毕这些对象成为了垃圾。
    • 一个登陆请求对象假设20个字段,一个对象估算500字节,30个登陆占用大约15kb,考虑到RPC和DB操作,网络通信、写库、写缓存一顿操作下来,可以扩大到20-50倍,大约1s产生几百k-1M数据。
    • 假设2C4G机器部署,分配2G堆内存,新生代则只有几百M,按照1s1M的垃圾产生速度,几百秒就会触发一次MinorGC了。
    • 假设4C8G机器部署,分配4G堆内存,新生代分配2G,如此需要几个小时才会触发一次MinorGC。

    到这里,可以粗略的推断出来一个每天100w次请求的登录系统,按照4C8G的3实例集群配置,分配4G堆内存、2G新生代的JVM,可以保障系统的一个正常负载。基本上把一个新系统的资源评估了出来,所以搭建新系统要每个实例需要多少容量多少配置,集群配置多少个实例等等这些,并不是拍拍脑袋和胸脯就可以决定的下来的。

    三、垃圾回收器的选择


    1.吞吐量还是响应时间

    首先引入两个概念:吞吐量和低延迟
    吞吐量 = CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)
    响应时间 = 平均每次的GC的耗时

    通常,吞吐优先还是响应优先这个在JVM中是一个两难之选。堆内存增大,gc一次能处理的数量变大,吞吐量大;但是gc一次的时间会变长,导致后面排队的线程等待时间变长;相反,如果堆内存小,gc一次时间短,排队等待的线程等待时间变短,延迟减少,但一次请求的数量变小(并不绝对符合)。无法同时兼顾,是吞吐优先还是响应优先,这是一个需要权衡的问题。

    2.垃圾回收器设计上的考量

    • JVM在GC时不允许一边垃圾回收,一边还创建新对象(就像不能一边打扫卫生,还在一边扔垃圾)。
    • JVM需要一段Stop the world的暂停时间,而STW会造成系统短暂停顿不能处理任何请求;
    • 新生代收集频率高,性能优先,常用复制算法;老年代频次低,空间敏感,避免复制方式。
    • 所有垃圾回收器的涉及目标都是要让GC频率更少,时间更短,减少GC对系统影响!

    3.CMS和G1

    目前主流的垃圾回收器配置是新生代采用ParNew,老年代采用CMS组合的方式,或者是完全采用G1回收器,从未来的趋势来看,G1是官方维护和更为推崇的垃圾回收器。

    image

    业务系统,延迟敏感的推荐CMS;大内存服务,要求高吞吐的,采用G1回收器!下面单独就两款回收器的工作机制和适用场景进行一下说明:

    四、CMS回收器

    1.CMS垃圾回收器的工作机制

    CMS主要是针对老年代的回收器,新生代的采用ParNew回收器,工作流程就是上文提到的经典复制算法,在三块区中进行流转回收,只不过采用多线程并行的方式加快了MinorGC速度。老年代是标记-清除,默认会在一次FullGC算法后做整理算法,清理内存碎片。

    image

    优点:并发收集、主打“低延时” 。在最耗时的两个阶段都没有发生STW,而需要STW的阶段都以很快速度完成。
    缺点:1、消耗CPU;2、浮动垃圾;3、内存碎片
    适用场景:重视服务器响应速度,要求系统停顿时间最短。

    2.登录系统的压测前配置

    调优场景以之前的登录系统为例,按照之前容量估算套路,引入性能压测环节,测试同学对登录接口压至1s内60M的对象生成速度,假设只配置了4C8G的机器配置,采用ParNew+CMS的组合回收器,堆内存分配4g,线程栈默认1M,初始配置如下:

    -Xms4g –Xmx4g –Xmn1536m -Xss1m -XX:+UseConcMarkSweepGC
    
    

    划分Eden和Surviror大小,如按照默认-XX:SurvivorRatio=8分配规则,基于CMS的JVM运行模型粗略计算如下:

    image

    基本上,可以看到20S后Eden区就满了,此时再运行的时候对象已经无法分配,会触发MinorGC,假设在这次GC后S1装入100M,马上过20S又会触发一次MinorGC,多出来的100M存活对象+S1区的100M已经无法顺利放入到S2区,此时就会触发JVM的动态年龄机制,将一批100M左右的对象推到老年代保存,持续运行一段时间,系统可能一个小时候内就会触发一次FullGC。

    3.基于CMS的调优思路

    首先采取上调Survior区容量策略:新生代划2g,维持E:S1:S2=8:1:1,此时Eden=1.6G,S=200M。60M/S速率,25s触发MinorGC,回收的垃圾超过200M才触发进入老年代,对象进入老年代的几率大大降低,短命对象在几次minorGC后就释放掉了。

    此时的JVM配置如下:

    -Xms4g –Xmx4g –Xmn2g -Xss1m -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC
    
    

    然后再下调晋升老年代年龄,默认为15——当躲过15次MinorGC后,可进入老年代;可适当调低改值为5~10,让长寿对象应尽快去往属于它的地方,而不是在新生代来回折腾,占用空间,这样可以优化每次MinorGC的耗时。

    -Xms4g –Xmx4g –Xmn2g -Xss1m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15   -XX:+UseConcMarkSweepGC
    
    

    再选择性的去优化老年代参数:比如老年代默认在标记清除以后会做整理,还可以在CMS的增加GC频次还是增加GC时长上做些取舍,如下是响应优先的参数调优:

    image

    那么最终我们可以得到一个比较适用于自身业务系统的、基于CMS回收器的JVM参数:

    -Xms4g –Xmx4g –Xmn2g -Xss1m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5   
    -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly -XX:+AlwaysPreTouch
    
    

    五、G1垃圾回收器


    1.CMS回收器的不足

    • 服务启动前就需要指定新生代和老年代大小,启动了就不能动态调整了!
    • 新生代和老年代都必须分配独立且连续的一整块内存空间!
    • 所有针对老年代的操作必须扫描整个老年代空间,相同的老年代对象,堆空间越大扫描耗时越长!

    2.G1回收器的设计思路

    G1回收天然的适用于大内存服务器,首先G1将堆内存空间拆分为多个大小相等的Region块,Region的个数默认2048个,配置4g堆内存,每个region的大小就为2M。Region动态的属于老年代或者新生代,上一秒还是分配成新生代,经过回收以后空出来,下一秒有可能被分为老年代区。

    在G1回收器这里已经不需要再提前设置新生代和老年代的大小,但是新生代仍区分Eden和Survivor区。大大降低了JVM参数的调优复杂度,只需配置-XX:MaxGCPauseMillis=n(ms),设置最大GC停顿时间,剩下的交给G1回收器。G1会自动追踪每个region可以回收的大小和预估的时间,最后在真正垃圾回收的时候,尽量把垃圾回收控制在设置的时间范围内,在有限的时间内回收更多的对象。

    image

    所以综合来看,G1主打高吞吐,特别适用多核、大内存服务(如Kafka/ElasticSearch)。

    3.G1的工作机制

    新生代回收:对象优先分配Eden的Region,JVM不停给新生代分配更多的region,直到新生代占堆总大小的60%,触发MinorGC。

    进入老年代对象的条件不变:达到晋升年龄;动态年龄判定;大对象等

    Mix混合回收:当老年代的Region占堆内存的45%以后,触发MixGC,会分阶段多次混合回收新生代和老年代的Region。

    Full GC:MixGC时发现无可用的新Region块了来分配复制的存活对象,立马触发FullGC,停止系统程序,单线程标记、清除和整理,空闲出一批Region,过程很缓慢。

    image

    4.G1的核心调优参数

    G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,即调整-XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单!同时也不要自己显式设置新生代的大小(用-Xmn或-XX:NewRatio参数),如果人为干预新生代的大小,会导致目标时间这个参数失效。

    image

    针对-XX:MaxGCPauseMillis来说,参数的设置带有明显的倾向性:
    调低↓:延迟更低,但MinorGC频繁,MixGC回收老年代区减少,增大Full GC的风险。
    调高↑:单次回收更多的对象,但系统整体响应时间也会被拉长。

    针对InitiatingHeapOccupancyPercent来说,调参大小的效果也不一样:
    调低↓: 更早触发MixGC,浪费cpu。
    调高↑:堆积过多代回收region,增大FullGC的风险。

    5.G1调优在Kafka集群的应用

    比如日志平台的Kafka集群每秒写入300M数据至内存,broker节点的配置为16C32G,假设堆内存给16g,新生代分配8g,每秒产生对象假设100M左右,差不多一分多钟就会产生一次MinorGC,CMS机制下需要等Eden满了以后,才一次性清理大约8g左右的垃圾对象,差不多会有秒级的STW停顿,如果是老年代的GC延时长则会有十秒级的STW停顿。

    -Xms16g –Xmx16g –Xmn8g -Xss1m -XX:+UseConcMarkSweepGC
    
    

    假设采用了G1回收器,适当调低最大耗时,设定MaxGCPauseMillis为100ms,并且适当调低堆使用率阈值,G1就会在允许的响应时间内自动的、多批次的去进行垃圾回收,保证每个STW的时间都不会太长。

    -Xms16g -Xmx16g -Xss1m -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=40
    
    

    所以线上的kafka和ES集群,动辄32~64g的大内存,如果让CMS去整块回收十多G乃至几十G的垃圾对象,对于系统而言绝对不利!一般来说,堆内存超过8g的大内存服务器,都更推荐使用G1回收器!

    六、调优总结


    1.系统在上线前的综合调优思路

    • 业务预估:根据预期的并发量、平均每个任务的内存需求大小,然后评估需要几台机器来承载,每台机器需要什么样的配置。
    • 容量预估:根据系统的任务处理速度,然后合理分配Eden、Surivior区大小,老年代的内存大小。
    • 回收器选型:响应优先的系统,建议采用ParNew+CMS回收器;吞吐优先、多核大内存(heap size≥8G)服务,建议采用G1回收器。
    • 优化思路:让短命对象在MinorGC阶段就被回收(同时回收后的存活对象<Survivor区域50%,可控制保留在新生代),长命对象尽早进入老年代,不要在新生代来回复制;尽量减少Full GC的频率,避免FGC系统的影响。

    到目前为止,总结到的调优的过程主要基于上线前的测试验证阶段,所以我们尽量在上线之前,就将机器的JVM参数设置到最优!

    2.一份通用的JVM参数模板

    一般来说,大企业或者架构师团队,都会为项目的业务系统定制一份较为通用的JVM参数模板,但是许多小企业和团队可能就疏于这一块的设计,如果老板某一天突然让你负责定制一个新系统的JVM参数,你上网去搜大量的JVM调优文章或博客,结果发现都是零零散散的、不成体系的JVM参数讲解,根本下不了手,这个时候你就需要一份较为通用的JVM参数模板了,不能保证性能最佳,但是至少能让JVM这一层是稳定可控的,我在这里给大家总结了两份模板:

    基于4C8G系统的ParNew+CMS回收器模板(响应优先),新生代大小根据业务灵活调整!

    -Xms4g
    -Xmx4g
    -Xmn2g
    -Xss1m
    -XX:SurvivorRatio=8
    -XX:MaxTenuringThreshold=10
    -XX:+UseConcMarkSweepGC
    -XX:CMSInitiatingOccupancyFraction=70
    -XX:+UseCMSInitiatingOccupancyOnly
    -XX:+AlwaysPreTouch
    -XX:+HeapDumpOnOutOfMemoryError
    -verbose:gc
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps
    -Xloggc:gc.log
    
    

    基于8C16G系统的G1回收器模板(吞吐优先):

    -Xms8g
    -Xmx8g
    -Xss1m
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=150
    -XX:InitiatingHeapOccupancyPercent=40
    -XX:+HeapDumpOnOutOfMemoryError
    -verbose:gc
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps
    -Xloggc:gc.log
    
    

    以上两份模板参数,都额外增加了GC日志打印、OOM自动dump等配置内容。

    这篇文章的最后,再多说几句啊,虽然本文主题是JVM实战与调优,但需知JVM调优只是一个手段,但并不一定所有问题都可以通过JVM进行调优解决,大多数的Java应用不需要进行JVM优化,我们可以遵循以下的一些原则:

    • 上线之前,应先考虑将机器的JVM参数设置到最优;
    • 减少创建对象的数量(代码层面);
    • 减少使用全局变量和大对象(代码层面);
    • 优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
    • 分析GC情况优化代码比优化JVM参数更好(代码层面);

    通过以上原则,我们发现,其实最有效的优化手段是架构和代码层面的优化,而JVM优化则是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。

    转载;https://www.jianshu.com/p/63853a33e4c0

    相关文章

      网友评论

          本文标题:JVM实战优化篇

          本文链接:https://www.haomeiwen.com/subject/zklrzhtx.html