JVM内存GC的骗局——JVM不抛出OOM但内存已经泄露
概述
在日常测试中,我们会去重点观察java的内存使用情况,比如:进程会抛出OOM异常,不再接收新的请求;响应时间在固定时间段内变长,超时或者不响应,CPU使用率时常像过山车一样等。有时候JVM还会发生欺骗你的场景, JVM不停的在垃圾回收,可是每次回收完后堆却还是满的,很明显程序内存被使用完了,已经无法正常工作了,但JVM就是不抛出OutOfMemoryError(OOM)这个异常来告诉程序员内部发出了什么,只是不停的做老好人尝试帮我们做垃圾回收,把服务器的资源耗光了,但是此时服务器已经无法响应用户的正常请求了,让我们一起来看看这些情况发生时候的现象,体会一下被欺骗的感觉。
现状:
同事在模拟用户不停的发送请求给某系统,在运行一段时间后,突然,系统上邮件报告测试用例请求失败,登录测试系统的服务器,首先看下JVM的参数设置,如下:
-server –Xms4g –Xmx4g -XX:MaxPermSize=256m -verbose:gc -XX:+PrintGCDetails -Xloggc:$CATALINA_BASE/logs/gc.log -XX:+PrintGCTimeStamp,再使用TOP命令看看服务器发生了什么。

观察一段时间后,CPU一直运行在100%,于是想当然的认为可能是那段程序里面触发了BUG,有可能是正则表达式或者某段代码里面有个死循环的坑跳进去,没有出来。这不是很简单的事吗?直接使用jstack + pid 把堆栈打出来即可,直接操作吧,界面上马上输出操作日志,由于日志过多并且其他的日志类似,帮只截取部分如下:

从上面的堆栈日志可以看出,所有的线程都被BLOCKED住了,然后堆栈里面也找不到任何业务的相关代码,难道直觉出错了,感觉一下子不太好了,但是至少可以排查到不是上面的二种原因了,好吧,那再看看应用的GC的情况,部分日志如下。
1403682.561: [GC [PSYoungGen: 1375104K->11376K(1386176K)] 4145665K->2782002K(4182400K), 0.0174410 secs] [Times: user=0.27 sys=0.00, real=0.02 secs]
1407799.743: [GC [PSYoungGen: 1386160K->11632K(1386432K)] 4156786K->2793538K(4182656K), 0.0285330 secs] [Times: user=0.48 sys=0.00, real=0.03 secs]
1409230.024: [GC [PSYoungGen: 1386416K->10688K(1377984K)] 4168322K->2803822K(4174208K), 0.0265000 secs] [Times: user=0.43 sys=0.00, real=0.02 secs]
1409230.051: [Full GC [PSYoungGen: 10688K->7014K(1377984K)] [PSOldGen: 2793134K->2796224K(2796224K)] 2803822K->2803238K(4174208K) [PSPermGen: 48439K->48439K(262144K)], 7.8892780 secs] [Times: user=7.92 sys=0.00, real=7.89 secs]
1410502.582: [Full GC [PSYoungGen: 1366336K->85344K(1377984K)] [PSOldGen: 2796224K->2796224K(2796224K)] 4162560K->2881568K(4174208K) [PSPermGen: 48577K->48577K(262144K)], 8.2720110 secs] [Times: user=8.29 sys=0.00, real=8.27 secs]
解释一下:
第一行:
1403682.561: [GC [PSYoungGen: 1375104K->11376K(1386176K)] 4145665K->2782002K(4182400K), 0.0174410 secs] [Times: user=0.27 sys=0.00, real=0.02 secs]
发生的时间点,:JVM运行的时间长度,以度为单位,也可以格式化成固定的时间格式
PSYoungGen:发生了何种类型的GC,此处代表发生了年轻代的GC
1375104K:回收前的大小
11376K:回收后的大小
1386176K:YOUNG代的大小
4145665 K:回收前总的占用大小
2782002K:回收后的占用大小
4182400K:总占用大小
0.27和0.00:代表在用户态(user)和系统状(sys)的CPU运行时间
0.02 secs:代表实际的GC的运行时间
注:上面总的运行时间小于用户态和系统态的时间总和,是由于后者仅指CPU的运行时间,包括等待或IO阻塞的时间,而且现在的GC是采用多线程收集的,同时机器也是多个CPU,因此,大部分是二者之和要比前面的值大,如果是采用串形化收集器( serial collector)的话,二者时间几乎相差不多。关于各种收集器的差别,后续有时间再安排详细总结。
接下来的二行,不再重复说明,第四行有Full字样,代表JVM发生了Full GC,不过多了二个分区的收集,PSOldGen:老生代的回收前后空间大小及总空间;PSPermGen:持久代的回收前后空间大小和总空间。从第三行,可以看出老空间的使用率达到饱和,从而触发了FULL GC,但是很遗憾的是第五行后又接着发生了FULL GC,后面的都是一直在持续进行,但是系统一直不抛出OOM异常或者进程退出,导致这台机器服务进程一直存在,但是基本无法正常工作。
GC,无论Young GC还是Full GC,每次都会导致JVM STW(STOP WORLD)暂停用户的业务工作,来处理垃圾回收任务,短时间内无法响应用户请求,特别是大量的Full GC会导致系统响应速度降低,另外还有OOM的巨大风险。Young GC频繁,就算GC采用多线程回收方式,尽管回收的时候非常短,但是如果GC次数和频率很高,因此对应用的影响是不可忽视的。 Full GC 包括整个分区的垃圾回收,包括新生代、旧生代、持久代等。因此其回收成本高,应用也会暂停更长时间,无法及时响应用户的请求,所以需要特别注意这个种情况,一般来讲,排除主动的调用GC操作外,JVM会在以下几种情况发生Full GC。
1. 旧生代内存不足
2. 持久代内存不足
3. 统计新生代 GC晋升到旧生代的平均大小大于旧生代的剩余空间
解决
知道发生的原因后,就可以使用JMAP -heap直接看一下JVM内存的对像值,或者使用JMAP -dump直接JVM的堆栈DUMP出来,使用MAT打开分析就行。如果这种现像发生之后,DUMP出来的文件会较大,有些会达到十多个G,因为一般不直接在工作机器上进行,需要把文件转发到其他的非线上服务并且内存足够的机器上分析,最后可以用MAT把分析后的文件打开即可,操作结果如下:

