不知道大家有没有听过扁鹊三兄弟的故事,故事是这样的:魏文王问扁鹊:你们三兄弟都精通医术,谁是医术最好的呢?扁鹊回答:大哥最好,二哥次之,我最差。魏文王不解的问:为什么这样说呢?扁鹊答:大哥治病是在病人发作之前,那时候病人自己不觉得有病,但大哥就下药铲除了病根,使他的医术难以被人认可,所以没有名气;二哥治病是在病起之初,症状尚不十分明显,病人也没有觉得痛苦,二哥就能药到病除,所以大家的印象就是小病找二哥;我治病是在病人危急时刻,病人痛苦万分,家人心急如焚,他们看到我治病时在经脉上扎针穿刺,或以毒试毒,或动大手续使病人减轻痛苦直至痊愈,所以我闻名天下。魏文王大悟。
为什么要讲这个故事呢?
因为JVM调优跟这个故事很像。JVM调优也有这三个阶段:
1、在项目部署到线上之前,基于可能的并发量进行预估调优
2、在项目运行过程中,部署监控收集性能数据,平时分析日志进行调优
3、线上出现OOM,进行问题排查与调优
这里每一点展开来讲涉及到的知识点都比较多,打算用三篇文章讲清楚每一阶段的调优,本篇文章讲第一阶段,主要讨论堆区调优。
JVM调优
很多同学可能不太理解:为什么要做JVM调优?
一、防止出现OOM
即在系统部署之前,根据一些关键数据进行预估不同内存区域需要给多少内存合适
二、解决OOM
即线上出现了OOM,应该如何调优以保证程序能正常运行
二、减少full gc出现的频率
这个主要是堆区,如果设置的不合理就会频繁full gc,导致系统运行一阵暂停一阵,导致体验下降
场景
这里以亿级流量秒杀电商系统为例:
如果每个用户平均访问20个商品详情页,那访客数约等于500w(一亿 / 20)
如果按转化率10%来算,那日均订单约等于50w(500w * 10%)
如果40%的订单是在秒杀前两分钟完成的,那么每秒产生1200笔订单(50w * 30% / 120s)
订单支付又涉及到发起支付流程、物流、优惠券、推荐、积分等环节,导致产生大量对象,这里我们假设整个支付流程生成的对象约等于20K,那每秒在Eden区生成的对象约等于20M(1200笔 * 20K)
在生产环境中,订单模块还涉及到百万商家查询订单、改价、包邮、发货等其他操作,又会产生大量对象,我们放大10倍,即每秒在Eden区生成的对象约等于200M(其实这里就是在大并发场景下可以考虑服务降级的地方,架构其实就是取舍)
这里的假设数据都是大部分电商系统的通用概率,是有一定代表性的。
如果你作为这个系统的架构师,面对这样的场景,你会如何做JVM调优呢?即将运行该系统的JVM堆区设置成多大呢?
前置知识
那做JVM调优需要什么的基础呢?
1、了解Java中基本数据类型的字节长度
public class TestLength { public static void main(String[] args) { System.out.println("Byte.SIZE=" + Byte.SIZE / 8); System.out.println("Character.SIZE=" + Character.SIZE / 8); System.out.println("Short.SIZE=" + Short.SIZE / 8); System.out.println("Integer.SIZE=" + Integer.SIZE / 8); System.out.println("Long.SIZE=" + Long.SIZE / 8); System.out.println("Float.SIZE=" + Float.SIZE / 8); System.out.println("Double.SIZE=" + Double.SIZE / 8); }}
2、深刻理解如何计算对象大小及指针压缩
3、深刻理解JVM内存模型
计算对象大小以及指针压缩,我之前已经写过一篇文章,传送门
看文章毕竟显得枯燥,这里面的很多知识点也不好理解。幸运的是,这两个知识点我在公开课上都有讲过,需要视频的同学可以加小姐姐微信:xyhh620(备注:启明南)
4、动态对象年龄判断
JVM并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
垃圾判断算法
gc过程中需要知道哪些对象能回收,哪些对象不能回收,那如何判断呢?就是靠这个算法。这里我就不展开讲了,因为学JVM实战的同学我相信对这两种算法都很了解了。如果不了解的同学可以百度搜点文章看看,这块的文章搜出来的基本还算靠谱。
1、引用计数法
2、可达性分析算法(GC Root)
如何调优
这里我们以内存32G服务器来计算。JVM的堆区最大值默认是物理内存的四分之一,即8G。新生代、老年代的比例默认是1:2,即新生代约等于2.7G,老年代约等于5.4G。新生代中Eden区、From区、To区的比例是8:1:1,即Eden区约等于2.2G,From区、To区各占约270M。
前面我们计算了亿级流量并发系统在高峰期时Eden区每秒产生大约200M的对象,如果在部署系统时未对JVM做任何调优,那么系统运行11s左右(2200M),Eden区就会被充满,就会产生young gc。一般来说整个付款环节3s完成算快的,我们这里就按3s来计算,那么在产生young gc时,Eden区有约600M的对象无法回收,因为600M已超过To区的大小,会触发空间担保机制,这600M的对象会直接被移入老年代。按照这个节奏,程序运行一分半种多点就会产生full gc,引起STW,这在付款环节是不可接受的,所以我们需要做调优。
那如何做调优呢?我们需要反向来推理:为了保证不触发空间担保机制,From区、To区都设置成600M,那个新生代就是6G、老年代就是12G。这样一个JVM就吃掉了18G的内存,所以还需要做其他事情:将这个服务器上其他比较吃内存的服务移走,以保证其他服务的正常运行。这里你为了保险起见,你可以设置得更大,具体调优的数值各位童鞋视实际情况而定吧。
这里有个注意点:看网上有些老师的视频或文章,他们调优的时候将老年代设置的大于新生代,我觉得这个方案非常危险。道理同学们可以自己想想。我不建议同学们这样做。
这篇文章就把预估调优堆区这件事情讲清楚了,下篇咱们再将线上产生OOM调优讲清楚。
恭喜你,对JVM的预估调优GET到了正确的姿势。意犹未尽,还想看子牙老师后续的文章?那就关注一波吧。