前言

通过上篇文章小白都能看得懂的java虚拟机内存模型我们已经基本了解了jvm的内存模型,并且通过一个jvm案例分析了程序运行中JVM的内存活动。

我们学习jvm是为了什么,为了面试么,当然是一部分,但是真正学习jvm的底层原理,一定是为了调优。

那么jvm调优到底是调什么呢,目的是什么呢?

Stop-The-World

       不知道同学们有没有听过SWT,意思是Stop-The-World,停止这个世界,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

       我们先说说为什么gc的时候需要Stop-The-World,我们上节提到过,垃圾回收是根据可达性分析算法,搜索GC Root根的引用链,将不在引用链上的对象当做垃圾回收,设想我们执行某个方法的时候,此时产生了很多局部变量,刚好老年代满了需要进行Full gc,如果不停止线程,垃圾回收正在根据这些局部变量也就是GC Root根搜索引用链,此时这个方法结束了,那么这些局部变量就都会被销毁,那么我们之前做的引用链搜索是不是白做了呢,这些引用链的GC Root根都销毁了,这些引用当然也成了垃圾对象,这样就会导致在垃圾回收的过程中还会不断的产生新的垃圾!

       但是Stop-The-World的结果是比较严重的,如果用户正在浏览你的网站,应用程序突然Stop-The-World,所有线程被挂起,那么用户就会感觉你的网站卡住了,尽管gc时间是比较快的,但是如果并发量比较大,用户感知是比较明显的,会影响用户体验。

       那么说到这里,我们jvm调优是为了什么?就是为了调优gc,主要就是两个点,一个是减少Full gc的次数,一个是缩短一次Full gc的时间。

案例分析

下面我们通过一个简单案例来实践一下如何进行jvm调优。

双十一刚过,再没有比电商并发量大的项目了吧,我们今天就讲一个电商案例吧。

       对于jvm的参数大家可能很多人还没接触过,就算接触了很多人也是凭随便分配一下,不就是给堆分配个2G或者3G的内存么?他也不知道为什么要给堆分配2G或者3G内存,是怎么得到这个结果的呢?有没有一个明确的依据,然后根据这个依据去设置参数来启动应用程序呢?

       在比较大的互联网公司,在上线之前,一般多会对这个系统进行一个预估,然后根据这个预估的结果,来对java虚拟机的一些参数进行调优。

       像京东、淘宝、唯品会这些比较大的电商平台,都属于亿级流量电商,什么是亿级流量电商呢,就是用户在网站上每日的点击是上亿的。

这些电商网站后台现在基本都是使用微服务架构搭建的,我们这里就交易系统来分析一下。

20191127175354310.png

大家在购买商品的时候,肯定会进行挑选,会有很多次的点击,我们这里假设每个用户平均点击二三十次,那么就可以推断出日活跃用户大概有500万。

20191203095145611.png

用户是有500万,但是像我就喜欢逛,不喜欢买,所以是有一个付费转化率的,也就是这些活跃用户中下单用户的占比,我们这里假设十个人中有一个人会买,也就是10%的付费转化率,那么一天就有50万的订单。

好,下面我们看一下日均50万订单的系统后台的压力是怎么样的呢?

20191203100628385.png

在平时,这些订单可能是在一天的几个时间段内陆续产生的,比如早上一个小时,晚上两个小时,也就是正常在一天中三四个小时内产生,那么平均下来也就是每秒几十单的样子。对于一般的网站,每秒几十单的并发压力是很小的,基本没什么jvm调优的必要。

       但是很多电商网站经常会搞一些活动,比如双十一,双十二,促销等等,那么就会在活动开始后的几分钟,十几分钟内有大规模 的流量来访问网站,在十几分钟内产生几十万的订单,这时我们假设50万的订单在十分钟内产生,那么每秒要产生将近1000多单的订单。

20191203102633950.png

前面说过,一个电商系统基本都是微服务架构搭建,有很多个系统,那么每个系统基本也是要做集群部署的,也就是每个系统至少都有好几台机器,一台机器一秒内要承受1000多单肯定是不行的,我们这里假设订单系统有三台机器。那么每台机器就是300多单每秒。

下面正片开始,我们来更加深入的分析一下。

一个订单,一个下单的过程,说白了就是主要产生一个订单Order对象,那么一个订单对象有多大呢?

20191203111638287.png