第四行里面看不出实际的业务相关的,第五行到六行还是可以看出来的,那就先看第四行的对象包括什么具体的实例吧。

打开后,首页会给出可疑的建议对象实例,直接跳转到列表中,打开折叠细节即可看到真面目,里面包括了三十多万个对象,找相关的人员对根据业务需要,直接把不需要的实例在使用完后移除,其他几行的问题类似处理就即可。
总结
从上面GC的发生的情况来看,JVM一次次不停的努力的帮我们进行GC操作,直接把CPU全部占光,但是就是不直接抛出异常直接告诉我们内存不够了,感觉把我们带了到一个巨大的庞氏骗局,也许我们把JVM的内存加大,这个坑还将帮我们隐藏下去,如果程序设置了定时重启之类的操作,这个坑就永远发现不了。一般产品开发人员非常希望应用程序能在用户发觉之前发现这个问题,JVM无法判断出这个问题,也就不能帮我们抛出几乎OOM的异常,不过可以通过调整GCTimeLimit和GCHeapFreeLimit参数来重新定义何时抛出OutOfMemoryError错误。GCTimeLimit 的默认值是98%,也就是说如果98%时间都用花在GC上,则会抛出OutOfMemoryError。GCHeapFreeLimit 是回收后可用堆的大小。默认值是2%。当然最好的办法就是开发工程师开始就很清楚如何使用相关的容器类的正确用法,并且在上线前能经过充分的测试或运行。本文只是引用GC方面的一个具体的安全来说明GC是怎么骗人的,关于GC和JVM内存相关的细节如何及时的发现此类的问题,有机会再通过示例和大家探讨学习。
注:以上资料仅以HOTSPOT VM 1.7.65 版本参考。
本文章为作者原创
此文已由作者尧飘海授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 概述 在日常程序开发中,很多JAVA程度员不太关心内存的使用情况.当然,如果程序员运气较好或者系统没有大规模 ... 内存管理和垃圾回收是JVM非常关键的点,对Java性能的剖析而言,了解内存管理和垃圾回收的基本策略非常重要. 1.在程序运行过程当中,会创建大量的对象,这些对象,大部分是短周期的对象,小部分是长周期的 ... 本系列是 我TM人傻了 系列第七期[捂脸],往期精彩回顾: 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了:https://zhuanlan.zhihu.com/p/3970425 ... Overview 学习JVM首先需要了解一下JVM管理的内存是如何分布的,在看了<深入理解Java虚拟机>和一些博文之后,我准备自己记录一下学习的过程. 下图是JVM中运行时数据区的大致示 ... 相对于C.C++等语言来说,Java语言一个很美好的特性就是自动内存管理机制.C语言等在申请堆内存时,需要malloc内存,用完还有手动进行free操作,若程序员忘记回收内存,那这块内存就只能在进程退 ... 引言 对于C++程序员,内存分配与回收的处理一直是令人头疼的问题.Java由于自身的自动内存管理机制,使得管理内存变得非常轻松,不容易出现内存泄漏,溢出的问题. 不容易不代表不会出现问题,一旦内存泄漏 ... Java内存结构: JVM内存结构主要是有三大块:堆内存.方法区和栈.堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分为三部分,Eden空间.From Survivor空间.To S ... 最近在学习java基础结构,刚好学到了jvm,总结了以下并可以结合思维导图认识以下Jvm的对象: 栈:什么是栈? 先说一下栈的数据结构吧,栈它是一种先进后出的数据结构(FILO),跟队列刚好相反(先进 ... 默认情况下,并不是等堆内存耗尽,才会报 OutOfMemoryError,而是如果 JVM 觉得 GC 效率不高,也会报这个错误. 那么怎么评价 GC 效率不高呢?来看下源码: 呢?来看下源码gcOv ... 本文内容 Web 负载均衡 HTTP 重定向 反向代理 IP 负载均衡 DNS 负载均衡 Web 系统缓存机制的建立和优化 MySQL 数据库内部缓存 搭建多台 MySQL 数据库 MySQL 数据库 ... 本文内容 并行编程 数据并行 环境 计算 PI 矩阵相乘 把目录中的全部图片复制到另一个目录 列出指定目录中的所有文件,包括其子目录 最近,对多线程编程,并行编程,异步编程,这三个概念有点晕了,之前我 ... 1:修改Tomcat默认端口号,将默认的8080修改为8081 apache-tomcat-8.5.31\conf\server.xml <Connector port="8081&q ... 解决方法:点击你创建的项目 右键> 属性>签名>从存储区选择>选择证书这时候显示无可用证书 ,然后我从文件区选择了一个结果,又出现了第二个问题.提示我“签名时出错: 指定了无效 ... 设计目标: a) 规则引擎语法能够满足分单,计费,WMS策略的配置要求.语法是一致和统一的 b) 能够在不修改规则引擎模块的情况下,加入任意一个新的规则:实现上述需求之外的规则配置需求 c) 运算速度 ... 第2章 输入控件 输入控件是应用程序中用户接口的一种交互式组件.Android提供了大量的可供人们在UI中使用的控件,比如按钮.文本区域.(带滑块的)进度条.复选框.缩放按钮以及切换按钮等等. 在UI ... Xcode 插件优缺点对照(推荐 20 款插件) 2016-01-22 06:16 编辑: lansekuangtu 分类:iOS开发 来源:董铂然 的博客 28 13527 /XCode/" ... 项目里需要用到,但是不是用在新手引导上,下面的代码可以绘制一个圆的四分之一,用四个即可拼出一个圆. private createShape(): egret.Shape { let magicNum ... Fluent动网格中的DEFINE_GRID_MOTION宏允许用户定义网格节点的运动.本案例演示采用DEFINE_GRID_MOTION宏指定边界节点的运动. 案例动网格效果如图所示. 案例描述 本 ... 实验现象: 核心代码: int main(void) { system_clock.initialize(); led.initialize(); adc.initialize(); delay.in ...JVM内存GC的骗局——JVM不抛出OOM但内存已经泄露的更多相关文章
随机推荐