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

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

概述

分割线.jpg

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


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就是新生代里的对象。


何解


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




相关文章
|
Arthas 运维 监控
定位频繁创建对象导致内存溢出风险的思路
定位频繁创建对象导致内存溢出风险的思路
284 1
|
1月前
|
Java
Java数组动态扩容和动态缩减
Java数组动态扩容和动态缩减
20 3
|
3月前
|
存储 缓存 JSON
一行代码,我优化掉了1G内存占用
这里一行代码,指的是:String.intern()的调用,为了调用这一行代码,也写了几十行额外的代码。
|
3月前
|
监控 负载均衡 算法
线程数突增!领导说再这么写就GC掉我:深入理解与优化策略
【8月更文挑战第29天】在软件开发的世界里,性能优化总是开发者们绕不开的话题。特别是当面对“线程数突增”这样的紧急情况时,更是考验着我们的技术功底和问题解决能力。今天,我们就来深入探讨这一话题,分享一些工作学习中积累的技术干货,帮助大家避免被“GC”(垃圾回收,也常用来幽默地表示“被炒鱿鱼”)的尴尬。
46 2
|
4月前
|
人工智能 Java
JVM内存问题之当老年代缓慢增加且Full GC无法清除时,应如何使用MAT进行分析
JVM内存问题之当老年代缓慢增加且Full GC无法清除时,应如何使用MAT进行分析
188 0
|
Java 调度
服务器常见问题排查(一)——cpu占用高、上下文频繁切换、频繁GC
文章主要讨论了服务器中常见性能问题的一些排查思路,这篇文章主要讨论了CPU负载过高,频繁GC和频繁切换上线文这三个问题。
1238 0
服务器常见问题排查(一)——cpu占用高、上下文频繁切换、频繁GC
|
算法 安全 Java
JVM学习日志(十一) 对象进入老年代的情况 及 空间担保机制
对象进入老年代的情况 及 空间担保机制 简述
412 0
JVM学习日志(十一) 对象进入老年代的情况 及 空间担保机制
|
Java
16-内存分配与回收策略-对象优先分配Eden+大对象进老年代
大多数情况下, 对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时, 虚拟机将发起一次Minor GC。HotSpot虚拟机提供了-XX: +PrintGCDetails这个收集器日志参数, 告诉虚拟机在发生垃圾收集行为时打印内存回收日志, 并且在进程退出的时候输出当前的内存各区域分配情况。 在实际的问题排查中, 收集器日志常会打印到文件后通过工具进行分析 。
115 0
16-内存分配与回收策略-对象优先分配Eden+大对象进老年代
|
算法 安全 Java
18-动态对象年龄判断+空间分配担保规则+老年代回收算法
本文中用到的案例是接着上一篇文章继续的,如果有不清楚同学请先查看上一篇文章
143 0
 18-动态对象年龄判断+空间分配担保规则+老年代回收算法
|
存储 算法 索引
内存管理(三)——非连续分配管理方式(分页)
内存管理(三)——非连续分配管理方式(分页)
581 0