前言
G1收集器是一个不太好调优的收集器,因为他不能像固定分代的收集器那样可以自己想划分多少就划分多少,更多的分配动作是由收集器动作,由于region是一块块的,同时自动增长也是由G1控制,好像确实不太好调优。
这篇文章更多的是提供调优的一个大致方向,更多的内容需要后续介绍JVM的工具进行讲解。
案例实战
这次使用一个在线的教育平台作为案例解释G1是如何进行优化的。
在线教育平台的压力来自于哪里?首先孩子白天需要上学同时家长也需要上班,所以白天的访问量不会很大,同时主要的业务也不在在线教育平台处理。但是一旦到了晚上,机器的压力就上来了,同时孩子也会在线进行听课上课,这时候用户量会暴增,会有上万人同时在线听课。这时候可以发现在线教育平台的压力在于直播,而直播的流量高峰在于课堂的互动环节,为什么是互动环节呢,因为孩子不喜欢枯燥的课堂,为了带动课堂氛围,课堂中的游戏一定是活跃气氛的关键,也是系统压力的核心,这时系统需要记录各种数据,比如活动时长,得分,积分奖励等等,同时也会出现大量的对象分配,为了保证直播的流畅,系统要求十分低的延迟响应时间。
所以最终的结论是:在线教育平台大概在直播的互动环节压力会倍增,系统要求十分低的延迟响应时间。
经过了上面的情况分析,我们假设单台机器每秒大概有600个请求,假设每一个请求占用10KB,则是6000KB的大小占用也就是最终6M左右的内存占用。同时部署在一个4核心8G的系统上面。
同样的,这种案例也只是模拟和假设,具体情况受到各种因素的限制,切勿过于深究细节。
如何分析系统
传统的分代概念
我们用传统分代的概念部署一下这个系统,根据每一秒的请求为6M的对象大小,同时根据系统4核心8的配置,那么给JVM的内存大概是4G,我们知道直播的互动环节产生的积分,奖励,计算等对象基本都是朝生夕死的小对象,所以我们不太需要给老年代过大的空间,所以出去方法区和虚拟机线程栈的内存,我们给大约会给新生代3G和老年代1G左右的内容。
如果每秒产生6M的对象,那么一分钟就是300多M,按照默认的新生代配比8:1:1则Eden区域大概为2.4G的大小,survior区域为两个300M的大小空间。一分钟300M,那么基本上8分钟左右新生代就会满,此时假设有300M左右对象存活进入Survior区域,这时候Survior区域虽然可以装的下,但是由于超过了50%的配比,最终还是有约150M的对象进入老年代。
这时候再推算,老年代每8分钟进入150M的对象,大约40分钟左右就会整个系统停顿一次,这个停顿时间还是乐观估计,因为系统不可能只运作这一块的内容,单单是推算直播互动这一块就会产生这样的效果,可想而知加上整个系统的其他模块,**实际上5、6分钟可能会停顿一次!!!**这样肯定是不行的,试想下你玩游戏隔几分钟就停一下,对于小孩子来说突然卡一下导致丢分最后成绩不佳孩子又哭又闹,家长这时候不用想肯定会大量的投诉,被骂也不远了......
使用G1收集器
我们接着使用G1的收集器进行替换,系统部署在一个4核心8G的系统上面,假设机器在JVM上分配4G给堆,新生代默认初始化比例为5%,最大占比为60%,JAVA线程堆栈为1M,则大约开启几百个线程需要200,300M的空间,而方法区占用256M够用。
在上一节在文章中介绍过,可以把G1的工作机制想象成收盘子,但是放到系统上就很头疼了,**G1什么时候回来收垃圾是我们无法预测的!!**这里就需要通过一些辅助手段,同时这部分的监控操作需要工具和日志进行解读,所以将会放到后续的专门一篇文章进行解读。
如何计算Region的占用和大小:
按照4096M/2048 每个region是2M,如果按照新生代初始为5%,则根据参数5%新生代的大小为100个Region左右,4G的内存机器结果可以得出新生代初始200M左右的内存占用大小
至关重要的参数:
-XX:MaxGCPauseMills 参数:默认值为200,代表了200MS,表示最大的停顿时间为200MS。
如果使用G1收集器,这个参数直接影响了整个JVM系统的性能,如果这个数值过大,会导致垃圾收集的时间过长而导致前台卡顿,也容易导致新生代来不及触发垃圾回收就满了,或者导致老年代内存无法及时的回收。
多久会触发新生代回收操作
根据之前的说明,新生代最大可以使用60%的空间,同时也说明了新生代使用复制算法,根据8:1:1的规则,大概到达新生代的80%左右就会触发垃圾的回收操作?这种做法显然不符合G1基于全堆以及混合回收的操作。所以不能用固定大小的回收思路思考g1的回收操作。
正确做法: G1会根据200MS的要求,定时去判定当前的新生代是否可以符合200MS的收集操作要求,大意就是当新生代的垃圾回收需要耗时200MS的时候,就会触发新生代的回收。
这里也可以直接按照之前的理解餐厅的服务员定时过来收盘子的操作理解新生代多久进行一次回收操作。
如何优化:
上面的讨论几点之后,这就头疼了,这要这么优化?这时我们需要用上一些压测的工具以及GC日志和内存分析工具来考虑了,但是也不要让GC挺多停顿时间预设值太大了导致GC停顿时间太长,应该给个合理的值。
Mixed gc如何优化?
既然新生代的优化都已经很麻烦了,更不用说老年代回收了。而老年代的回收本身也没有了Old GC,取而代之的是Mixed GC,所以需要小心对待,我们从根本上还是需要防止让对象进入到老年代不断扩展导致mixed gc,这更加需要关注时间停顿模型这个参数。
最终分析
新生代:
- 由于是复制算法,所以需要从根本上处理的话依然需要控制新生代的存活对象进入survior的大小,同时控制在50%以内。尽量避免GC之后对象直接进入老年代。另外60%的新生代空间通常也不用怎么调整,除非业务对象频繁创建新生代会产生大量对象才需要考虑。
- 新生代有一个参数是存活对象大于85%的时候不需要进行拷贝,这个值如果设置小一点可能会提高回收效率,但是有可能造成大量的短命对象进入老年代的风险。
老年代:
- 按照G1的最后一个步骤,垃圾回收和系统回交替8次,同时在回到5%的region的时候停止收集,这个参数其实可以适当调大一些:G1HeapWastePercent
- 45%的老年代占用触发垃圾回收的机制,这个参数也不需要大改,因为JDK设置这个参数肯定是经过了很多测试和考量之后的结果。
总结
这一篇更多的是提供优化思路,JVM调优没有万金油的解决方案,特别是G1收集器这种算法细节十分复杂的收集器,调优需要更多的精力和时间测试调优效果。
写在最后
下一篇章会做一个整个系列到目前为止的小节,温故而知新,人的遗忘曲线更加需要我们反复的巩固知识和内容。