1:fullgc思路
首先先分析一下什么时候会ygc,什么时候会fgc,上篇其实已经把jvm对象的产生过程以及原理大致分析了一下,这章就直接实战了。其实答案很简单,eden区满了就会ygc,old区满了就会fgc,当然,还有可能元数据区(就是永久代)满了,也会fgc。大致就这几种,没啥玄乎的。看图
举个例子,假设堆内存一共20M,Eden区8M,S0 1M,S1 1M,old区10M,现在已经有6M的对象在Eden区,并且其中4M已经是垃圾对象,如果一次再产生4M的对象,Eden区发现目前一共就剩下2M空间,很明显放不下,就会触发ygc
另外,在触发ygc之前,jvm会先去看一下老年代的连续可用内存还剩多少,如果足够放下年轻代所有对象,就正常执行ygc,一旦发现不够,会触发担保机制(默认开启的),这个担保机制会算一下你原来平均每次会有多少对象进老年代,一旦发现你你原来平均的值大于老年代目前可用内存,就会触发fgc,然后再ygc。
- 说的是不是有点晕,不急,举个例子吧,你们就看者第一个图,我刚才举了一个例子,按照刚才的代码,这次ygc会有2M的对象不能释放,正常情况下,存活的对象是会被放到S区的,但现在S区一共也就1M,很明显放不下,只能放到老年代,好了,这次ygc结束,那个byte3的4M对象就有空间放到Eden区了,所以,目前Eden区有4M(byte3的),S区没有(放不下嘛),old区有2M(byte2的),一切又恢复原样,只不过老年代多了2M的对象,如果这样的过程执行四次,老年代就会有8M的垃圾,好了,现在第五次开始,继续之前的判断,准备ygc了(这时候还没ygc呢),这时候会看老年代剩余内存还剩2M(一般来说不到,因为除了我们声明的对象,还有JDK自动加载的一些对象),很明显放不下年轻代活着的所有对象(6M),触发担保机制,继续看历次平均进入老年代大小,很明显平均值是2,这时候一看,你这剩余的也不够担保啊,你先FGC吧(这里解释一下,FGC一般是全部gc,包括年轻代,老年代,永久代,但为啥一般很多人说oldGC就是fgc呢,因为oldgc几乎都会伴随着一次全部gc,所以其实一般oldgc大家都叫他fgc),老年代就一顿操作,把自己的8M内存释放了(方法执行完毕就会出栈,这些对象就不会有人引用了,也就成为了垃圾对象),这下舒服了,年轻代放心的开始ygc,开开心心的把自己的垃圾对象清空,又把存活对象扔到老年代(不扔也没办法,你S区也放不下啊)。
- 这下有没有清楚点?别急,事情远远没有那么简单,大家都知道,fgc是很消耗cpu和浪费时间的,那如果把这个例子交给你优化,你怎么样才能做到不会产生fgc呢?你可能会想,很简单,加机器内存,或者既然是因为S区放不下导致的,把S区比例加大就行了。好,加内存咱就不说了,除非说你有理由说不加不行,不然那肯定是最后选择的方案。先分析加大S区,我把比例改一下,现在让Eden区6M,S0 和S1 分别2M,这下行了吧,每次存活的都在S区,完美!事实上并不一定行。。年轻代晋升老年代有以下几种:1:大对象直接进老年代(这个大家都懂,也都听过,可以设置大小,默认的不同版本会有所不同 2:就像刚才说的,S区放不下 3:触发年龄阈值,这个一般也都听过,什么存活15次就晋升老年代,老实说一个对象ygc15次还不能回收,这个对象要么是你写的代码有问题,要么就是spring或者其他框架加载的对象,需要长期存在的,一般你不需要关注这个。4:动态年龄阈值,这个可能你也听过,原话我就不说了,说起来也拗口,我直接举例,假设现在S区共有20M内存,现在存活了10M的对象,分别是001号4M,002号4M,和003号2M,001号已经存活了2次,002和003存活了一次。内存已经到达S区总内存一半了,现在如果又进来一个新人004号4M,此时S区有14M对象,那这个时候002 003 和004加起来是10M,凡是超过这些对象的年龄,通通都要去老年代,001很明显就进了老年代。。惨,明明s区还有空间..
- 所以,一般来说,尽量不要使用S区超过一半,这玩意坑,至少你不可控啊,虽然按照我们举的例子,一般是没啥问题的,因为基本每个对象只会存活一次,但在高并发的场景下,确实有可能出现这种问题,最好的方法就是从源头上解决,我压根不用你超过一半。好了,那这个案例怎么优化呢?这就简单了啊,在不加内存的情况下,把老年代的内存抽出来点,改成5M,把空出的5M分给S区,优化后就是这样的
分配后Eden区会变成5M,S0和S1都是5M,老年代5M,就我们的代码而言(要排除一下其他干扰,有的jdk版本一启动就会产生几M对象,这个时候我们这个就凉了),第一个4M对象分配后,第二个2M的就直接触发YGC,把4M清理掉,存活的放S区,下一个4M的继续放Eden,周而复始,再也不用担心对象进老年代,更别提什么fgc了。(注:案例为了深刻,都是很极端的,你实际执行很可能OOM的哈哈,比如你jdk直接生成3M对象,加上我们那个2M存活,老年代也放不下了,可能会OOM的,所以如果你想执行,建议参数都加倍,主要理解原理)。
2:怎么知道对象生成速度
一般来说,刚才的ygc案例出来了,老年代gc就没啥说的了,老年代对象又不是凭空出来的,oldGC无非就是放不下了,会触发fgc
我给个思路,一旦线上频发fgc,你脑子里就自动浮现几种可能 1:内存设置不合理,导致对象频繁进入老年代 2:是不是有大对象产生?导致大对象频繁进入老年代。3:元数据区是不是满了4:是不是调用了system.gc
怎么验证?看参数!一般来说,大公司都有监控,你可以看他的对象生成速度,咱们这里就不说了,有时候真没命令舒服。先介绍几个常见命令,jps,jmap,jstat。jps可以查看你进程的pid,没这个pid你也查不了问题。拿到pid,我们可以用jmap -heap pid看一下内存分配,当然也可以用jstat直接看
这是先瞅瞅大概分配了多少比例,很明显,图中是我直接用java -jar启动的,没带任何jvm命令,默认就给我分了个这,2g的堆内存,给我eden区211M,S区才十几M,两个还不一样你敢信?
jstat -gc -h10 pid 1000 这个命令大概每秒输出一次gc信息,我这没访问,大家先看一下参数
- S0C,就是S0的总容量 大约22M
- S1C S1的总容量 大约12M
- S0U S0已使用内存
- S1U S1已使用内存
- EC eden区总容量
- EU eden区已使用
- OC old区总容量
- OU old区已使用
- MC 元数据区容量
- MU 元数据区已使用
- YGC 年轻代gc次数
- YGCT 年轻代总耗时
- FGC fgc次数
- FGCT fullgc总耗时
- 这些参数足够了,看一下每秒对象的存活,以及多久发生一次gc,每次gc各区域前后变化,就可以较为清晰的分析出哪块出了问题,比如每次ygc后你发现OU区域(老年代区域)都变大一点,你就知道是内存分配不合理,再分析一下每秒大约有多少对象存活,假如每次有50M对象存活,你就要保证S区尽量在100M以上,如果发现是eden还没满就开始fgc,就考虑是不是有人写了system.gc,显示调用强行回收(有时候有的类库也会调用,比如解析excel的时候,内部有可能会调用,解决方案就是配置参数禁止显示调用),或者看一下元数据空间是不是慢慢的增大,元数据空间是放类的地方,一般设置256M就够了,有时候有人代码里会写动态代理类,一个处理不好,循环调用,有可能元数据空间的类越来越多,就会触发fgc。发生fgc了,也可以用jmap -dump:live,format=b,file=xxx.bin pid 把gc信息给dump下来,用mat等工具分析,看看到底哪块占得内存大。
周记
- cms并发清除会有浮动垃圾产生,所以会有一个阈值,默认92%,如果超出,则会直接变成单线程回收
- 默认会有 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 每次oldgc会整理空间,0代表每几次就执行一次整理 设置xss线程大小1M,设置永久代256M就够了
- G1收集器每个region的大小是2M的倍数,1M也可,最多有2048个,年轻代默认年轻代有5%,达到60%会触发ygc,老年代如果到达45%则会触发混合回收 大对象则是单独的区域,每次ygc都会顺带回收,复制对象的时候如果有85%对象都存活,就不去回收了,混合回收的时候可以分多次回收,默认8次,一旦空闲的 region占总内存的5%则停止回收。大对象是超过region一半。
- jmap -histo pid 可以按顺序标出占用内存是哪些对象
- jhat 快照文件 -port 7000 可以图形化分析对象分布
- “-XX:TraceClassLoading -XX:TraceClassUnloading” 可以追踪类加载卸载情况 SoftRefLRUPolicyMSPerMB参数如果设置为0,则会很快清除软引用 有可能导致反射创建的类被不停创建导致元数据空间发生gc引起fgc 建议设置1000左右或者按默认值