关于内存溢出,咱再聊点有意思的?

简介: 关于内存溢出,咱再聊点有意思的?

概述


上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白


本文其实很犹豫写不写,因为感觉没有太多值得探索的东西,不过文末估计会给你点小惊喜


或许大家曾经都碰到过HashMap因为其非线程安全的多线程并发操作导致cpu飙高的问题,不过这个问题在JDK8里已经解决掉了,其根本原因网上也早已遍地开花,所以我这篇文章里就不再熬述了,不了解的可以去网上找找相关文章,本文和大家聊的是看到的另外一个现象—-内存溢出


现象


同事丢了一个链接过来,是内存分析的,我看到一个线程占用的内存非常高,这个问题其实已然非常明显了,展开看了下线程栈


8.jpg

正在调用一个Map对象的toString方法,直到抛出java.lang.OutOfMemoryError,之所以这个栈顶能看到OutOfMemoryError的逻辑是因为配置了-XX:+HeapDumpOnOutOfMemoryError参数

不过这个参数只会生效一次,不会每次OOM的时候都做内存dump,大家可以想像一下,如果是代码的问题会发生连续的OOM,那连续做dump也没必要,于是JVM里控制这个参数只会在第一次发生OOM的时候做一次内存dump


分析


其实在我看到这个OutOfMemoryError栈之后,还没等同事说多少话,我就立马要同事去看我之前那篇关于OOM的文章了,想表达的是虽然这个线程栈里看到了OOM,但是内存泄露其实不一定是和这个线程有关的,可能只是临门一脚而已,不过后面细看了下这个线程占的内存其实真的挺高了,高达2G多,所以就这个案例来说还是和这个线程有关的,有时候不能太相信自己的经验,具体问题还是得具体分析才好


那为什么这个线程会占用这么大的内存呢?看到整个栈后面都在做字符串的拼接扩容动作,因为都是toString方法触发的,难道真的有个2G的字符串?询问同事他们说绝对不可能存在这么大的字符串,貌似老早之前有同事问过我类似的问题,不过我都一直怀疑他们说的,觉得肯定是存在这么大的字符串的,只是他们不知道而已,原来那个问题我也已经忘记最后情况了。今天又有类似的问题过来,我想也许我想的真的不对?后面同事打我电话说了下场景,他打印一个Map,但是这个Map其实是一个ConcurrentHashMap,是线程安全的,但是这个map里的value是一个HashSet,这个HashSet是非线程安全的,并且存在多个线程修改这个Set的情况,那会不会是因为并发导致的呢,HashSet里其实就是一个HasMap的结构,我觉得是很有可能的,于是要同事自己去模拟下这个场景,看能否重现出来


我继续看他们的内存dump,果然发现了一些猫腻,确实在打印那个HashSet过程中,next字段是循环连起来的,于是基本确定了死循环的存在,没过一会儿,同事也重现出来了,大概逻辑如下:

9.jpg

注意,这个得在JDK6或者7下跑才会重现,JDK8下不存在这个问题


Demo里就是两个线程同时对HashSet进行修改,可能带来的一个后果是里面的HashMap因为要扩容并且做rehash而出现死循环的情况,当有线程要打印这个HashSet的时候,会调用其toString方法,再看看其父类AbstractCollection的toString的逻辑:


10.jpg

就是挨个遍历,然后将值塞到StringBuilder里,如果正巧之前因为多线程的并发操作导致了死循环链的产生,那可能会导致这个StringBuilder会非常大,并且还会不断进行扩容,正如上面的堆栈看到的一样,这直接带来的一个后果就是出现内存溢出


内存富余下的OutOfMemory


对于同事线上碰到的那个问题看到的OOM提示是Requested array size exceeds VM limit,这个提示讲真我还是第一次碰到有发生的,假如说你的内存其实非常大,足够的剩余,但是当你要创建一个数组的时候,如果你的数组的长度超过Integer.MAX_VALUE-2的话,那你将会看到一个这个提示的OOM抛出来,其实这也是你能创建的数组的最大长度了,这或许很多人都没有注意到的,就把这个当做本文的一个最有价值的亮点吧

相关文章
程序员真的有必要把GC算法好好过一遍,因为它是进大厂必备的
最早的GC算法可以追溯到20世纪60年代,但到目前为止,GC的基本算法没有太多的创新,可以分为复制算法(Copying GC)、标记清除(MarkSweep GC)和标记压缩(Mark-Compact GC)。近些年推出的GC算法也都是在基础算法上针对一些场景进行优化,所以非常有必要理解基础的GC算法。
|
3月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
132 4
|
4月前
|
Arthas 监控 算法
JVM成神路终章:深入死磕Java虚拟机序列总纲
JVM成神路终章:深入死磕Java虚拟机序列总纲
|
Java
40-对象太多了!堆内存实在是放不下,只能内存溢出!
之前通过三篇文章的分析,介绍了 直接内存、Metaspace和栈内存三块区域的内存溢出,同时给出了一些常见的引发内存溢出的场景以及对应解决方案,一般只要vm参数配置合理,代码上不出现大问题,一般不太容易引发对应的OOM
187 0
40-对象太多了!堆内存实在是放不下,只能内存溢出!
|
存储 算法 Java
JVM垃圾清理机制详解 ✨ 每日积累
JVM垃圾清理机制详解 ✨ 每日积累
JVM垃圾清理机制详解 ✨ 每日积累
|
存储 Java 数据库
java内存溢出问题(工作中常用、面试中常问的一个知识点)
内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。这篇文章整理自《深入理解java虚拟机》。因为内存溢出问题不仅是工作中的一个重要方面,而且面试中也是经常问。
170 0
java内存溢出问题(工作中常用、面试中常问的一个知识点)
|
Java Linux Android开发
这次,我想把内存泄漏讲明白
这次,我想把内存泄漏讲明白
291 0
这次,我想把内存泄漏讲明白
|
存储 缓存 监控
|
存储 Web App开发 监控
一文让你读懂内存泄露
[TOC] ## 0. 背景 [![e62eaf67-820c-49ee-9295-86011d7d596c](https://typro-img-1256878004.cos.ap-nanjing.myqcloud.com/e62eaf67-820c-49ee-9295-86011d7d596c-1607941296624.jpg)](https://typro-img-1256878004.cos.ap-nanjing.myqcloud.com/e62eaf67-820c-49ee-9295-86011d7d596c-1607941296624.jpg) 没想到项目放到线上后
118 0
|
负载均衡 算法 Java
终于把JVM垃圾回收的来龙去脉搞清楚了(下)
终于把JVM垃圾回收的来龙去脉搞清楚了(下)