#那些年,我们一起纠结过的OutOfMemoryError
>注:本文首发于公司论坛,是为公司论坛活动准备的帖子,所以文中提到很多与公司、同事相关的事情……部分敏感信息已屏蔽。
**目录**
[TOC]
##引子
作为一个技术宅,本不应该过分评价三次元的事,但需要声明的是:我不是陶渊明,我不独爱菊,也没有采菊东篱下的悠然……可是毕竟都是心为形役,心为形役呀……
与其说这是一篇技术分享,倒不如说这是一篇感想……(其实,这是一篇节操……)
说到感想,大家真的敢想么?所以,请原谅我把象牙吐在键盘上。(此句演绎自宋静茹某句,抱歉,原句已忘)
**————我———是———无———节———操———分———界———线———**
##前言
数据库最苦恼的问题,如果不是“会话失败”(Communications link failure )的话,那一定就是“死锁”(dead lock)了
web最苦恼的问题如果不是404或503的话,那一定就是“锟斤拷锟斤拷”(用以代指所有乱码问题,此类乱码只是因为很经典而已,但确实不难解决,早期ASP经常出现)了
C/C++最苦恼的问题如果不是“屯屯屯屯”和“烫烫烫烫”的话,那一定就是段错误(segmentation fault,windows下的表述为“该内存不能为read/write”)了……
java最苦恼的问题如果不是NullPointerException的话,那一定就是OutOfMemoryError了……
俗话说的好:1G的内存装了2G的烦恼。跟时间复杂度和空间复杂度离的最近的无非是CPU和RAM,从C到C++到Java没有一种语言是脱离内存解决方案和计算模式而存在的……
“内存不足”已不再是当前的主要矛盾,但是作为一个基本矛盾它将永远存在,并在一定情况下可能激化,当事先分配的内存大小已不能再满足应用日益增长的空间需求时,OutOfMemoryError就应运而生了……
也许在多少年以后,程序员们会想起当年那被硬件限制所支配的恐惧,以及我们这些在这个战场上牺牲多次已经血流成河的烈士……故作此文,以纪念那些年我们曾经烦恼过的OutOfMemoryError……
OutOfMemoryError简称OOM错误或OOME。如果说内存泄露是C/C++程序的吸血幽灵,那么OOME就是Java程序的地震灾情……在它发生之前,没人知道它是怎么发生的,包括国家地震局(在人大西门哦~各位四川分支的同事来总部的话可以去拍照留念,公开这个“秘密”的原因是因为我相信每个同事都是文明人,拒绝骂街扔臭鸡蛋等不文明行为),但是在它发生之后我们就应该“高度重视”了……下面总结一下我所知的一些OOM错误及感想。
##一、java.lang.OutOfMemoryError: PermGen space
遇到这类异常不要紧张,因为我们是不会承认这个异常是代码过错的(:-p)……
下面一段引自网络:
>PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen space错误,这种错误常见在web服务器对JSP进行pre compile的时候。如果你的WEB APP下都用了大量的第三方jar, 其大小超过了jvm默认的大小(4M)那么就会产生此错误信息了。
遇到这类异常的解决方式很简单,一般是使用JVM参数-XX:PermSize(初始化)和-XX:MaxPermSize(最大分配)将PermGen设的大一些……
啥?你说优化一下程序少用几个类来解决此类问题?何苦呢(嵌入式另当别论,此处讨论的是JavaWeb服务)……我的一位好基友(Geek好友)曾经说过一句话:在很多时候砸钱提配置往往要比给程序做优化更见效……有道理!!!因为无论哪个程序员都不会承认“金钱复杂度”是计算机理论的一部分……如果你也觉得有道理的话,那么恭喜你!!你跟我一样都拥有一颗屌丝之心……因为说这句话的人一定不是那个该为效率砸钱的那个人……真的程序员,要敢于面对屌丝的内心,要敢于直视永无休止的debugging……
发生PermGen溢出,谁都不想嘛,但是写那段程序的人一定不想对它负责。因为程序员的思维应该是这样的:如果有人胆敢怀疑你程序的性能有问题,你心里一定有一万匹草泥马奔腾而过然后说“一定是你打开方式不对吧!”。就像前台工程师多想在必要的时候输出一句类似于“this program cannot be run in dos mode”的话告诉客户他们是多么地不想支持IE6一样,对于运行环境,后台工程师更是男默女泪。
![cannot be run in DOS](dos.jpg "This program cannot be run in DOS mode.")
所以你一定想知道自己的孩子(应用)当前的工作环境如何(毕竟是亲生的额)。如果想在java程序中了解当前JVM进程的内存分配现状的话,请查阅JDK API文档中的java.lang.management.MemoryMXBean接口。其实编程跟谈恋爱是一样一样一样滴:如果一个程序当它还是一条命令行的时候就已经注定对于它的所有对象来说,结果终究是一个“记忆之外的错误”(Out Of Memory Error)的话——好吧,除了在代码中写上一句“/* 爱过 */”,你也可以在报错日志中帮你的应用“吼”出在这样一个连PermGen都出不起的小气环境中是有多憋屈(你说你想实时监控?慢慢看,后面会有的……)
大家先看看MyEclipse是怎么干的:
![eclipse cry...](eclipse.jpg "what happened in eclipse.")
好帅的样子……其实,这种内存不足的痛楚让我每一次关MyEclipse的时候都有一种“打土豪,分田地”的快感。可叹的是,MyEclipse在侵占了几百兆内存的时候还是一副不知足的样子。
![taskmgr said](taskmgr.jpg "eclipse is fat!")
>总有人永不知足、总有人装不在乎,兜兜转转迷了路。——《迷途》
MyEclipse在对待内存不足的时候是何等的英姿飒爽,令人不禁拍案叫绝!不知天下多少英雄豪杰,无论是何等的英明神武,遇到这种情况也会放下屠刀,就此拜倒跪舔……
但是请各位不要高兴的的太早,上面那个MyEclipse提示的是Java Heap Speace溢出,PermGen貌似不那么好玩,更多时候PermGen类型的OOME会在你的代码执行之前先报,刚才提到的日志中的幻想往往都是你自己的一厢情愿,因为当OOME出现的时候、当代码开始执行的时候,可能一切挽回都已经太晚了……
更可恨的事情在于,像Spring、Struts、Hibernate这些号称轻量级的框架也经常搞出这么个异常来,这么赶时髦的异常弄得java程序员们写个类库,不抛这个异常都不好意思跟别人打招呼……(如果你认为这三个框架只会搞出点PermGen的小儿科OOME来,那你就太小看它们了,俗话说的好:吃素的往往比吃肉的长得更庞大。)
>啊!多么痛的领悟 ——《领悟》
##二、java.lang.OutOfMemoryError: unable to create new native thread
这个异常看上去是如此的无力,就像五月天遇见了五月病……(五月病是啥?那种感觉很难描述,就像是一个程序员来了大姨夫)
这类异常可不是java独有的,我就曾在C语言程序中遇见过类似的因僵尸线程过多导致的堵塞,直到我知道pthread_create出的线程原来是需要pthread_detach一下的(当时的水平是有多弱呀……)。但是今天要谈的不是C/C++,之所以要谈这个例子,只是想提醒大家一下,当遇到一些异常的时候,有些“量”可能会帮我们搜索到答案,比如,为什么每次都是到第306个线程之后才开始出问题、为什么每次mysql断开链接都是八个小时?在搜索框里输入“第306个线程”、“八小时断开连接”就会发现,这些数字其实并不是偶然。一般情况下,在2G内存主机的windows环境发生这类异常时内存中的线程数约为五千多个,但是如果你调过JVM参数的话,这个数量就不一定了……
言归正传,跟“第306个线程”和“八小时断开连接”问题一样,大部分情况下,遇到这种异常,单凭修改系统配置总是治标不治本的,但并不证明所有的问题都不需要修改系统配置。JVM所能创建的最大线程数公式如下:
>(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Max Num of Threads
>MaxProcessMemory 指的是一个进程可以占用的最大内存,受硬件和OS(操作系统)限制,此参数一般接近于OS所能访问的最大空间限度
>JVMMemory JVM内存,受参数“-Xms”、“-Xmx”和“XX:PermSize”、“-XX:MaxPermSize”的影响
>ReservedOsMemory 保留的操作系统内存,一般是除目标进程之外的已占用内存
>ThreadStackSize 线程栈的大小,受参数“-Xss”影响
上面这个公式博大精深,让我们意识到,原来“-Xmx”和“-XX:MaxPermSize”不是愈大越好的……它已经彻底打破了“你付出越多就会收获越多的回报”的逻辑……“-XX:MaxPermSize”参数上面已经说过了,不过在你急着调“-Xmx”这个参数之前,建议看完下面的JavaHeap溢出。
上面已经说过了,除非你真的需要五千个以上的进程数,哦,抱歉,如果不幸言中了的话——你最应该干的事应该是换台大内存机器并安装64位OS,而不是拆东墙补西墙最后以捉襟见肘告终:折腾可怜的JVM参数的后果就是导致上面的PermGen溢出或者下面JavaHeap溢出。(欢迎各位同事借此文写一篇《论砸钱与屌丝程序员的节操》的读后感)
大家暂时不要绝望,因为刚才可能遗漏了一点,那就是虚拟内存。是的,尽管对于一个32位系统来说,无论虚拟内存有多大,访问4G以上的内存都是很麻烦的一件事,但是有些事升级64位OS或许能使问题得到很好的解决,尽管虚拟内存在处理速度上并不给力,但是它给苦逼的小内存用户提供了一个解决方案……那么要达到五千线程以上的话,前提是你的内存大小能顺利支持64位OS的运行……之前说到PermGen的时候并没有提虚拟内存,那是因为如果你连PermGen的内存空间都出不起的话,事情就没什么好继续谈的了……
等、等一下!我们是什么时候开始有了我们需要更多线程和更好的机器的错觉?创建更多的线程和更好的服务器好像很酷的样子,但是当我们把久违的节操拾回来的时候,就会发现大部分情况下是应该知足的吧……想一想在linux的C语言下默认只能创建三百个线程,那五千个线程将是怎样一种优越感呀……
要不是资源是有限的,估计也不会有人懂得节约……资源不足大部分情况下源自于资源浪费……那到底是什么吞噬了仅有的资源?僵尸进程?死锁?死循环?很难说……尽管现实有点残酷,但是JDK1.5及以上版本用户应该很庆幸JDK自带了一大堆的调试、分析、监控工具,其中的jstack可以用来查看当前JVM中到底有哪些线程,关于这些调试工具,后面再说吧……但是我想告诉大家的是,想当年我遇到的这个问题的原因是因为有段代码,每次触发都会产生一个计时器,最后那些计时器吞噬了一切。
>你、存在我某一个模块里,我的梦里、我的心里、我的程序里。
>你、存在我哪一个模块里,我的梦里、我的心里、我的程序里……
> ——网友填词《我的程序里》(原曲:《我的歌声里》)
##三、java.lang.OutOfMemoryError: queue full
这类异常有些人一辈子都没见到过,但有些人却一直被它困扰……
有界队列是不会导致OOME的,OOME只有无界queue才会发生。说到这儿你可能会说,照这么说String也可以引发OOME的呀!而且所有的无界的集合类数据结构都有可能引发OOME的呀,为何单单拿出queue full来说事?是的,当前我正好就有一位同事现在就正在饱受ArrayList过大引起的OOME的折磨,这也是为什么我要发这个帖子的原因……但是对于大数据量的String来说,大家都会很自然的采用流读取,而对于其他数据结构来说,实在是没有queue full有讲头。
好吧,其实这不是理由,而是借口。首先需要告诉大家的一点是无界的集合类数据结构,包括String这类怪怪的基本数据类型,在发生OOME的时候,大部分情况下报的都是大家所熟知的java heap speace而不是什么String full或者List full之类的神奇提示。其次,不知道大家有没有意识到一个矛盾在里面:如果一个队列无界,那么不应该会full,只有有界的队列会full;如果一个队列有界,那它就不应该OOME,即便是OOME,也应该是先报java heap speace而不是queue full……这不科学……
队列是一种很“公平”的数据结构,它的特点被称为FIFO(First In First Out,先进先出),它常常用来辅助算法的实现,如树或图的层次遍历以及FCFS(First Come First Server,先来先服务,这是一种资源调度算法,注意它与FIFO在概念上的差距,尽管它俩关系很铁,但是如果你自己不注意点的话也一样有人说你乱入)。在这些情况下,都很难遇到一次queue full的情况。但是我相信每一个中国人都懂得“队列溢出”是怎样一种概念,也许你想到的场景更多的是在接近9:30am的时刻你还在XXXX大厦的一楼等电梯的焦急,或者是在十楼排队上厕所的崩溃,那么,可能是你想的太多了……不管是春哥显灵、“贾俊鹏出世”还是“圣战”再现,就算你历尽千辛万苦在12306订到年假回家的火车票(12306战果斐然,曾秒杀github),好不容易全家凑到一起看场春节联欢晚会,那除夕夜零点的祝福短信也可以变成一枚定时炸弹让你不得安宁……在这方面,中国人总有让全世界不敢直视的爆发力,那近乎于DDOS攻击的灾难,让所有人光靠脑补就不寒而栗——它们是发生在IT界的“群体性事件”。
>我们平时的扩展性都是纸上谈兵,.cn的扩展性才是实战。——信息安全技术工程师Peck
闲言碎语不要讲,下面我们来讨论一下什么情况下才会发生这种错误。对于一个队列来说,生产者与消费者,一个都不能少。是的,如果你想到了多线程同步中的“生产者消费者问题”,那么恭喜你学会抢答了。哲学家要就餐,理发师要睡眠,作为一个写者,写了这么多,没有读者也是白搭……于是国外诞生了一个专门的学科来研究生产者和消费者之间的关系的问题,它被称为《西方经济学》,书曰:生产能力高于消费能力曰通货膨胀,queue full就是经济危机,边际效益递减规律告诉我们这是一个恶性死循环……于是,马克思说:“其实《西方经济学》里都是骗人的,那是高帅富的成功哲学”……如果有人想问我最后一个《西方经济学》问题的话,我的答案是“挂过”……作为一名学霸,这是我一生中最大的污点,从此我感觉自己再也不会相信爱情了。其实……那门学科既不是《西方经济学》也不是《哲♂学》,它叫《排队论》……
但是,请你相信我:尽管《排队论》值得你去爱,但是不要相信它能帮你解决这个问题……老老实实排队?作为一个优先级不高的屌丝你等不起……(所以屌丝们才会这么无节操吧?屌丝们,脸皮厚心黑无所不用其极吧……这么想的肯定后宫片看多了……)
队列的用法一般有两种,一种是刚才说的作为辅助算法的数据结构,这种情况下,生产者和消费者往往在同一个线程中,他们之间不需要专门的线程同步控制,他们之间有严格的顺次执行关系,也不需要考虑线程安全,在这种情况下,queue full问题只能是由问题规模决定的,这种由问题规模决定的结果属于不可抗力,因为就像刚才说的,所有无界的集合类数据结构遇见大规模情况都会毫无例外的OOME,但是这些问题往往都不会报queue full,而是java heap speace……对与这种情况来说,有两种解决方案:优化算法,寻找更合适的数据结构,如采取一些时间换空间的手段神马的,还有刚才提到的虚拟内存,或者直接使用外存来分段解决问题神马的;另外还有一种,你懂的……
然后另一种才是多线程的“生产者消费者同步问题”,在这类问题中,生产者和消费者处在不同线程,组合方式也是五花八门。在java中除了可以使用信号量(java.util.concurrent.Semaphore)、互斥锁(java.util.concurrent.locks.Lock)和互斥关键字(synchronized)来解决同步问题之外,还有一种相当好用而省事的数据结构,它就是堵塞队列(BlockedQueue)。堵塞队列常常用来方便的解决一些简单的同步问题,像理发师睡眠问题、简单的读者写者问题(像哲学家就餐问题这么复杂的问题虽说也能解决,不过还是免了吧)。以上说了那么多废话,其实无界的堵塞队列才是产生queue full类OOME的关键所在。
生产者与消费者的速度不协调是一个极度难以解决的运筹学问题(经济学问题?),但是正如上面所说,排队论一般情况下会无能为力。因为大部分情况下,生产者的速度往往是不可控制的,而且往往是触发性的,因为在这种情况下,堵塞队列才能发挥它作为没有活干的时候休息,有活干的时候立刻唤醒的功效。生产者可能是供应链的上游发来的报文,也可能是上级下发的任务。如果生产者和消费者在宏观上平均速度是协调的,那么只要在缓存足够的情况下,理论上不会出现queue full的情况。如果消费者的速度快于生产者,那么消费者线程就有足够的时间睡大觉了(这个时候更多应该考虑的是怎样解决八小时断开链接异常的问题吧……对于大数据量的频繁写入操作,采用批量提交的方式如果遭遇空闲八小时断开链接的话果然是很蛋疼的,而且不光是八小时,只要在事务提交前连接中断,这就是一个世界性的难题。欢迎有见识的同事参与此问题的讨论。)。但是如果反过来的话,不仅消费者线程没时间睡懒觉,事情也变得复杂起来……如果,此时消费者可以选择将一些不重要的优先级不高的内容忽略掉的话,那是最好啦,就像美女拒绝过多的求爱者一样,但是更多时候是投鼠忌器的,就像资本家倒掉过剩的牛奶……但是无论如何足够大的缓存,可以帮助消费者撑上个一时半会,正如前面提到的,如果内存真的不足的话,那就动用外存吧,比如说硬盘:将暂时处理不了的任务放到硬盘里,直到硬盘被塞满……那么这就是能力不足又患得患失的后果……
说来说去,无非是消费者线程的处理能力不足引起的。此时,有很多想到了多线程,认为增加消费者线程的数目就能加快处理速度……显而易见,第一个示例往往是反例(此处不讨论多线程的多CPU调度)。因为消费者线程的处理能力不足,往往是CPU运算速度跟不上节奏引起的,如果增大线程数目,得到的结果往往是加重了CPU的负担,最终降低处理速度。在这种情况下,想到并行处理是对的,但不是在一个机器上(更确切的说是在一个CPU上),于是就有了多服务器负载均衡以及集群之类的“高科技”名词……
但是更多时候提高消费者线程处理速度往往没有想象中的那么复杂,如果我告诉你想当年我们碰到的消费者线程处理能力不足到最后查明原因是因为输出的日志内容过多,导致频繁的的IO访问从而影响了处理速度,你会相信么?
>为了你我哭过痛过都不算什么
>可是我走不进你记忆(Memory)角落
>太多如果(if)猜不出的结果
>好像bug又比bug多了一些什么
>可是我……为了你……哦……
>——演绎版《可是我》
##四、java.lang.OutOfMemoryError: Java Heap Space
终于到了这个期盼已久的OOME。但是写到这儿的时候我发现我可以用来完成这个帖子的时间真的不多了,请原谅我把压轴到最后搞成了狗尾续貂……这就是人生呀,直到最后才发现我们把时间都花在了那些陪衬并非真正是重点的事物上。
其实该说的前面都说了,无非是堆内存不够用而已,MyEclipse会报,Spring、Struts、Hibernate也会报。遇见这种问题的话,在综合考虑机器性能等方面的基础上修改JVM参数了,关于如何提高性能和效率这方面的讨论其实上面已经说的很多了。至于Java Heap Space的介绍、JVM相关参数以及设置多大合适还是引用一下网上段落吧:
>设置 JVM堆的设置是指java程序运行过程中JVM可以调配使用的内存空间的设置.JVM在启动的时候会自动设置Heap size的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap size 的大小是Young Generation 和Tenured Generaion 之和。提示:在JVM中如果98%的时间是用于GC且可用的Heap size 不足2%的时候将抛出此异常信息。提示:Heap Size 最大不要超过可用物理内存的80%,一般的要将-Xms和-Xmx选项设置为相同,而-Xmn为1/4的-Xmx值。
此处还是说一下,常用构架会导致这种异常的原因。从数据库中取出的数据原本是放在ResultSet对象里面的,而ResultSet的真正数据实际上还在数据库系统中,通过记录集游标来逐行读取。为了保证分层封装,往往将数据库操作封装到DAO里,将数据库底层操作与上层隔离,于是便有了将所有数据从ResultSet中一次性全部取出放到List里的常规做法。只可惜,在结果集很大的情况下,不仅经过List的传递使得前台的响应速度变慢,还使得后台报OOME。
不得不单独拿出Hibernate来说事,这是一个很奇葩的东西,值得我们去吐槽。因为要不是Hibernate老是报OOME问题,我的毕业论文就没素材了。Hibernate使得好多程序员误以为“缓存的才是最好的”,它不仅仅使用缓存,还有两级缓存,听上去很先进的样子。但事实上,缓存这个东西,在更多不适用的情况下不仅不会加快速度,还会造成不必要的麻烦,更会引发OOME。不了解的人还是少用为妙。我相信更多处理大数据的人会选择将它们关掉,因为不必太高的数量级就可以轻松的占满缓存,产生OOME。但是会话级缓存是无法关闭的,而会话级缓存属于事务级缓存,在事务提交之后才可以放心大胆的清空。对于频繁写入型,一般的看法是把多次写入攒到一个事务里一起提交可以提高效率,这是对的。但是麻烦在于,如果事务提交的时机不对,要么写入过于频繁,效率降低,要么就会导致会话级缓存爆满,严重时就会引发OOME。此时如何保证封装性也变成了一道难题。
除了Hibernate的缓存机制,另一个要吐槽的就是Hibernate的线程安全了。Hibernate留下了太多线程安全方面的陷阱……Hibernate将线程安全问题成功的抛在了类库之外,自己却毫无顾忌逍遥法外。也许写到这儿你就会明白Queue full的问题来自何处了……是的,使用堵塞队列是我想到的一个既能解决线程安全问题,又能解决事务提交时机问题,还能解决封装性的一个很好的办法。可惜,当我正要高兴时,Queue full不约而至。结论:机器性能不好,好比一个熊孩子的叛逆心理,它就像一个气球充满了气,你按住这儿,别的地方又会鼓起来,而且鼓得比刚才那个还要高,当你把所有的地方都按住的时候,它砰的一下爆了。
>你伤害了Word,却Excel而过,你Access灿烂,我Outlook懦弱。——恶搞版《一笑而过》
**————我———是———有———节———操———分———界———线———**
##后话
好了,四类常见的OOME终于话痨完了,本来预计是要专门介绍一下JDK 1.5及以上版本自带的调试工具以及各种OOME的调试、分析、检测、检查以及监控方式的,看来时间和空间以及精力都不允许了。当前打算是另开一个贴,然后转发一个各调试工具用法的帖子吧。工欲善其事,必先利其器。所以,好的调试工具、高性能的服务器……(掌嘴!)
最后我想说的是,调试、分析、监控虽然很重要并不是解决问题的王道。好的程序员把时间花在设计和编码上,差的程序员过劳死在了调试的路上(在这个连“大黄鸭”都会倒下的年代,大家要多注意身体哟~)……理想中是:我们把所有的臭虫(bug)都掐死在了开发的路上;现实却是:如果一个程序员过劳死了,那他一定留下了一大堆bug;如果一个程序员辞职了,哔——……
有首诗提到了有先见之明是多么的重要,虽然不明白是怎么回事,但是好像很厉害的样子(你不可能到现在还在想地震局的事吧?):
>曲突徙薪不谓贤,焦头烂额飨盘筵。
>时人多是轻先见,不独田家国亦然。
>——周昙《春秋战国门再吟》
预防、预测、预警都很重要,但也过犹不及,杞人忧天亦不可取。综上,想要面面俱到还有很长的路要走呀……(写到这儿我突然有种想把标题改为《从OOM谈家国社稷》的冲动)
最后肯定是要用几句歌来结束文章,才能体现文艺气息,为了对应标题,那就用《那些年》吧:
>曾经想征服全世界,到最后回首才发现,这世界滴滴点点,全部都是你。 ——《那些年》