JVM内存GC的骗局

此文已由作者尧飘海授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

概述

在日常程序开发中,很多JAVA程度员不太关心内存的使用情况。当然,如果程序员运气较好或者系统没有大规模的被测试或者被用户使用时,这个问题或许永远不出现,使得程序员一直认为内存反正是无限的,可以一直使用。确实,JVM的垃圾回收器会帮我们处理好所有的事情,可如果运气不是那么好,不幸就有可能发生在我们的身上,比如:进程会抛出OOM异常,不再接收新的请求;响应时间在固定时间段内变长,超时或者不响应,CPU使用率时常像过山车一样等。内存使用在大部分的工作时间可以正常工作,这样会导致很多的人对JAVA应用的内存使用情况不明了或者得不到充分的性能测试,而导致程序无法正常工作。出现上面的情况程序员一般会比较好的较快的发现问题或能总结一定的规律。

问题

有时候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 版本参考。

参考资料:

JVM http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gc-ergonomics.html

HotSpot JVM就是个庞氏骗局 http://it.deepinmind.com/gc/2014/04/01/hotspot-jvm-ponzi-scheme.html

Java内存泄露分析 http://doc.hz.netease.com/pages/viewpage.action?pageId=36468038

免费体验云安全(易盾)内容安全、验证码等服务

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 SVN迁移到GIT

原文地址:https://www.cnblogs.com/163yun/p/9875928.html

时间: 2024-10-13 22:44:02

JVM内存GC的骗局的相关文章

JVM内存模型以及HotSpot的GC策略

概述 想要进一步掌握Java语言,必须要深入了解一下Java程序的运行环境.本文会对JVM的内存模型.Java内存自动管理机制.以及Oracle官方虚拟机HotSpot在GC方面的实现策略进行大概的梳理. 什么是Java的内存模型? 众所周知,Java程序是运行在JVM上面的,但是不具体指定是哪一款JVM,只要是符合一定的规范的JVM,都可以正确的运行Java字节码.该规范由Oracle官方提供,旨在描述一个抽象的JVM,它包含很多部分,包括class文件结构.运行时内存状态.指令集等. 而上述

Jvm 内存浅析 及 GC个人学习总结

从诞生至今,20多年过去,Java至今仍是使用最为广泛的语言.这仰赖于Java提供的各种技术和特性,让开发人员能优雅的编写高效的程序.今天我们就来说说Java的一项基本但非常重要的技术内存管理 了解C语言的同学都知道,在C语言中内存的开辟和释放都是由我们自己来管理的,每一个new操作都要对于一个delete操作,否则就会参数内存泄漏和溢出的问题,导致非常槽糕的后果.但在Java开发过程中,则完全不需要担心这个问题.因为jvm提供了自动内存管理的机制.内存管理的工作由jvm帮我们完成.这样我们就不

java jvm内存管理/gc策略/参数设置

1. JVM内存管理:深入垃圾收集器与内存分配策略 http://www.iteye.com/topic/802638 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 概述: 说起垃圾收集(Garbage Collection,下文简称GC),大部分人都把这项技术当做Java语言的伴生产物.事实上GC的历史远远比Java来得久远,在1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言.当Lisp还在胚胎时期,

JVM内存管理&GC

一.JVM内存划分 |--------------------|-------------PC寄存器-------| |----方法区 ---------|--------------java 虚拟机栈--| |----堆区----------- |-------------本地方法栈------| |----共享区域------- |--------------线程独有-------| 其中左边为共享内存空间,右边为线程独有的线程级别的内存 各个区存储什么? 1.方法区:类的描述,常量等 2.

JVM内存管理及GC机制

一.概述 JavaGC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢.经过这么长时间的发展,javaGC机制已经日臻完善,几乎可以自动的为我们做绝大多数的事情. 虽然java不需要开发人员显示的分配和回收内存,这对开发人员确实降低了不少编程难度,但也可能带来一些副作用: 1. 有可能不知不觉浪费了很多内存 2. JVM花费过

现代JVM内存管理方法的发展历程,GC的实现及相关设计概述(转)

JVM区域总体分两类,heap区和非heap区.heap区又分:Eden Space(伊甸园).Survivor Space(幸存者区).Tenured Gen(老年代-养老区). 非heap区又分:Code Cache(代码缓存区).Perm Gen(永久代).Jvm Stack(java虚拟机栈).Local Method Statck(本地方法栈). HotSpot虚拟机GC算法采用分代收集算法: 1.一个人(对象)出来(new 出来)后会在Eden Space(伊甸园)无忧无虑的生活,直

一、JVM内存之GC

1.JVM内存划分为堆内存和非堆内存 2.堆内存用途:存放对象,垃圾收集器就是收集这些对象,然后根据GC算法回收. 3.非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据.方法.常量.属性等. 元数据: calss的文本,路径等类属性: static属性类方法; 在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存. 4.JDK1.8为什么要废弃永久代?

jvm-垃圾回收gc简介+jvm内存模型简介

gc是jvm自动执行的,自动清除jvm内存垃圾,无须人为干涉,虽然方便了程序员的开发,但同时增加了开发人员对内存的不可控性. 1.jvm是在计算机系统上又虚拟出来的一个伪计算机系统,它存在于计算机内存中并运行在操作系统之上, jvm在执行字节码时,把字节码解释成具体平台上的机器指令执行,那我们就来看看这个JVM伪计算机系统是怎么设计的,如下是jvm的体系图,运行数据区是jvm的内存模型: 程序计数器:一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器.字节码解释器工作时就是

JVM内存模型与GC算法(简介)

JVM内存模型如上图,需要声明一点,这是<Java虚拟机规范(Java SE 7版)>规定的内容,实际区域由各JVM自己实现,所以可能略有不同.以下对各区域进行简短说明. 1.1程序计数器 程序计数器是众多编程语言都共有的一部分,作用是标示下一条需要执行的指令的位置,分支.循环.跳转.异常处理.线程恢复等基础功能都是依赖程序计数器完成的. 对于Java的多线程程序而言,不同的线程都是通过轮流获得cpu的时间片运行的,这符合计算机组成原理的基本概念,因此不同的线程之间需要不停的获得运行,挂起等待