在小白都能看得懂的java虚拟机内存模型我们曾简单介绍了一下对象在内存中的组成,有对象头,实例数据以及对齐填充,其中主要的大小时实例数据占用的,一个对象有很多字段,一个字段占几个字节,一个对象撑死几百个字段,我们这里假设一个订单对象大小1KB,就是1024个字节,这已经算比较大的,一般对象不会超过这么大,

       那么接着上面的推算,每秒就会有300KB的订单对象生成,但是我们这里只是估算,下单过程中不仅仅会产生订单对象 ,还会涉及库存,优惠券,积分等其他相关对象,既然是估算,我们肯定要放大一下才可靠。

       假设我们这里放大20倍,那么每秒就会有300KB*20也就是6M的对象产生。但是订单系统也不只提供下单操作,可能还有订单查询,订单退款等操作,我们再把这业务放大十倍,那么每秒就会产生60M的对象放入伊甸园区,而且这些对象有一个特点,当我的订单对象一旦生成完毕之后,在堆中生成的对象都会变为垃圾对象,都应该被回收掉。

问题

       在工作中项目上线时,都会对这个项目进行一些jvm参数的设置,但是大多数都是随便凭感觉,比如根据机器大小给堆设置个两三G的内存。

我们这里假设机器是4核8G,那么一般可能会给我们虚拟机分配个四五G的内存,就会给堆分配个3G的内存

2019120314224243.png

那么方法区分配256M,单个栈内存分配1M。

   那么这么分配有什么问题呢?如果是一个并发量不大的系统,基本上也不会有什么问题,因为本身也没有多少对象在堆里生成。但是如果是我们上面说的亿级流量系统,就不能简简单单这么设置了,我们来分析一下:

2019120314594788.png

根据之前我们讲的jvm内存分配,我们可以得知各个区域的所占内存,每秒有60M对象产生进去Eden区,那么13秒就会占满Eden区域,发生minor gc,这些订单对象其中90%其实已经是垃圾对象了,为什么说90%,因为在gc的那一刻,这些对象肯定还有一部分的业务操作还没有完成,所以他们不会被回收,我们这里假设每次minor gc都有60M对象还不会被回收。

       那么这60M对象会从Eden区移到S0区域,我们之前的文章有说过对象进入老年代有很多条件,比如较大的对象,这里的60M虽然小于100M,同样会直接被移入老年代,为什么呢?因为还有一个条件是对象动态年龄判断,就是如果你移动的这批对象超过了Surviror区的50%,同样会把这批对象移入老年代。

       那么按现在这种参数设置,每13秒就会有60M的对象被移入老年代,那么大概五六分钟老年代的2G就会被占满,那么老年代一满就要发生full gc,但是full gc使我们最不愿意看到的结果。对于这么大的一个系统,每过五六分钟就发生一次full gc,就会让系统卡顿一次,用户体验很差,这是很大的问题。

调优

那么我们该怎么去调优呢?

这也是面试中可能会被问到的问题:能否对JVM调优,让其几乎不发生full gc?

我们先来分析一下:我们这个系统其实没有很多会长久存在的对象,也就是老不死的对象,我们放在老年代的那些60M的对象,在一两秒后其实都会变为垃圾对象,在下一次full gc时都会被回收掉,那么我们这种业务场景,完全没必要给老年代设置2G的内存,根本用不到。

我们完全可以把年轻代设置的大一些,我们现在来对jvm参数进行一些更改。

20191203164816320.png

年轻代如果不设置,堆中各个区域默认占比是2:1(8:1:1), 现在我们设置了之后,那么堆中各区域占比就会发生变化

20191203165646965.png

当我们进行参数修改之后,你再来按上面的分析来分析这个过程,你会发现一个奇迹效果。

此时需要25秒把伊甸园区放满,放满minor gc后有60M对象不被回收,要移到S0区,这时60M<200M/2,是可以移入S0区域的,下一次伊甸园区再放满做minor gc的时候,这时这60M对象所对应的订单已经生成了,已经变成了垃圾对象,是可以直接被回收的,所以没有什么对象是需要被移入老年代的。

那么这么一设置的话,这个系统是不是正常情况下基本不会再发生full gc了呢?就算发生,也是很久才会一次了。

总结

1.分析核心主线业务

2.估算涉及对象大小

3.画出对象内存模型

4.对应JVM参数调优

版权声明:文章转载请注明来源,如有侵权请联系博主删除!
最后修改:2019 年 12 月 26 日 05 : 06 PM
如果觉得我的文章对你有用,请随意赞赏