扫描下方海报二维码,试听课程:
面试
(课程详细大纲,请参见文末)数组
============================================缓存
本文来源于专栏:《从零开始带你成为JVM实战高手》
并发
是做者救火队队长开放的试读文章jvm
案例背景引入分布式
特殊的电商大促场景高并发
抗住大促的瞬时压力须要几台机器?性能
大促高峰期订单系统的内存使用模型估算优化
内存到底该如何分配?操作系统
新生代垃圾回收优化之一:Survivor空间够不够
新生代对象躲过多少次垃圾回收后进入老年代?
多大的对象直接进入老年代?
别忘了指定垃圾回收器
今日思考题
按照惯例,咱们接下来会用案例驱动来带着你们分析到底该如何在特定场景下,预估系统的内存使用模型。
而后合理优化新生代、老年代、Eden和Survivor各个区域的内存大小。
接着再尽可能优化参数避免新生代的对象进入老年代,尽可能让对象留在新生代里被回收掉。
咱们这里的背景是电商系统,电商系统其实通常会拆分为不少的子系统独立部署
好比商品系统、订单系统、促销系统、库存系统、仓储系统、会员系统,等等
咱们这里就以比较核心的订单系统做为例子来讲明。
(提示:食用本案例以前,请务必充分理解专栏以前两周的文章!)
咱们的案例背景是每日上亿请求量的电商系统,那么你们能够来推算一下每日上亿请求量的电商系统,他会每日有多少活跃用户?
通常按每一个用户平均访问20次来计算,那么上亿请求量,大体须要有500万日活用户。
那么继续来推算一下,这500万的日活用户都是会进来进行大量的浏览,那么多少人会下订单?
这里能够按照10%的付费转化率来计算,天天大概有50万人会下订单,那么大体就是天天会有50万订单。
这50万订单算他集中在天天4小时的高峰期内,那么其实平均下来每秒钟大概也就几十个订单,你们是否是以为根本没啥可说的?
由于几十个订单的压力下,根本就不须要对JVM多关注,基本上就是每秒钟占用一些新生代内存,隔好久新生代才会满。而后一次Minor GC后垃圾对象清理掉,内存就空出来了,几乎无压力。
可是若是你要是考虑到特殊的电商大促场景,就不会这么想了
由于不少中小型的电商平台,确实平时系统压力其实没那么大,也没太大的高并发,每秒几千并发压力就算是高峰压力了。
可是若是遇到一些大促场景,好比双11什么的,状况就不一样了。
假设在相似双11的节日里,零点的时候,不少人等着大促开始就要剁手购物,这个时候,可能在大促开始的短短10分钟内,瞬间就会有50万订单。
那么此时每秒就会有接近1000的下单请求,咱们就针对这种大促场景来对订单系统的内存使用模型分析一下。
那么要抗住大促期间的瞬时下单压力,订单系统须要部署几台机器呢?
基本上能够按3台来算,就是每台机器每秒须要抗300个下单请求。这个也是很是合理的,并且须要假设订单系统部署的就是最普通的标配4核8G机器。
从机器自己的CPU资源和内存资源角度,抗住每秒300个下单请求是没问题的。
可是问题就在于须要对JVM有限的内存资源进行合理的分配和优化,包括对垃圾回收进行合理的优化,让JVM的GC次数尽量最少,并且尽可能避免Full GC,这样能够尽量减小JVM的GC对高峰期的系统新更难的影响。
背景已经所有说完了,接下来我们就得来预估订单系统的内存使用模型了.
基本上能够按照每秒钟处理300个下单请求来估算,其实不管是订单处理性能仍是并发状况,都跟生产很接近
由于处理下单请求是比较耗时的,涉及不少接口的调用,基本上每秒处理100~300个下单请求是差很少的。
那么每一个订单我们就按1kb的大小来估算,单单是300个订单就会有300kb的内存开销
而后算上订单对象连带的订单条目对象、库存、促销、优惠券等等一系列的其余业务对象,通常须要对单个对象开销放大10倍~20倍。
此外,除了下单以外,这个订单系统还会有不少订单相关的其余操做,好比订单查询之类的,因此连带算起来,能够往大了估算,再扩大10倍的量。
那么每秒钟会有大概300kb * 20 * 10 = 60mb的内存开销。
可是一秒事后,能够认为这60mb的对象就是垃圾了,由于300个订单处理完了,全部相关对象都失去了引用,能够回收的状态。
你们看下图:
假设咱们有4核8G的机器,那么给JVM的内存通常会到4G,剩下几个G会留点空余给操做系统之类的来使用
不要想着把机器内存一会儿都耗尽,其中堆内存咱们能够给3G,新生代咱们能够给到1.5G,老年代也是1.5G。
而后每一个线程的Java虚拟机栈有1M,那么JVM里若是有几百个线程大概会有几百M
而后再给永久代256M内存,基本上这4G内存就差很少了。
同时还要记得设置一些必要的参数,好比说打开“-XX:HandlePromotionFailure”选项(不熟悉这个参数的,能够回头复习一下专栏以前的文章)
JVM参数以下所示:
“-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:HandlePromotionFailure”
可是“-XX:HandlePromotionFailure”参数在JDK 1.6之后就被废弃了,因此如今通常都不会在生产环境里设置这个参数了。
在JDK 1.6之后,只要判断“老年代可用空间”> “新生代对象总和”,或者“老年代可用空间”> “历次Minor GC升入老年代对象的平均大小”
上述两个条件知足一个,就能够直接进行Minor GC,不须要提早触发Full GC了。
因此实际上,若是你们用的是JDK 1.7或者JDK 1.8,那么JVM参数就保持以下便可,后面也都再也不加入这个参数了:
“-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M”
此时JVM内存入下图所示。
接着就很明确了,订单系统的系统程序在大促期间不停的运行,每秒处理300个订单,都会占据新生代60MB的内存空间
可是1秒事后这60MB对象都会变成垃圾,那么新生代1.5G的内存空间大概须要25秒就会占满,以下图。
25秒事后就会要进行Minor GC了,此时由于有“-XX:HandlePromotionFailure”选项,因此你能够认为须要进行的检查,主要就是比较 “老年代可用空间大小”和“历次Minor GC后进入老年代对象的平均大小”,刚开始确定这个检查是能够经过的。
因此Minor GC直接运行,一会儿能够回收掉99%的新生代对象,由于除了最近一秒的订单请求还在处理,大部分订单早就处理完了,因此此时可能存活对象就100MB左右。
可是这里问题来了,若是“-XX:SurvivorRatio”参数默认值为8,那么此时新生代里Eden区大概占据了1.2GB内存,每一个Survivor区是150MB的内存,以下图。
因此Eden区1.2GB满了就要进行Minor GC了,所以大概只须要20秒,就会把Eden区塞满,就要进行Minor GC了。
而后GC后存活对象在100MB左右,会放入S1区域内。以下图。
而后再次运行20秒,把Eden区占满,再次垃圾回收Eden和S1中的对象,存活对象可能仍是在100MB左右会进入S2区,以下图。
此时JVM参数以下:
“-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8”
首先在进行JVM优化的时候,第一个要考虑的问题,就是你经过估算,你的新生代的Survivor区到底够不够?
按照上述逻辑,首先每次新生代垃圾回收在100MB左右,有可能会突破150MB,那么岂不是常常会出现Minor GC事后的对象没法放入Survivor中?而后岂不是频繁会让对象进入老年代?
还有,即便Minor GC后的对象少于150MB,可是即便是100MB的对象进入Survivor区,由于这是一批同龄对象,直接超过了Survivor区空间的50%,此时也可能会致使对象进入老年代。
(关于jvm的垃圾回收规则,若是不太清楚,请参加专栏以前的文章)
因此其实按照咱们这个模型来讲,Survivor区域是明显不足的。
这里其实建议的是调整新生代和老年代的大小,由于这种普通业务系统,明显大部分对象都是短生存周期的,根本不该该频繁进入老年代,也不必给老年代维持过大的内存空间,首先得先让对象尽可能留在新生代里。
因此此时能够考虑把新生代调整为2G,老年代为1G,那么此时Eden为1.6G,每一个Survivor为200MB,以下图。
这个时候,Survivor区域变大,就大大下降了新生代GC事后存活对象在Survivor里放不下的问题,或者是同龄对象超过Survivor 50%的问题。
这样就大大下降了新生代对象进入老年代的几率。
此时JVM的参数以下:
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8”
其实对任何系统,首先相似上文的内存使用模型预估以及合理的分配内存,尽可能让每次Minor GC后的对象都留在Survivor里,不要进入老年代,这是你首先要进行优化的一个地方。
你们都知道,除了Minor GC后对象没法放入Survivor会致使一批对象进入老年代以外,还有就是有些对象连续躲过15次垃圾回收后会自动升入老年代。
其实按照上述内存运行模型,基本上20多秒触发一次Minor GC,那么若是按照“-XX:MaxTenuringThreshold”参数的默认值15次来讲,你要是连续躲过15次GC,就是一个对象在新生代停留超过了几分钟了,此时他进入老年代也是应该的。
有些博客会说,应该提升这个参数,好比增长到20次,或者30次,其实那种说法根本是不对的
由于你对这个参数考虑必须结合系统的运行模型来讲,若是躲过15次GC都几分钟了,一个对象几分钟都不能被回收,说明确定是系统里相似用@Service、@Controller之类的注解标注的那种须要长期存活的核心业务逻辑组件。
那么他就应该进入老年代,况且这种对象通常不多,一个系统累计起来最多也就几十MB而已。
因此你说你提升“-XX:MaxTenuringThreshold”参数的值,有啥用呢?让这些对象在新生代里多停留几分钟?
所以考虑问题,必定不要人云亦云,要结合运行原理,本身推演和思考,不一样的业务系统还都是不同的。
其实这个参数甚至你均可以下降他的值,好比下降到5次,也就是说一个对象若是躲过5次Minor GC,在新生代里停留超过1分钟了,尽快就让他进入老年代,别在新生代里占着内存了。
总之,对于这个参数务必是结合你的系统具体运行的模型来考虑。
要记住,JVM没有万能的最佳参数,可是有一套通用的分析和优化的方法。
此时JVM参数以下:
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5”
另外有一个逻辑是说,大对象能够直接进入老年代 ,由于大对象说明是要长期存活和使用的
好比在JVM里可能会缓存一些数据,这个通常能够结合本身系统中到底有没有建立大对象来决定。
可是通常来讲,给他设置个1MB足以,由于通常不多有超过1MB的大对象。若是有,多是你提早分配了一个大数组、大List之类的东西用来放缓存的数据。
此时JVM参数以下:
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M”
同时你们别忘了要指定垃圾回收器,新生代使用ParNew,老年代使用CMS,以下JVM参数 :
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC”
ParNew垃圾回收器的核心参数,其实就是配套的新生代内存大小、Eden和Survivor的比例
只要你设置合理,避免Minor GC后对象放不下Survivor进入老年代,或者是动态年龄断定以后进入老年代,给新生代里的Survivor充足的空间,那么Minor GC通常就没什么问题。
而后根据你的系统运行模型,合理设置“-XX:MaxTenuringThreshold”,让那些长期存活的对象,抓紧尽快进入老年代,别在新生代里一直待着。
这样基本上一个初步的优化好的JVM参数就结合你的业务出来了。明天咱们继续结合案例来分析 老年代的垃圾回收和参数优化方式。
你们看完这个案例,能够直接去看看本身生产系统的JVM参数了,看看你的新生代、老年代、Eden和Survivor的大小
而后去估算一下你的系统运行模型:
每秒占用多少内存?
多长时间触发一次Minor GC?
通常Minor GC后有多少存活对象?
Survivor能放的下吗?
会不会频繁由于Survivor放不下致使对象进入老年代?
会不会因动态年龄判断规则进入老年代?
请你们把本身的思考发至讨论区进行互动讨论,在明天的文章中,我也会进行相应的点评。
END
《21天互联网Java进阶面试训练营(分布式篇)》详细目录,扫描图片末尾的二维码,试听课程