JAVA内存溢出解决方案.docx
《JAVA内存溢出解决方案.docx》由会员分享,可在线阅读,更多相关《JAVA内存溢出解决方案.docx(15页珍藏版)》请在冰豆网上搜索。
![JAVA内存溢出解决方案.docx](https://file1.bdocx.com/fileroot1/2023-7/24/6af9aae5-1cf9-4a2a-a249-d441295b05e1/6af9aae5-1cf9-4a2a-a249-d441295b05e11.gif)
JAVA内存溢出解决方案
JAVA内存溢出
解决方案
1.内存溢出类型
1.1.java.lang.OutOfMemoryError:
PermGenspace
JVM管理两种类型的内存,堆和非堆。
堆是给开发人员用的上面说的就是,是在JVM启动时创建;非堆是留给JVM自己用的,用来存放类的信息的。
它和堆不同,运行期内GC不会释放空间。
如果webapp用了大量的第三方jar或者应用有太多的class文件而恰好MaxPermSize设置较小,超出了也会导致这块内存的占用过多造成溢出,或者tomcat热部署时侯不会清理前面加载的环境,只会将context更改为新部署的,非堆存的内容就会越来越多。
PermGenspace的全称是PermanentGenerationspace,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGenspace中,它和存放类实例(Instance)的Heap区域不同,GC(GarbageCollection)不会在主程序运行期对PermGenspace进行清理,所以如果你的应用中有很CLASS的话,就很可能出现PermGenspace错误,这种错误常见在web服务器对JSP进行precompile的时候。
如果你的WEBAPP下都用了大量的第三方jar,其大小超过了jvm默认的大小(4M)那么就会产生此错误信息了。
一个最佳的配置例子:
(经过本人验证,自从用此配置之后,再未出现过tomcat死掉的情况)
setJAVA_OPTS=-Xms800m-Xmx800m-XX:
PermSize=128M-XX:
MaxNewSize=256m-XX:
MaxPermSize=256m
1.2.java.lang.OutOfMemoryError:
Javaheapspace
第一种情况是个补充,主要存在问题就是出现在这个情况中。
其默认空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。
如果内存剩余不到40%,JVM就会增大堆到Xmx设置的值,内存剩余超过70%,JVM就会减小堆到Xms设置的值。
所以服务器的Xmx和Xms设置一般应该设置相同避免每次GC后都要调整虚拟机堆的大小。
假设物理内存无限大,那么JVM内存的最大值跟操作系统有关,一般32位机是1.5g到3g之间,而64位的就不会有限制了。
注意:
如果Xms超过了Xmx值,或者堆最大值和非堆最大值的总和超过了物理内存或者操作系统的最大限制都会引起服务器启动不起来。
垃圾回收GC的角色
JVM调用GC的频度还是很高的,主要两种情况下进行垃圾回收:
当应用程序线程空闲;另一个是java内存堆不足时,会不断调用GC,若连续回收都解决不了内存堆不足的问题时,就会报outofmemory错误。
因为这个异常根据系统运行环境决定,所以无法预期它何时出现。
根据GC的机制,程序的运行会引起系统运行环境的变化,增加GC的触发机会。
为了避免这些问题,程序的设计和编写就应避免垃圾对象的内存占用和GC的开销。
显示调用System.GC()只能建议JVM需要在内存中对垃圾对象进行回收,但不是必须马上回收,
一个是并不能解决内存资源耗空的局面,另外也会增加GC的消耗。
2.JVM内存区域组成
简单的说java中的堆和栈;java把内存分两种:
一种是栈内存,另一种是堆内存。
2.1.栈
在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配;在函数(代码块)中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间。
栈调整:
参数有+UseDefaultStackSize-Xss256K,表示每个线程可申请256k的栈空间,每个线程都有他自己的Stack。
●优势是存取速度比堆要快;
●缺点是存在栈中的数据大小与生存期必须是确定的无灵活性。
2.2.堆
堆内存用来存放由new创建的对象和数组;在堆中分配的内存由java虚拟机的自动垃圾回收器来管理。
java堆分为三个区:
New、Old和Permanent。
GC有两个线程:
新创建的对象被分配到New区,当该区被填满时会被GC辅助线程移到Old区,当Old区也填满了会触发GC主线程遍历堆内存里的所有对象。
Old区的大小等于Xmx减去-Xmn。
●优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的;
●缺点就是要在运行时动态分配内存,存取速度较慢。
3.JVM如何设置虚拟内存
提示:
在JVM中如果98%的时间是用于GC且可用的Heapsize不足2%的时候将抛出此异常信息。
提示:
HeapSize最大不要超过可用物理内存的80%,一般的要将-Xms和-Xmx选项设置为相同,而-Xmn为1/4的-Xmx值。
提示:
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
因此服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小。
提示:
假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。
简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,
这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了
提示:
注意:
如果Xms超过了Xmx值,或者堆最大值和非堆最大值的总和超过了物理内存或者操作系统的最大限制都会引起服务器启动不起来。
提示:
设置NewSize、MaxNewSize相等,"new"的大小最好不要大于"old"的一半,原因是old区如果不够大会频繁的触发"主"GC,大大降低了性能
JVM使用-XX:
PermSize设置非堆内存初始值,默认是物理内存的1/64;
由XX:
MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
解决方法:
手动设置Heapsize
修改TOMCAT_HOME/bin/catalina.bat
在“echo"UsingCATALINA_BASE:
$CATALINA_BASE"”上面加入以下行:
JAVA_OPTS="-server-Xms800m-Xmx800m-XX:
MaxNewSize=256m"
4.内存溢出检测
如何查找引起内存泄漏的原因一般有两个步骤:
第一是安排有经验的编程人员对代码进行走查和分析,找出内存泄漏发生的位置;第二是使用专门的内存泄漏测试工具进行测试。
第一个步骤在代码走查的工作中,可以安排对系统业务和开发语言工具比较熟悉的开发人员对应用的代码进行了交叉走查,尽量找出代码中存在的数据库连接声明和结果集未关闭、代码冗余等故障代码。
第二个步骤就是检测Java的内存泄漏。
在这里我们通常使用一些工具来检查Java程序的内存泄漏问题。
市场上已有几种专业检查Java内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。
开发人员将根据这些信息判断程序是否有内存泄漏问题。
这些工具包括OptimizeitProfiler,JProbeProfiler,JinSight,Rational公司的Purify等。
4.1.性能检查工具使用
4.1.1.定位内存泄漏
JProfiler工具主要用于检查和跟踪系统(限于Java开发的)的性能。
JProfiler可以通过时时的监控系统的内存使用情况,随时监视垃圾回收,线程运行状况等手段,从而很好的监视JVM运行情况及其性能。
1.应用服务器内存长期不合理占用,内存经常处于高位占用,很难回收到低位;
2.应用服务器极为不稳定,几乎每两天重新启动一次,有时甚至每天重新启动一次;
3.应用服务器经常做FullGC(GarbageCollection),而且时间很长,大约需要30-40秒,应用服务器在做FullGC的时候是不响应客户的交易请求的,非常影响系统性能。
因为开发环境和产品环境会有不同,导致该问题发生有时会在产品环境中发生,通常可以使用工具跟踪系统的内存使用情况,在有些个别情况下或许某个时刻确实是使用了大量内存导致outofmemory,这时应继续跟踪看接下来是否会有下降,
如果一直居高不下这肯定就因为程序的原因导致内存泄漏。
通常在知道发生内存泄漏之后,第一步是要弄清楚泄漏了什么数据和哪个类的对象引起了泄漏。
一般说来,一个正常的系统在其运行稳定后其内存的占用量是基本稳定的,不应该是无限制的增长的。
同样,对任何一个类的对象的使用个数也有一个相对稳定的上限,不应该是持续增长的。
根据这样的基本假设,我们持续地观察系统运行时使用的内存的大小和各实例的个数,如果内存的大小持续地增长,则说明系统存在内存泄漏,如果特定类的实例对象个数随时间而增长(就是所谓的“增长率”),则说明这个类的实例可能存在泄漏情况。
另一方面通常发生内存泄漏的第一个迹象是:
在应用程序中出现了OutOfMemoryError。
在这种情况下,需要使用一些开销较低的工具来监控和查找内存泄漏。
虽然OutOfMemoryError也有可能应用程序确实正在使用这么多的内存;对于这种情况则可以增加JVM可用的堆的数量,或者对应用程序进行某种更改,使它使用较少的内存。
但是,在许多情况下,OutOfMemoryError都是内存泄漏的信号。
一种查明方法是不间断地监控GC的活动,确定内存使用量是否随着时间增加。
如果确实如此,就可能发生了内存泄漏。
4.1.2.处理内存泄漏的方法
一旦知道确实发生了内存泄漏,就需要更专业的工具来查明为什么会发生泄漏。
JVM自己是不会告诉您的。
这些专业工具从JVM获得内存系统信息的方法基本上有两种:
JVMTI和字节码技术(bytecodeinstrumentation)。
Java虚拟机工具接口(JavaVirtualMachineToolsInterface,JVMTI)及其前身Java虚拟机监视程序接口(JavaVirtualMachineProfilingInterface,JVMPI)是外部工具与JVM通信并从JVM收集信息的标准化接口。
字节码技术是指使用探测器处理字节码以获得工具所需的信息的技术。
Optimizeit是Borland公司的产品,主要用于协助对软件系统进行代码优化和故障诊断,其中的OptimizeitProfiler主要用于内存泄漏的分析。
Profiler的堆视图就是用来观察系统运行使用的内存大小和各个类的实例分配的个数的。
首先,Profiler会进行趋势分析,找出是哪个类的对象在泄漏。
系统运行长时间后可以得到四个内存快照。
对这四个内存快照进行综合分析,如果每一次快照的内存使用都比上一次有增长,可以认定系统存在内存泄漏,找出在四个快照中实例个数都保持增长的类,这些类可以初步被认定为存在泄漏。
通过数据收集和初步分析,可以得出初步结论:
系统是否存在内存泄漏和哪些对象存在泄漏(被泄漏)。
接下来,看看有哪些其他的类与泄漏的类的对象相关联。
前面已经谈到Java中的内存泄漏就是无用的对象保持,简单地说就是因为编码的错误导致了一条本来不应该存在的引用链的存在(从而导致了被引用的对象无法释放),因此内存泄漏分析的任务就是找出这条多余的引用链,并找到其形成的原因。
查看对象分配到哪里是很有用的。
同时只知道它们如何与其他对象相关联(即哪些对象引用了它们)是不够的,关于它们在何处创建的信息也很有用。
最后,进一步研究单个对象,看看它们是如何互相关联的。
借助于Profiler工具,应用程序中的代码可以在分配时进行动态添加,以创建堆栈跟踪。
也有可以对系统中所有对象分配进行动态的堆栈跟踪。
这些堆栈跟踪可以在工具中进行累积和分析。
对每个被泄漏的实例对象,必然存在一条从某个牵引对象出发到达该对象的引用链。
处于堆栈空间的牵引对象在被从栈中弹出后就失去其牵引的能力,变为非牵引对象。
因此,在长时间的运行后,被泄露的对象基本上都是被作为类的静态变量的牵引对象牵引。
4.2.代码进行走查和分析
4.2.1.不健壮代码的特征及解决办法
1、尽早释放无用对象的引用。
好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。
对于仍然有指针指向的实例,jvm就不会回收该资源,因为垃圾回收会将值为null的对象作为垃圾,提高GC回收机制效率;
2、我们的程序里不可避免大量使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域;
Stringstr="aaa";
Stringstr2="bbb";
Stringstr3=str+str2;//假如执行此次之后str,str2以后再不被调用,那它就会被放在内存中等待Java的gc去回收,程序内过多的出现这样的情况就会报上面的那个错误,建议在使用字符串时能使用StringBuffer就不要用String,这样可以省不少开销;
3、尽量少用静态变量,因为静态变量是全局的,GC不会回收的;
4、避免集中创建对象尤其是大对象,JVM会突然需要大量内存,这时必然会触发GC优化系统内存环境;显示的声明数组空间,而且申请数量还极大。
5、尽量运用对象池技术以提高系统性能;生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。
6、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。
可以适当的使用hashtable,vector创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃
7、一般都是发生在开启大型文件或跟数据库一次拿了太多的数据,造成OutOfMemoryError的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
4.3.几种典型的Java内存泄漏
我们知道了在Java中确实会存在内存泄漏,那么就让我们看一看几种典型的泄漏,并找出他们发生的原因和解决方法。
4.3.1.全局集合
在大型应用程序中存在各种各样的全局数据仓库是很普遍的,比如一个JNDI-tree或者一个sessiontable。
在这些情况下,必须注意管理储存库的大小。
必须有某种机制从储存库中移除不再需要的数据。
通常有很多不同的解决形式,其中最常用的是一种周期运行的清除作业。
这个作业会验证仓库中的数据然后清除一切不需要的数据。
另一种管理储存库的方法是使用反向链接(referrer)计数。
然后集合负责统计集合中每个入口的反向链接的数目。
这要求反向链接告诉集合何时会退出入口。
当反向链接数目为零时,该元素就可以从集合中移除了。
4.3.2.缓存
缓存一种用来快速查找已经执行过的操作结果的数据结构。
因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操作结果进行缓存,以便在下次调用该操作时使用缓存的数据。
缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,因此需要将所使用的内存容量与检索数据的速度加以平衡。
常用的解决途径是使用java.lang.ref.SoftReference类坚持将对象放入缓存。
这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这些对象的引用。
4.3.3.类装载器
Java类装载器的使用为内存泄漏提供了许多可乘之机。
一般来说类装载器都具有复杂结构,因为类装载器不仅仅是只与"常规"对象引用有关,同时也和对象内部的引用有关。
比如数据变量,方法和各种类。
这意味着只要存在对数据变量,方法,各种类和对象的类装载器,那么类装载器将驻留在JVM中。
既然类装载器可以同很多的类关联,同时也可以和静态数据变量关联,那么相当多的内存就可能发生泄漏。
5.案例
这里有两个案例想定供大家警戒。
5.1jspsmartUpload文件上传
使用jspsmartUpload作文件上传,现在运行过程中经常出现java.outofMemoryError的错误,用top命令看看进程使用情况,发现内存不足2M,花了很长时间,发现是jspsmartupload的问题。
把jspsmartupload组件的源码文件(class文件)反编译成Java文件,如梦方醒:
1.m_totalBytes = m_request.getContentLength();
2.m_binArray = new byte[m_totalBytes];
问题原因是totalBytes这个变量得到的数极大,导致该数组分配了很多内存空间,而且该数组不能及时释放。
解决办法只能换一种更合适的办法,至少是不会引发outofMemoryError的方式解决。
变量m_totalBytes表示用户上传的文件的总长度,这是一个很大的数。
如果用这样大的数去声明一个byte数组,并给数组的每个元素分配内存空间,而且m_binArray数组不能马上被释放,JVM的垃圾回收确实有问题,导致的结果就是内存溢出。
jspsmartUpload为什么要这样做,有他的原因,根据RFC1867的http上传标准,得到一个文件流,并不知道文件流的长度。
设计者如果想文件的长度,只有操作servletinputstream一次才知道,因为任何流都不知道大小。
只有知道文件长度了,才可以限制用户上传文件的长度。
为了省去这个麻烦,jspsmartUpload设计者直接在内存中打开文件,判断长度是否符合标准,符合就写到服务器的硬盘。
这样产生内存溢出,这只是我的一个猜测而已。
所以编程的时候,不要在内存中申请大的空间,因为web服务器的内存有限,并且尽可能的使用流操作,例如
1.byte[] mFileBody = new byte[512];
2. Blob vField= rs.getBlob("FileBody");
3. InputStream instream=vField.getBinaryStream();
4. FileOutputStream fos=new FileOutputStream(saveFilePath+CFILENAME);
5. int b;
6. while( (b =instream.read(mFileBody)) !
= -1){
7. fos.write(mFileBody,0,b);
8. }
9. fos.close();
10. instream.close();
5.2Swing子窗体
曾经在刚入行的时候做过一个小的swing程序,用到了javaSE,swing,Thread等东东,当初经验少也没有做过严格的性能测试,布到生产环境用了一段时间后发现那个小程序有时候会抛java.lang.OutofMemoryError异常,就是java的内存溢出。
当时也上网查了不少资料,试过一些办法,代码也稍微做了些优化,但是有一个问题我始终是找不到解决的方案-不知为什么子窗体关闭后java的垃圾回收机制无法回收其资源,因为这个Java程序可能要经常开关一些子窗体,那么这些子窗体关闭后无法释放资源就造成了Java程序OutOfMemoryError的潜在的隐患!
最近无意间在网上看到了一个监控java程序内存使用的工具-JProbe,马上回想起那个有关内存溢出的难题,于是我就下载了JProbe8.0.0希望从分析内存入手找到我要的答案。
软件下载安装后,在安装目录里详尽的用户指南(懂点软件和英语的人很快就能上手),主要是两个步骤:
1.用JProbeConfig工具根据提示生成J2SE或者J2EE程序的控制脚本(一个.jpl文件和一个.bat文件),在命令行里进入.bat文件所在的目录,然后执行该批处理让要监控的java程序跑起来
2.运行JProbeConsole工具,点击“AttachtoSession...”按钮,弹出java程序的内存实时监控图表“RuntimeSummary”,我们主要是看“Data”卡片里的内容(注意:
第一次使用该软件可能会遇到一些小问题:
比如发布为jar包的程序如果运行时会去读配置文件,从控制脚本启动的话,可能会发生配置文件找不到的异常,解决办法是:
不要打jar包,直接就用文件夹发布;还有可能因为一些杀毒软件的网络防火墙导致JProbe无法连接到控制脚本的session,造成监控图表打不开,解决办法是:
取消防火墙对于JProbe访问网络的限制)
实时监控图表“RuntimeSummary”如下图所示:
可以设置要监视的包或者类,然后点击“RefreshRuntimeData”按钮刷新这些对象占用内存的情况,当你觉得某个类比较可疑的话,你可以在不断的使用程序的过程中监视它的生命周期,看看它是否像预期的那样在结束了生命周期后占用的内存就被释放。
众所周知:
java的内存回收是自动进行的,无需程序员干预,我们称其为垃圾回收,这个垃圾回收可能是不定期的,就是当程序占用内存资源比较少的情况下可能jvm的垃圾回收频率就比较低;反之,java程序消耗内存资源比较多的情况下,垃圾回收的频率和力度就比较高,这种垃圾回收的不确定性很可能会影响我们的判断,但我们可以点击JProbe监控界面右上方的“RequestaGarbageCollection”(像一个垃圾桶的图标)按钮来向jvm发出垃圾回收的请求,等几秒后再去点击“RefreshRuntimeData”,这个时候如果那个预期应该已经销毁的对象的类名还是没有从监控界面下方的class列中消失或者其对象数量没有减少的话(请多试几次,中间可以夹杂些其他增加程序内存使用的操作以确保jvm确实执行了垃圾回收),那恭喜你!
90%的可能性你已经找到了程序的某个缺陷
这个查找元凶的过程可能是相当耗时的,是对程序员的耐心的挑战。
我熬了一下午一晚上,功夫不负有心人,基本上把我那个小程序的所有内存溢出的漏洞都找到并补上了。
事实告诉我之前那个子窗体关闭后资源无法释放的根本原因是:
子窗体虽然调用了dispose()方法,但是子窗体对象的引用一直都在:
或者是被静态HashMap引用、或者是它的内部子线程类没有释放、或者是它的某个事件监听类没有释放(借用JProbe的火眼金睛一检验,发现问题真是一大堆啊!
),so.我们要彻底释放某个对象占用资源的关键在于找到并释放所有对它的引用!
下面是我解决具体问题的一些经验:
程序中造成内存溢出可能性最大的是HashMap,Hashtable等等集合类,尤其是静态的,更是要慎之又慎!
!
!
它们引用的对象可能你感觉已经销毁了,其实很可能你忘记remove键值,而如果这些集合对象还是静态的挂