当前位置: 首页 > 科技观察

又抓到一个导致频繁GC的鬼——动态数组扩展

时间:2023-03-16 13:15:45 科技观察

概述这周有同事来咨询一个比较奇葩的gc问题。一般现象是系统一直在做cmsgc,但是oldgeneration并没有降下来,但是在执行一次jmap-histo:live之后,也就是主动触发fullgc后,oldgeneration会在a后降下来同时通过jstat-gcutil。乍一看理论上是不可能的,因为fullgc也会影响旧的Doing回收,于是请同事给他们的场景写了一个简单的demo,确实可以复现,但是他的demo中设置的Heap是32G,于是慢慢调整,终于用小内存搞定了。也可以重现。demo测试代码如下:就像上面注释写的JVM参数一样,控制新生代200M,老年代300M,老年代使用率达到90%时触发CMSGC。做CMSGC,但是oldgeneration不会down,但是只要你主动触发FullGC,oldgeneration会立刻被回收。执行allocateMemory方法时,预期的结果是List和里面的byte数组在gc后应该被回收,但事实并非如此。最初定位这段代码非常简单。我一遍又一遍地看着这段代码,我想改变一些东西来使问题出现转机。我一直在控制for循环的次数和每次分配的内存大小,最后我转移了目标。在ArrayList上,List中有一个数组。如果在添加过程中发现数组不足,则扩容。扩展就是新建一个数组,把旧的对象放到新的数组中。那我就试着想想还是不想想。扩容会不会有问题?于是开始调整ArrayList的初始大小。当我调整到一定大小的时候,我保证add过程中不会扩容。问题反了,可以正常回收了,比如上面的demo,设置数组长度为len,结果就完全不一样了,老年代很快就被回收了。目标可以锁定到arrayexpansionArrayexpansionArrayList中的arrayexpansion使用了System.arrayCopy调用,这是一个在java层面创建一个新长度数组的native方法,然后将旧数组和新数组都传过去array进去,将旧数组中的元素指针复制到native中的新数组中。实际上,您所做的是浅拷贝。反复看native的实现,基本无法解释现象。我一度怀疑自己对GC的理解是吧?什么细节没注意。经过我的内存转储分析,发现上面Demo中的List对象确实被回收了,但是List中的数组没有被回收,这个数组中的byte数组也没有被回收。原来这个鬼带着莫名的疑惑和我们组的同事讨论,看看有没有其他没有考虑到的可能的疑点。一开始他也很疑惑,后来传生突然想到可能是跨代引用的问题,于是回过头来仔细想了想每一步,好像真的可以,因为新的数组传给了系统.另外,拷贝只是浅拷贝,所以老年代的字节数组在新生代中有对新书组的引用,所以只做CMSGC是不可能回收老年代的这些对象的,因为CMSGC的一个gcroot是新生代中的对象。那为什么到现在我终于抓到鬼了,所以要对付攻略。这种情况下,只要保证在cmsgc回收old之前做一次ygc,就可以保证回收新生代的new数组,不指向老年代的byte数组。,那么这些数组是可以被cmsgc正常回收的,所以加上-XX:+CMSScavengeBeforeRemark可以解决这个问题。【本文为专栏作家李嘉鹏原创文章,转载请微信公众号(你个假笨蛋,id:lovestblog)联系作者授权转载】点此查看本作者更多好文