转自(http://www.myexception.cn/ruby-rails/903889.html)
使用jacob组件造成的内存溢出解决方案(java.lang.OutOfMemoryError: Java heap space)
都说内存泄漏是C++的通病,内存溢出是Java的硬伤,这个头疼的问题算是让我给碰到了。我在做的这个功能涉及到修改word文档,因为微软没有公开word源代码,所以直接用java流来读取word的后果是读出来的会是乱码,经过查资料得知可以使用poi和jacob来操作word,jacob使用起来相对poi要方便很多,因此我选择了jacob,Jacob 是Java-COM Bridge的缩写,它在Java与微软的COM组件之间构建一座桥梁。使用Jacob自带的DLL动态链接库,并通过JNI(Java Native Interface Java本地调用)的方式实现了在Java平台上对COM程序的调用。因为dll文件不能在linux上运行,而客户端只和linux交互,所以还需要一个windows服务器,这两个服务器不断的互相下载word,下载的频繁度最高连续达到十万次,以下是服务器之间的交互图:
当功能实现了之后进行了一下测试,结果内存溢出了,于是就开始连查带改弄了半个月,检查打开的流有没有关闭,有没有大量使用静态变量,有没有大量使用String进行字符串拼接,遗憾的是没有找出问题在哪里(说明我写的代码质量还是不错的),也试图增加jvm内存,但增加jvm内存只能治标而不能治本,不是可靠的办法,经过大量查阅资料,得知com的线程回收不由java垃圾回收器进行处理,因此,每new一次jacob提供的类就要分配一定大小的内存给该操作,new出来的这个com对象在使用结束之后产生的垃圾java是无法回收的,new出来的对象越来越多,内存溢出就不可避免了,即使增加jvm内存也只是暂时的,迟早这些对象会把内存用完。既然java不能回收这些垃圾,那么com组件也应该提供了回收垃圾的方法,最后得知是ComThread.InitSTA()和ComThread.Release()方法,这两个方法其实就是初始化一个线程和结束这个线程,在创建com对象的时候初始化一个线程来运行这个对象,这个对象使用结束之后再结束线程,问题就这样得到解决了,程序连续运行一两天内存一直很平稳,弄了快一个月的问题终于解决了,以下是全部代码:
/** * @fileName MSWordManager.java * @description 该类用于查找word文档指定位置并将图片插入 * @date 2011-10-21 * @time * @author wst */ public class MSWordManager { private Logger log = Logger.getLogger(MSWordManager.class); // word文档 private Dispatch doc; // word运行程序对象 private ActiveXComponent word; // 所有word文档集合 private Dispatch documents; // 选定的范围或插入点 private Dispatch selection; public static int instanceSize=3;//一个线程存放的MSWordManager数量 public MSWordManager(int index) { if (word == null) { word = new ActiveXComponent("Word.Application"); //为true表示word应用程序可见 word.setProperty("Visible", new Variant(false)); } if (documents == null){ documents = word.getProperty("Documents").toDispatch(); } if(index==0){ ComThread.InitSTA();//初始化一个线程并放入内存中等待调用 } } /** * 打开一个已经存在的文档 * @param docPath 要打开的文档 * @param key 文本框的内容,根据该key获取文本框当前位置 * @date 2011-12-9 * @author wst */ public void openDocumentAndGetSelection(String docPath, String key) { try{ closeDocument(); // 打开文档 doc = Dispatch.call(documents, "Open", docPath).toDispatch(); // shapes集合 Dispatch shapes = Dispatch.get(doc, "Shapes").toDispatch(); // shape的个数 String Count = Dispatch.get(shapes, "Count").toString(); for (int i = 1; i <= Integer.parseInt(Count); i++) { // 取得一个shape Dispatch shape = Dispatch.call(shapes, "Item", new Variant(i)).toDispatch(); // 从一个shape里面获取到文本框 Dispatch textframe = Dispatch.get(shape, "TextFrame").toDispatch(); boolean hasText = Dispatch.call(textframe, "HasText").toBoolean(); if (hasText) { // 获取该文本框对象 Dispatch TextRange = Dispatch.get(textframe, "TextRange").toDispatch(); // 获取文本框中的字符串 String str = Dispatch.get(TextRange, "Text").toString(); //获取指定字符key所在的文本框的位置 if (str != null && !str.equals("") && str.indexOf(key) > -1) { //当前文本框的位置 selection = Dispatch.get(textframe, "TextRange").toDispatch(); // 情况文本框内容 Dispatch.put(selection, "Text", ""); break; } } } }catch(Exception e){ log.error(e); return; } } /** * 在当前位置插入图片 * @param imagePath 产生图片的路径 * @return 成功:true;失败:false */ public boolean insertImage(String imagePath) { try{ Dispatch.call(Dispatch.get(selection, "InLineShapes").toDispatch(),"AddPicture", imagePath); }catch(Exception e){ log.error(e); return false; } return true; } //关闭文档 public void closeDocument() { if (doc != null) { Dispatch.call(doc, "Close"); doc = null; } } //关闭全部应用 public void close(int index) { if (word != null) { Dispatch.call(word, "Quit"); word = null; } selection = null; documents = null; if(index==instanceSize){ //释放占用的内存空间,因为com的线程回收不由java的垃圾回收器处理 ComThread.Release(); } } }
问题解决了,虽然写的java程序没有什么问题,但是也学习到了一些如何防止内存溢出的知识,下面来看看我在网络找到的几种常见的内存溢出以及如何检测出内存溢出和出来办法。
一、 几种典型的内存泄漏
我们知道了在Java中确实会存在内存泄漏,那么就让我们看一看几种典型的泄漏,并找出他们发生的原因和解决方法。
1 全局集合
在大型应用程序中存在各种各样的全局数据仓库是很普遍的,比如一个JNDI-tree或者一个session table。在这些情况下,必须注意管理储存库的大小。必须有某种机制从储存库中移除不再需要的数据。
通常有很多不同的解决形式,其中最常用的是一种周期运行的清除作业。这个作业会验证仓库中的数据然后清除一切不需要的数据。
另一种管理储存库的方法是使用反向链接(referrer)计数。然后集合负责统计集合中每个入口的反向链接的数目。这要求反向链接告诉集合何时会退出入口。当反向链接数目为零时,该元素就可以从集合中移除了。
2 缓存
缓存一种用来快速查找已经执行过的操作结果的数据结构。因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操作结果进行缓存,以便在下次调用该操作时使用缓存的数据。缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,因此需要将所使用的内存容量与检索数据的速度加以平衡。
常用的解决途径是使用java.lang.ref.SoftReference类坚持将对象放入缓存。这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这些对象的引用。
3 类装载器
Java类装载器的使用为内存泄漏提供了许多可乘之机。一般来说类装载器都具有复杂结构,因为类装载器不仅仅是只与"常规"对象引用有关,同时也和对象内部的引用有关。比如数据变量,方法和各种类。这意味着只要存在对数据变量,方法,各种类和对象的类装载器,那么类装载器将驻留在JVM中。既然类装载器可以同很多的类关联,同时也可以和静态数据变量关联,那么相当多的内存就可能发生泄漏。二、 如何检测和处理内存泄漏
如何查找引起内存泄漏的原因一般有两个步骤:第一是安排有经验的编程人员对代码进行走查和分析,找出内存泄漏发生的位置;第二是使用专门的内存泄漏测试工具进行测试。
第一个步骤在代码走查的工作中,可以安排对系统业务和开发语言工具比较熟悉的开发人员对应用的代码进行了交叉走查,尽量找出代码中存在的数据库连接声明和结果集未关闭、代码冗余等故障代码。
第二个步骤就是检测Java的内存泄漏。在这里我们通常使用一些工具来检查Java程序的内存泄漏问题。市场上已有几种专业检查Java内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。
1 检测内存泄漏的存在
这里我们将简单介绍我们在使用Optimizeit检查的过程。通常在知道发生内存泄漏之后,第一步是要弄清楚泄漏了什么数据和哪个类的对象引起了泄漏。
一般说来,一个正常的系统在其运行稳定后其内存的占用量是基本稳定的,不应该是无限制的增长的。同样,对任何一个类的对象的使用个数也有一个相对稳定的上限,不应该是持续增长的。根据这样的基本假设,我们持续地观察系统运行时使用的内存的大小和各实例的个数,如果内存的大小持续地增长,则说明系统存在内存泄漏,如果特定类的实例对象个数随时间而增长(就是所谓的“增长率”),则说明这个类的实例可能存在泄漏情况。
另一方面通常发生内存泄漏的第一个迹象是:在应用程序中出现了OutOfMemoryError。在这种情况下,需要使用一些开销较低的工具来监控和查找内存泄漏。虽然OutOfMemoryError也有可能应用程序确实正在使用这么多的内存;对于这种情况则可以增加JVM可用的堆的数量,或者对应用程序进行某种更改,使它使用较少的内存。
但是,在许多情况下,OutOfMemoryError都是内存泄漏的信号。一种查明方法是不间断地监控GC的活动,确定内存使用量是否随着时间增加。如果确实如此,就可能发生了内存泄漏。2 处理内存泄漏的方法
一旦知道确实发生了内存泄漏,就需要更专业的工具来查明为什么会发生泄漏。JVM自己是不会告诉您的。这些专业工具从JVM获得内存系统信息的方法基本上有两种:JVMTI和字节码技术(byte code instrumentation)。Java虚拟机工具接口(Java Virtual Machine Tools Interface,JVMTI)及其前身Java虚拟机监视程序接口(Java Virtual Machine Profiling Interface,JVMPI)是外部工具与JVM通信并从JVM收集信息的标准化接口。字节码技术是指使用探测器处理字节码以获得工具所需的信息的技术。
Optimizeit是Borland公司的产品,主要用于协助对软件系统进行代码优化和故障诊断,其中的Optimizeit Profiler主要用于内存泄漏的分析。Profiler的堆视图就是用来观察系统运行使用的内存大小和各个类的实例分配的个数的。
首先,Profiler会进行趋势分析,找出是哪个类的对象在泄漏。系统运行长时间后可以得到四个内存快照。对这四个内存快照进行综合分析,如果每一次快照的内存使用都比上一次有增长,可以认定系统存在内存泄漏,找出在四个快照中实例个数都保持增长的类,这些类可以初步被认定为存在泄漏。通过数据收集和初步分析,可以得出初步结论:系统是否存在内存泄漏和哪些对象存在泄漏(被泄漏)。
接下来,看看有哪些其他的类与泄漏的类的对象相关联。前面已经谈到Java中的内存泄漏就是无用的对象保持,简单地说就是因为编码的错误导致了一条本来不应该存在的引用链的存在(从而导致了被引用的对象无法释放),因此内存泄漏分析的任务就是找出这条多余的引用链,并找到其形成的原因。查看对象分配到哪里是很有用的。同时只知道它们如何与其他对象相关联(即哪些对象引用了它们)是不够的,关于它们在何处创建的信息也很有用。
最后,进一步研究单个对象,看看它们是如何互相关联的。借助于Profiler工具,应用程序中的代码可以在分配时进行动态添加,以创建堆栈跟踪。也有可以对系统中所有对象分配进行动态的堆栈跟踪。这些堆栈跟踪可以在工具中进行累积和分析。对每个被泄漏的实例对象,必然存在一条从某个牵引对象出发到达该对象的引用链。处于堆栈空间的牵引对象在被从栈中弹出后就失去其牵引的能力,变为非牵引对象。因此,在长时间的运行后,被泄露的对象基本上都是被作为类的静态变量的牵引对象牵引。
总而言之, Java虽然有自动回收管理内存的功能,但内存泄漏也是不容忽视,它往往是破坏系统稳定性的重要因素。