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 ... 1.前言 快半年没有写博客了,荒废了很久,工作中的杂事太多,自己越来越懒了.为了鞭策自己成长,还是要坚持写写博客,记录自己的成长. 2.shell函数介绍 语法: [ function ] funna ... 之前演示了一个 Hello World 程序,而后讲解了一些基础概念,下面开始一些基础工作. 将脚本放在哪里 脚本可以放置在两个位置 <head></head>之间,头脚本(h ... 虚拟机更换VDI文件,启动时会出现 "UUID already exists"的错误,这是因为删除虚拟机时候没有选择"删除所有",只是选择移除造成的. 方法一: ... 基本原理可以参考另一篇文章:鼠标滑过table时修改表格行的背景颜色 下面是针对GridView实现该效果的代码:就是编写GridView控件的RowDataBound事件的代码. protected ... 在学校的时候还总是自己用eclipse自带的jar导出工具,然后人工来给项目打包,那是相当的原始. 而后工作了,项目中都是用ant,慢慢的开始学会使用这个工具.感觉就和脚本一样,很容易读懂,做项目构建 ... 定位 工具位于jdk\bin目录下(双击运行): 可以本地或者远程调试,本地调试会自动加载,远程调试需要配置.下面以Tomcat为例. 概述 监视 线程 抽样器 Java 堆 百度经验参考:https ... border-radius:由浮点数字和单位标识符组成的长度值.border-top-left-radius --- 左上border-top-right-radius --- 右上border-bo ... 教程来自: Git教程(廖雪峰的官方网站) 学习Git的基本内容,对教程内容进行理解并脱水 1. Git简介 2. 安装Git 1. ubuntu上安装Git $ sudo apt-get insta ... 登录公司的跳板机是挺麻烦的事,首先要ssh,然后输入密码,有的公司可能还要动态密码,前两步操作都是固定的,所以能免去前两步的操作就会方便很多(线上出问题也能尽快登上去,免得紧张密码一直输错,哈哈哈). ... ##概览 下面列出了部署的一些方法: 1.本地构建程序并拷贝至服务器 2.在服务器上获取更新代码,然后构建并运行 3.使用`Heroku`来管理部署 使用交互式的命令行会话来进行部署 - 通常需要一种 ...JVM内存GC的骗局——JVM不抛出OOM但内存已经泄露的更多相关文章
随机推荐