又抓了一个致使频繁GC的鬼--数组动态扩容

概述

本周有个同事过来咨询一个比较诡异的gc问题,大概现象是,系统一直在作cms gc,可是老生代一直不降下去,可是执行一次jmap -histo:live以后,也就是主动触发一次full gc以后,经过jstat -gcutil来看老生代一下就降下去了,初看下理论上不太可能,由于full gc也会对old作回收,因而我要同事针对他们的场景写了一个简单的demo出来,而后果真还真能重现,不过他的demo设置的Heap有32G,因而我经过慢慢调整,最终在很小的内存下也能重现出来java

Demo

测试代码以下:
image.png
正如我上面注释里写的JVM参数,控制新生代200M,老生代300M,老生代使用率达到90%的时候触发CMS GC,你们能够跑跑看,这种状况下会发现不断作CMS GC,可是老生代就是不降下去,可是只要你主动触发一次Full GC,老生代立马就会回收。
当allocateMemory方法执行完以后,期待的结果是gc以后List及里面的byte数组都应该被回收掉,但是事实并非这样的数组

初步定位

这段代码很是简单,我翻来覆去地看着这段代码,试图想改变点什么,能让问题出现峰回路转,我不断地控制for循环的次数和每次分配的内存大小,最终我将目标转移到那个ArrayList上,List里有个数组,在add过程当中若是发现数组不够了,因而会进行扩容,那扩容就是建立新的数组,将老的对象放到新数组里,那我试想要是不作扩容会不会有问题?因而我开始调整ArrayList的初始化大小,当我调到必定大小,保证在add过程当中不会作扩容,问题真出现了反转,竟然能正常回收了,好比上面的demo,将数组长度设置为len,那结果就彻底不同了,老生代很快就被回收了
那目标能锁定到数组扩容了学习

数组扩容

ArrayList里的数组扩容,使用的是System.arrayCopy调用,这是一个native方法,在java层面建立一个新的长度的数组,而后将老数组和新数组都传进去,在native里将老数组里的元素指针拷贝到新数组里,其实作的是浅拷贝,反复看native这块实现,也基本解释不通那个现象,一度怀疑我对GC的理解了,是否是有哪些细节没有注意到。
通过我内存dump分析,发现上面Demo里的List对象确实被回收了,可是List里的数组没有被回收,这个数组里的byte数组都没有被回收测试

原来是这个鬼

带着百思不得其解的疑惑和咱们组同事讨论,看看还有没有其余可能的没考虑到疑惑点,开始也都以为疑惑,后来传胜忽然想到会不会是存在跨代引用的问题,因而回过来仔细再想一想每一个步骤,好像还真有可能,由于传给System.arrayCopy的新数组是在java层面构建传进来的,在新生代分配的可能性最大,这样再加上拷贝仅仅是浅拷贝,那么老生代里的byte数组由于存在新生代里新数组的引用,那仅仅作CMS GC就不可能回收这些老生代的对象了,由于CMS GC的一个gc root就是新生代里的对象spa

那何解

至此终于抓出了那个鬼,因而想应对策略,既然这样,只要保证在cms gc回收old以前作一次ygc就能保证新生代里的那个新数组被回收而没有指向老生代那些byte数组,那么这些数组就能正常被cms gc回收了,因此加上-XX:+CMSScavengeBeforeRemark便可解此问题。指针

一块儿来学习吧对象

PerfMa KO 系列课之 JVM 参数【Memory篇】blog

实战:OOM 后我如何分析解决的内存

相关文章
相关标签/搜索