为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

Java_内存溢出错误处理

2013-03-21 44页 doc 663KB 26阅读

用户头像

is_486830

暂无简介

举报
Java_内存溢出错误处理使用技巧:Java EE 性能问题解决手册 这篇文章,是PRO JAVA EE 5 Performance Management and Optimization 的一个章节,作者Steven Haines分享了他在调优企业级JAVA应用时所遇到的常见问题。    Java EE(Java企业开发平台)应用程序,无论应用程序服务器如何部署,所面对的一系列问题大致相同。作为一个JAVAEE问题解决专家,我曾经面对过众多 的环境同时也写了不少常见问题的观察报告。在这方面,我觉得我很象一个汽车修理工人:你告诉修理工人发动机有声音...
Java_内存溢出错误处理
使用技巧:Java EE 性能问题解决手册 这篇文章,是PRO JAVA EE 5 Performance Management and Optimization 的一个章节,作者Steven Haines分享了他在调优企业级JAVA应用时所遇到的常见问题。    Java EE(Java企业开发平台)应用程序,无论应用程序服务器如何部署,所面对的一系列问题大致相同。作为一个JAVAEE问题解决专家,我曾经面对过众多 的环境同时也写了不少常见问题的观察报告。在这方面,我觉得我很象一个汽车修理工人:你告诉修理工人发动机有声音,他就会询问你一系列的问题,帮你回忆发 动机运行的情形。从这些信息中,他寻找到可能引起问题的原因。   众多解决问题的方法思路基本相同,第一天我同要解决问题的客户接触,接 触的时候,我会寻找已经出现的问题以及造成的负面的影响。了解应用程序的体系结构和问题表现出的症状,这些工作很够很大程度上提高我解决问题的几率。在这 一节,我分享我在这个领域遇过的常见问题和他们的症状。希望这篇文章能成为你JAVAEE的故障检测手册。   内存溢出错误   最常见的折磨着企业级应用程序的错误是让人恐惧的outofmemoryError(内存溢出错误)   这个错误引起下面这些典型的症状:   ----应用服务器崩溃   ----性能下降   ----一个看起来好像无法结束的死循环在重复不断的执行垃圾收集,它会导致程序停止运行,并且经常导致应用服务器崩溃   不管症状是什么,如果你想让程序恢复正常运行,你一般都需要重新启动应用服务器。   引发out-of-memory 错误的原因    在你打算解决out-of-memory 错误之前,首先了解为什么会引发这个错误对你有很大的帮助。如果JVM里运行的程序, 它的内存堆和持久存储区域的都满了,这个时候程序还想创建对象实例的话,垃圾收集器就会启动,试图释放足够的内存来创建这个对象。这个时候如果垃圾收集器 没有能力释放出足够的内存,它就会抛出OutOfMemoryError内存溢出错误。   Out-of-memory错误一般是 JAVA内存泄漏引起的。回忆上面所讨论的内容,内存泄漏的原因是一个对象虽然不被使用了,但是依然还有对象引用他。当一个对象不再被使用时,但是依然有 一个或多个对象引用这个对象,因此垃圾收集器就不会释放它所占据的内存。这块内存就被占用了,堆中也就少了块可用的空间。在WEB REQUESTS中这种类型的的内存泄漏很典型,一两个内存对象的泄漏可能不会导致程序服务器的崩溃,但是10000或者20000个就可能会导致这个恶 果。而且,大多数这些泄漏的对象并不是象DOUBLE或者INTEGER这样的简单对象,而可能是存在于堆中一系列相关的对象。例如,你可能在不经意间引 用了一个Person对象,但是这个对象包含一个Profile对象,此对象还包含了许多拥有一系列数据的PerformanceReview对象。这样 不只是丢失了那个Person对象所占据的100 bytes的内存,你丢失了这一系列相关对象所占据的内存空间,可能是高达500KB甚至更多。   为了寻找这个问题的真正根源,你需要判断是内存泄漏还是以OutOfMemoryError形式出现的其他一些故障。我使用以下2种方法来判断:   ----深入分析内存数据   ----观察堆的增长方式   不同JVM(JAVA虚拟机)的调整程序的运作方式是不相同的,例如SUN和IBM的JVM,但都有相同的的地方。   SUN JVM的内存管理方式   SUN的JVM是类似人类家族,也就是在一个地方创建对象,在它长期占据空间之前给它多次死亡的机会。   SUN JVM会划分为:   1 年轻的一代(Young generation),包括EDEN和2个幸存者空间(出发地和目的地the From space and the To space)   2 老一代(Old generation)   3 永久的一代(Permanent generation) 图1 解释了SUN 堆的家族和空间的详细分类 INCLUDEPICTURE "http://java.ccidnet.com/col/attachment/2006/12/948527.jpg" \* MERGEFORMATINET 对 象在EDEN出生就是被创建,当EDEN满了的时候,垃圾收集器就把所有在EDEN中的对象扫描一次,把所有有效的对象拷贝到第一个幸存者空间,同时把无 效的对象所占用的空间释放。当EDEN再次变满了的时候,就启动移动程序把EDEN中有效的对象拷贝到第二个幸存者空间,同时,也将第一个幸存者空间中的 有效对象拷贝到第二个幸存者空间。如果填充到第二个生存者空间中的有效对象被第一个生存者空间或EDEN中的对象引用,那么这些对象就是长期存在的(也就 是说,他们被拷贝到老一代)。若垃圾收集器依据这种小幅度的调整收集(minor collection)不能找出足够的空间,就是象这样的拷贝收集(copy collection),就运行大幅度的收集,就是让所有的东西停止(stop-the-world collection)。运行这个大幅度的调整收集时,垃圾收集器就停止所有在堆中运行的线程并执行清除动作(mark-and-sweep collection),把新一代空间释放空并准备重启程序。   图2和图3展示的是了小幅度收集如何运行 图2。对象在EDEN被创建一直到这个空间变满。 图3。处理的顺序十分重要:垃圾收集器首先扫描EDEN和生存者空间,这就保证了占据空间的对象有足够的机会证明自己是有效的。 图4展示了一个小幅度调整是如何运行的 图4:当垃圾收集器释放所有的无效的对象并把有效的对象移动到一个更紧凑整齐的新空间,它将EDEN和生存者空间清空。   以上就是SUN实现的垃圾收集器机制,你可以看出在老一代中的对象会被大幅度调整器收集清除。长生命周期的对象的清除花费的代价很高,因此如果你希望生命周期短的对象在占据空间前及时的死亡,就需要一个主垃圾收集器去回收他们的内存。    上面所讲解的东西是为了更好的帮助我们识别出内存泄漏。当JAVA中的一个对象包含了一个并不想要的一个指向其他对象的引用的时候,内存就会泄漏,这个 引用阻止了垃圾收集器去回收它所占据的内存。采用这种机制的SUN 虚拟机,对象不会被丢弃,而是利用自己特有的方法把他们从乐园和幸存者空间移动到老一代地区。因此,在一个基于多用户的WEB环境,如果许多请求造成了泄 漏,你就会发现老一代的增长。   图5显示了那些潜在可能造成泄漏的对象:主收集器收集后遗留下来占据空间的对象会越来越多。不是所有的占据空间的对象都造成内存泄漏,但是造成内存泄漏的对象最终都占据者空间。如果内存泄漏的确存在,这些造成泄漏的对象就会不断的占据空间,直至造成内存溢出。   因此,我们需要去跟踪垃圾收集器在处理老一代中的运行:每次垃圾收集器大幅度收集运行时,有多少内存被释放?老一代内容是不是按一定的原理来增长?  图5。阴影表示在经过大幅度的收集后幸存下来的对象,这些对象是潜在可能引发内存泄漏的对象    一部分这些相关的信息是可以通过跟踪API得到,更详细的信息通过详细的垃圾收集器的日志也可以看到。和所有的跟踪技术一样,日值详细的程度影响着 JVM的性能,你想得到的信息越详细,付出的代价也就越高。为了能够判断内存是否泄漏,我使用了能够显示辈分之间所有的不同的较权威的技术来显示他们的区 别,并以此来得到结果。SUN 的日志报告提供的信息比这个详细的程度超过5%,我的很多客户都一直使用那些设置来保证他们管理和调整垃圾收集器。下面的这个设置能够给你提供足够的分析 数据:   –verbose:gc –xloggc:gc.log –XX:+PrintGCDetails –XX:+PrintGCTimeStamps   明确发现在整个堆中存在有潜在可能泄漏内存的情况,用老一代增长的速率才比较有说服力。切记调查不能决定些什么:为了能够最终确定你内存泄漏,你需要离线在内存模拟器中运行你的应用程序。 IBM JVM内存管理模式    IBM的JVM的机制有一点不同。它不是运行在一个巨大的继承HEAP中,它仅在一个单一的地区维护了所有的对象同时随着堆的增长来释放内存。这个堆是 这样运行的:在一开始运行的时候,它会很小,随着对象实例不断的填充,在需要执行垃圾收集的地方清除掉无效的对象同时把所有有效的对象紧凑的放置到堆的底 部。因此你可能猜测到了,如果想寻找可能发生的内存泄漏应该观察堆中所有的动作,堆的使用率是在提高?   如何分析内存泄漏   内存泄漏非常难确定,如果你能够确定是请求导致的,那你的工作就非常简单了。把你的程序放入到运行环境中,并在内存模拟器中运行,按下面的步骤来:   1. 在内存模拟器中运行你的应用程序   2. 执行使用(制造请求)以便让程序在内存中装载请求所需要的所有的对象,这可以为以后详细的分析排除不必要的干扰   3. 在执行使用方案前对堆进行拍照以便捕获其中所有运行的对象。   4. 再次运行使用方案。   5. 再次拍照,来捕获使用方案运行之后堆中所有对象的状态。   6. 比较这2个快照,找出执行使用方案后本不应该出现在堆中的对象。    这个时候,你需要去和开发者交流,告诉他你所碰到的棘手的请求,他们可以判断究竟是对象泄漏还是为了某个目的特地让对象保留下来的。如果执行完后并没有 发现内存泄漏的情况,我一般会转到步骤4再进行多次类似的跟踪。比如,我可能会将我的请求反复运行17次,希望我的泄漏分析能够得到17个情况(或更 多)。这个方法不一定总有用,但如果是因为请求引起的对象泄漏的话,就会有很大的帮助。   如果你无法明确的判断泄漏是因为请求引发的,你有2个选择:   1. 模拟每一个被怀疑的请求直至发现内存泄漏   2. 存配置一个内存性能跟踪工具    第一个选项在小应用程序中是确实可用的或者你非常走运的解决了问题,但对大型应用程序不太有用。如果你有跟踪工具的话第二个选择是比较有用的。这些工具 利用字节流工具跟踪对象的创建和销毁的数量,他们可以报告特定类中的对象的数量状态,例如把Collections类作为特定的请求。例如,一个跟踪工具 可以跟踪/action/login.do请求,并在它完成后将其中的100个对象放入HASHMAP中。这个报告并不能告诉你造成泄漏的是代码还是某个 对象,而是告诉你在内存模拟器中应该留意那些类型的请求。把程序服务器放到产品环境中并不会使他们变敏感,而是跟踪性能的工具可以使你的工作变的更简单 化。   虚假内存泄漏   少数的一些问题看起来是内存泄漏实际上并非如此。   我将这些情况称为假泄漏,表现在下面几种情况:   1. 分析过早   2. Session泄漏   3. 异常的持久区域   这章节对这些假泄漏都进行了调查,描述了如何去判断这些情况以及如何处理.   不要过早分析   为了在寻找内存泄漏的时候尽量减少出现判断错误的可能性,你应当在适当的时候分析堆。危险是:一些生命周期长的对象需要装载到堆中,因此在堆达到稳定状态且包含了核心对象之前具有很大的欺骗性。在分析堆之前,应该让应用程序达到稳定状态。   为了判断是否过早的对堆进行分析,持续2个小时对跟踪到的分析快照进行分析,看堆的使用率是上升还是下降。如果是下降,保存这个时候的内存记录。如果是上升,这个时候就需要分析内存中的SESSION了。 发生泄漏的session   WEB请求经常导致内存泄漏,在一个WEB请求中,对象会被限制存储在有限的几个区域。这些区域就是:   1. 页面区域   2. 请求区域   3. 上下文区域   4. 应用程序区域   5. 静态变量   6. 长生命周期的变量,例如SERVLET    当实现一些JSP(JAVASERVER页面)时,在页面上声明的变量在页面结束的时候就被释放,这些变量仅仅在这个单独的页面存在时存在。WEB服务 器会向应用程序服务器传送一系列参数和属性,也就是在SERVLET和JSP之间传输HttpServletRequest中的对象。你的动态页面依靠 HttpServletRequest在不同的组件之间传输信息,但当请求完成或者socket结束的时候,SERVLET控制器会释放所有在 HttpServletRequest 中的对象。这些对象仅在他们的请求的生命周期内存在。   HTTP是无状态的,这意味着客户向服 务器发送一个请求,服务器回应这个请求,这个传递就完成了,就是会话结束了。我们应该感激WEB页面帮我们做的日志,这样我们就能向购物车放置东西,并去 检查它,服务器能够定义一个跨越多请求的扩展对话。属性和参数被放在各自用户的HttpSession对象中,并通过它让程序的SERVLET和JSP交 流。利用这种办法,页面存储你的信息并把他们添加到HttpSession中,因此你可以用购物车购买东西,并检查商品和使用信用卡付帐。作为一个无状态 的,它总是客户端发起连接请求,服务器需要知道一个会话存在多长时间,到时候就应该释放这个用户的数据。超过这个会话的最长时间就是会话超时,他们在 程序服务器中设置。除非明确的要求释放对象或者这个会话失效,否则在会话超时之前会话中的对象会一直存在。   正如session是为每 个用户管理对象一样,ServletContext为整个程序管理对象。ServletContext的有效范围是整个程序,因此你可以利用 Servlet中的ServletContext或者JSP应用程序对象在所有的Servlet和JSP之间让在这个程序中的所有用户共享数据。 ServletContext是最主要的存放程序配置信息和缓存程序数据的地方,例如JNDI的信息。   如果数据不是存储这个四个地方(页面范围,请求范围,会话范围,程序范围)那就可能存储在下面的对象中:   1. 静态变量   2. 长生命周期的类变量    每个类的静态变量被JVM(JAVA虚拟机)所控制,他们存在与否和类是否已经被初始化无关。一个类的所有实例共用一个存储静态变量的地方,因此在任何 一个实例中修改静态变量会影响这个类的其他实例。因此,如果一个程序在静态变量中存放了一个对象,如果这个变量生命周期没有到,那么这个对象就不会被 JVM释放。这些静态对象是造成内存泄漏的主要原因。   最后,对象能够被放到内部数据类型或者长生命周期类中的成员变量中,例如 SERVLET。当一个SERVLET被创建并且被装载到内存,它在内存中仅有一个实例,采用多线程去访问这个SERVLET实例。如果在INIT()方 法中装载配置信息,将他存储于类变量中,那么当需要维护的时候就可以随时读出这些信息,这样所有的对象就用相同的配置。我常碰到的一个问题就是利用 SERVLET类变量去存储象页面缓存这样的信息。在他们自己内部本身存贮这些缓存配置是个不错的选择,但存贮在SERVLET中是最糟糕的情况。如果你 需要使用缓存,你最好使用第三方控制插件,例如 TANGOSOL的COHERENCE。   当在页面或者请求范围中利用变量存放对象的时候,在他们结束的时候这些对象会自动释放。同样,在SESSION中存放对象的时候,当程序明确说明此SESSION失效的或者会话执行超时的时候,这些对象才会自动被释放。    很多看起来象内存泄漏的情况都是上面的那些会话中的泄漏。一个造成泄漏的会话并不是泄漏了内存而是类似于泄漏,它消耗了内存,但最终这些内存都会被释放 的。如果程序服务器发生内存溢出,判断是内存泄漏还是内存缺乏的最好的方法就是:停止所有向这个服务器所发的请求的对象,等待会话超时,看内存时候会被释 放出来。这虽然不会一定能够达到你要的目的,但是这是最好的分段处理方法,当你装载测试器的时候,你应该先挂断你内容巨大的会话而不是先去寻找内存泄漏。   通常来说,如果你执行了一个很大的会话,你应该尽量去减少它所占用的内存空间,如果可以的话最好能重构程序,以减少session所占据的内存空间。下面2种方法可以降低大会话和内存的冲突:   1. 增大堆的空间以支持你的大会话   2. 缩短会话的超时时间,让它能够快速的失效    一个巨大的堆会导致垃圾回收花费更多的时间,因此这不是一个好解决方法,但总比发生OutofMemoryError强。增加足够的堆空间以使它能够存 储所有应该保存的有效值,也意味着你必须有足够的内存去存储所有访问你站点的用户的有效会话。如果商业规则允许的话最好能缩短会话超时的时间,以减少堆占 用空间的冲突。 总结下,你应该依据合理性和重要性按下面的步骤依次去执行:   1. 重构程序,尽量减少拥有session范围的变量所存储的信息量   2. 鼓励你的客户在他们使用完后,明确的释放会话   3. 缩短超时的时间,以便于让你内存尽快的得到回收   4. 增加你堆空间的大小   无论如何,不要让程序范围级的变量,静态变量,长生命周期的类存储对象,事实上,你需要在内存模拟器中去分析泄漏。   异常的持久空间    容易误解JVM为持久空间分配内存的目的。堆仅仅存储类的实例,但JVM在堆中创建类实例之前,它必须把字节流文件(.class文件)装载到程序内存 中。它利用内存中的字节流在堆中创建类的实例。JVM利用程序的内存来装载字节流文件,这个内存空间称为持久空间。图6显示了持久空间和堆的关系:它存在 于JVM程序中,并不是堆的一部分。 Figure 6. The relationship between the permanent space and the heap   通常,你可能想让你的持久空间足够大以便于它能够装载你程序所有的类,因为很明显,从文件系统中读取类文件比从内存中装载代价高很多。JVM提供了一个参数让你不的程序不卸载已经装载到持久空间中的类文件:   –noclassgc    这个参数选项告诉JVM不要跑到持久空间去执行垃圾收集释放其中已经装载的类文件。这个参数选项很聪明,但是会引起一个问题:当持久空间满了以后依然需 要装载新文件的时候JVM会怎么处理呢?我观测到的资料说明:如果JVM检测到持久空间还需要内存,就会调用主垃圾收集程序。垃圾收集器清除堆,但它并不 会对持久空间进行任何操作,因此它的努力是白费的。于是JVM就再重新检测持久空间,看它是否满,然后再次执行程序,一遍的一遍重复。  我第一次 碰到这种问题的时候,用户抱怨说程序性能很差劲,并且在运行了几次后就出现了问题,可能是内存溢出问题。在我调查了详细的关于堆和程序内存利用的收集器的 记录后,我迅速发觉堆的状态非常正常,但程序确发生了内存溢出。这个用户维持了数千的JSP页面,在装载到内存前把他们都编译成了字节流文件放入持久空 间。他的环境已经造成了持久空间溢出,但是在堆中由于用了 -noclassgc 选项,于是JVM并不去释放类文件来装载新的类文件。于是就导致了内存溢出错误,我把他的持久空间改为512M大小,并去掉了 -noclassgc 参数。   正像图7显示的,当持久空间变满了的时候,就引发垃圾收集,清理了乐园和幸存者空间,但是并不释放持久空间中的一点内存。 Figure 7. Garbage collection behavior when the permanent space becomes full. Click on thumbnail to view full-sized image.   注意    当设置持久空间大小时候,一般考虑128M,除非你的程序有很多的类文件,这个时候,你就可以考虑使用256M大小。如果你想让他能够装载所有的类的时 候,就会导致一个典型的结构错误。设置成512M就足够了,它仅仅是暂时的时间的花费。把持久空间设置成512M大小就象给一个脚痛的人吃止痛药,虽然暂 时缓解了痛,但是脚还是没有好,依然需要医生把痛治疗好,否则只是把问题延迟了而已。   线程池   外界同WEB或程序 服务器连接的主要方法就是向他们发送请求,这些请求被放置到程序的执行次序队列中。和内存最大的冲突就是程序服务器所设置的线程池的大小。线程池的大小就 是程序可以同时处理的请求的数量。如果池太小,请求就需要在队列中等待程序处理,如果太大,CPU就需要花费太多的时间在这些众多的线程之间来回的切换。   每个服务器都有一个SOCKET负责监听。程序把接受到的请求放到待执行队列中,然后将这个请求从队列移动到线程中被程序处理。   图8显示了服务器的处理程序。 Figure 8. 服务器处理请求的次序结构 线程池太小   每当我碰到有人抱怨装载速度的性能随着装载的数量的增加变的越来越糟糕的时候,我会首先检查线程池。特别是,我在看到下面这些信息的时候:   1.线程池的使用   2.很多请求等待处理(在队列中等待处理)    当一个线程池被待处理的请求装满的时候,响应的时间就变的极其糟糕,因为这些在队列中等待处理的请求会消耗很多的额外时间。这个时候,CPU的利用率会 非常低,因为程序服务器没有时间去指挥CPU工作。这个时候,我会按一定幅度增加调节池的大小,并在未处理请求的数量减少前一直监视程序的吞吐量,你需要 一个合理甚至更好的负载量者,一个精确的负载量测试工具可以准确的帮你测试出结果。当你观测吞吐量的时候,如果你发现吞吐量降低了,你就应该把池的大小下 调一个幅度,一直到找到让它保持最大吞吐量的大小为止。   图9显示了连接池太小的情况 Figure但为第二个程序配置的池是100,就有点太大了,因为CPU可能就能应付50个线程。    但是,很多程序并没有在这种情况下动态的去调整的功能。多数情况下是做相同的事,但是应该为他们划分范围。因此,我建议你为一个CPU分配50到75个 左右的线程。对一些程序来说,这个数量可能太少,对另一个些来说可能太多,我刚开始为每个CPU分配50到75个线程,然后根据吞吐量和CPU的性能,并 做适当的调整。   线程池太大   除了线程池数量太小之外的情况外,环境也可能把线程数量配置的过大。当这些环境中的负载量不断增大的时候,CPU的使用率会持续无法降低,就没有什么响应请求的时间了,因为CPU只顾的在众多的线程之间来回的切换跳动,没时间让线程去做他们应该做的事了。    连接池过大的最主要的迹象就是CPU的使用率一直很高。有些时候,垃圾收集也可能导致CPU使用率很高,但是垃圾收集导致的CPU使用率很高和池过大导 致的使用率有一个主要的区别就是:垃圾收集引起的只是短时间的高使用率就象个钉子,而池过大导致的就是一直持续很高呈线性。   这个情况 发生的时候,请求会被放在队列中不被处理,但是不会始终如此,因为请求占用CPU的情况和程序占用的情况造成的后果不同。降低线程池的大小可能会让请求等 待,但是让请求等待总比为了处理请求而让CPU忙不过来的好。让CPU保持持续的高使用率,同时性能不降低,新请求到来的时候放入到队列中,这是最理想的 程序。考虑下面这个很类似的情况:很多高速公里有交通灯来保证车辆进入到拥挤的公里中。在我看来,这些交通灯根本没用,道理很充分。比如你来了,在交通灯 后面的安全线上等待进入到高速公路上。如果所有的车辆都同时涌向公里,我们就动弹不得,但是只要减缓涌向高速公路车辆的速度,交通迟早会畅通。事实上,很 多的大城市都有这样功能,但根本没用,他们真正需要的是一些更多的小路(CPU),涌向高速公路的速度真的降低了,那么交通会变的正常起来。 设置一个饱和的池,然后逐步减少连接池大小,一直到CPU占用率为75%到85%之间,同时用户负载正常。如果等待队列大小实在无法控制,考虑下面2中建议:   1.把你的程序放入代码模拟器运行,调整程序代码   2.增加额外的硬件   如果你的用户负载超过了环境能承受的范围,你应该考虑修正代码减少和CPU的冲突或者增加CPU。   JDBC连接池    很多JAVA EE 程序连接到一个后台数据源,大多数是通过JDBC(JAVA DATABASE CONNECTIVITY)将程序和后台连接起来。由于创建数据库连接的代价很高,程序服务器让在同一个程序服务器实例下的所有程序共享特定数量的一些连 接。如果一个请求需要连接到数据库,但是数据库的连接池无法为这个请求创建一个新连接,这个时候请求就会停下来等待连接池完成自己的操作再给她分配一个连 接。反过来,如果数据库连接池太大程序服务器就会浪费资源,并且程序有可能强迫数据库承受过量的负荷。我们调试的目的就是尽量减少请求的等待时间和饱和的 资源之间之间的冲突,让一个请求在数据库外等待要比强迫数据库好的多。   一个程序服务器如果设置连接的数量不合理就会有下面这些特征:   1.程序运行速度缓慢   2.CPU使用率低   3.数据库连接池使用率非常高   4.线程等待数据库的连接   5.线程使用率很高   6.请求队列中有待处理的请求(潜在的)   7.数据库CPU使用率很低(因为没有足够的请求能够让他繁忙起来)   JDBC prepared statements   和JDBC相关的另一个重要的设置就是:为JDBC使用的statement 所预设的缓存的大小。当你的程序在数据库中运行SQL statement 的时候三下面3个步骤进行:   1.准备   2.执行   3.返回数值   在准备阶段,数据库驱动器让数据库完成队列中的执行。执行的时候,数据库执行语句并返回指向结果的引用。在返回的时候,程序重新描述这些结果并描述出这些被请求的信息。   数据库驱动会这样优化程序:首先,你需要去准备一个statement ,这个statement 它会让数据库做好执行和缓存结果的准备。在此同时,数据库驱动会从缓存中装载已经准备好的statement ,而不用直接连接到数据库。    如果prepared statement 设置太小,数据库驱动器会被迫去查询没有装载进缓存区的statement ,这就会增加额外的连接到数据库的时间。prepared statement 缓存区设置不恰当最主要的症状就是花费大量的时间去连接相同的statement。这段被浪费的时间本来是为了让它去装载后面的调用的。    事情变的稍微复杂了点,缓存prepared statement 是每个statement的基础,就是说在一个statement连接之前都应当缓存起来。这个增加的复杂性就产生了一个冲突:如果你有100个 prepared statement需要去缓存,但你的连接池中有50个数据库连接,这个时候你就需要有存放5000条预备语句的内存。   通过跟踪性能,确定出你程序所执行的不重复的statement 的数量,并从这些statement 中找出哪些条是频繁执行的。   Entity bean(实体BEAN)和stateful session bean的缓冲    无状态(stateless)对象可以被放入到池中共享,但象Entity beans和 stateful session bean这样的有状态的对象就需要被缓存,因为这些bean的每个实例都是不相同的。当你需要一个有状态对象时,你需要明确创建这个对象的特定实例,普通 的实例是不能满足的。类似的,你考虑一个超市类似的情况,你需要个售货员但他叫什么并不重要,任何售货员都可以满足你。也就是,售货员被放入池中共享,因 为你只需要是售货员就可以,而不是一个叫做史缔夫的这个售货员。当你离开超市的时候,你需要带上你的孩子,不是其他人的孩子,而是你自己的。这个时候,孩 子就需要被缓存。 Figure 10. The application requests an object from the cache that is in the cache, so a reference to that object is returned without making a network trip to the database 当你的缓存区太小的时候,缓存的性能就会明显的受到影响。特别是,当一个请求去一个已经满了的缓存区域去请求一个对象的时候,下面的步骤就会执行,这些步骤会在图11中显示:   1. 程序请求一个对象   2. 缓存检测这个对象是否已经存在于缓存中   3. 缓存决定把一个对象开除出缓存(一般采用的算法是遗弃最近使用次数最少的对象)   4. 把这个对象扔出缓存(称为passivated)   5. 把从数据库中装载这个新对象并放入到缓存(称为activated)   6. 把指向这个对象的引用返回给程序  Figure 除非你把超时设置的很短才会出现这种错误。资源回滚就是当一个程序服务器管理内部的资源的时候发生错误。例如,如果你设置你的程序服务器通过一个简单的 SQL语句去测试数据库的连接,但数据库对于程序服务器来说是无法连接的,这个时候任何和这个资源相关的事情都会发生资源回滚。   如果发生非程序回滚,我们应该立刻注意,这个是不小的问题,但是你也需要留意程序回滚发生的频率。很多时候人们对发生的异常很敏感,因此你需要哪些异常对你程序来说才是重要的。   总结   尽管各个程序和他们的环境都各不相同,但是有一些共同的问题困扰着他们。这篇文章的注意力并不是放在程序代码的问题上,因为把注意力放在因为环境的问题而导致的低性能的问题上:   1.内存溢出   2.线程池大小   3.JDBC连接池大小   4.JDBC预先声明语句缓存大小   5.缓存大小   6.池大小   7.执行事务时候的回滚   为了有效的诊断性能的问题,你应该了解什么问题会导致什么样的症状。如果主要是程序的代码导致的恶果那你应该带着问题去寻求负责代码的人寻求帮助,但是如果问题是由环境引起的,那么就要依靠你的操作来解决了。   问题的根源依赖于很多要素,但是一些指示器可以增加一些你处理问题时候的一些信心,依靠他们可以完全排除一些其他的原因。我希望这个文章能对你排解JAVAEE环境问题起到帮助。 编者注:本文改编自发表在VoIP Mase '06 workshop学报(IEEE Catalog,Number 06EX1301)上的“Enabling Java-based VoIP backend platforms through JVM performance tuning”。 摘要   在Voice over IP (VoIP)服务产品中,软件后端平台正日益变得重要。Java和J2EE平台已经发展成为在电信后端平台上设计和实现业务逻辑的重要软件框架。考虑到 Java的流行,有人提出了这样一个问题:基于Java的后端平台能否满足VoIP应用程序的需求?   会话发起协议(Session Initiation Protocol,SIP)是通常用于VoIP的重要信令协议。SIP Servlet技术被开发出来以构建基于Java的VoIP服务。本文将评估BEA Weblogic SIP Servlet实现的性能,介绍评估过程和所得到的结果,还将研究对Java虚拟机(JVM)进行调优所带来的影响。此外,根据所得到的结果,本文还将证 明对于一般应用程序,以及更明确地对于VoIP相关应用程序,一些用于调整JVM以优化垃圾收集程序的技术的效果。 简介   电信应用程序通常有非常严格的吞吐量(例如,一个软交换产品(soft switch)每秒处理的VoIP呼叫连接数)和延迟需求(例如,呼叫的建立必须非常快)。这些要求显示,Java可能不是用于此用途的好的选择,因为垃 圾收集和虚拟机的行为可能会与这些严格的要求相冲突。   Java语言是高度结构化的、强类型的和面向对象的。它不是被编译为特定于机器的指令,而是被编译为字节码,然后这些字节码将在各种平台上都有 的虚拟机上执行。虽然这会造成一定的性能损失,但是这也使得Java具有高度的可移植性。另一个具有重要意义的特性是垃圾收集程序的使用。Java将自动 化的内存管理(垃圾收集)作为Java运行时的一部分。这意味着以前开发人员常犯的一些与内存管理相关的错误都不会再出现了。由于垃圾收集功能成为 Java运行时的一部分,不再是在应用程序开发人员的完全控制之下,所以这也造成了性能损失,并使得对垃圾收集的预测变得很复杂。   为了使Java对电信行业更具吸引力,JAIN (Java APIs for Integrated Networks)提供了一组丰富的化API,用于方便电信服务的开发和部署。虽然JAIN确实提供了适用于电信应用程序的开放的标准化规范,但是它 并没有直接解决Java垃圾收集的问题。本文将提出一些技术,用于根据特定的要求(比如:垃圾收集暂停时间和延迟的最小值,或者电信服务的总执行时间的最 小值)帮助调优。   首先我们将简要地说明一下所选择的用于性能评估的用例。接下来我们将解释一下相关的Java虚拟机内部结构,继之以可能的优化选项和调优技术。然后我们将概述测试设置和性能评估的结果。 SIP用例   会话发起协议(SIP)描述了一个应用层控制(信令)协议,用于创建、修改和终止与一个或多个参与者的会话。这些会话包括Internet电话呼叫、多媒体分布和多媒体会议。   下列Dev2Dev文章提供了对SIP、SIP Servlet和Weblogic Communications platform的更深入描述: · SIP简介,第1部分:SIP初探 · SIP简介,第2部分:SIP SERVLET · BEA WEBLOGIC COMMUNICATIONS PLATFORM简介   图1显示了一个用于建立一个VoIP呼叫的典型用例,Proxy 200测试。本文选择它作为性能评估的例子。我们对它建立一个呼叫的时间感兴趣。下面就是从Alice发出初始INVITE(邀请)到她从Bob接收到OK所花费的时间。 图1. Proxy 200测试   此后,Alice将向Bob发送一个确认,说明她接收到了OK,然后就可以开始媒体会话(media session)(例如,音频或视频会议)了。在进行基准测试时,不会启动媒体会话,呼叫会立即被终止。 Java虚拟机内部结构   通常所使用的硬件平台会对性能和评估造成很大的影响,但是其实Java虚拟机也会产生很大影响。此处极为重要的因素是垃圾收集和内存管理。垃圾 收集可能导致整个虚拟机暂停。在VOIP应用程序中,这些暂停不应该持续太长时间,因为在建立呼叫时,较长的暂停可能会导致超时。 Java虚拟机内存   Java虚拟机的堆被分为3个称为generation的主要部分,它们对应于对象的生存期,如图2所示。3个generation分别是Young、Tenured和Perm,标记为Virtual的部分被保留,在必要时才实际分配出去。   Young generation由Eden和两个survivor空间组成。新对象通常创建于Eden中。其中一个survivor空间会随时被清空,并用作另一个 survivor空间的目的地。当进行垃圾收集时,所有来自Eden和survivor空间的活动对象都被复制到另一个survivor空间。对象在两个 survivor空间之间移动,直到它们足够“老”,能够被移入保存生存期较长对象的tenured generation中。    图2. Java虚拟机内存的结构   Perm generation保存那些在虚拟机的整个生存期都生存的对象。因此,该generation不需要被垃圾收集程序清空。 Java垃圾收集策略   除了默认的垃圾收集程序,还对其他的两个收集程序,Parallel收集程序和Concurrent Mark and Sweep收集程序(后面称为Concurrent收集程序)进行了评估。在标准的Sun Java运行时环境中,这些收集程序都可用。通常在进行垃圾收集时,虚拟机会暂停,让垃圾收集程序执行它的工作。Parallel收集程序会产生多个垃圾 收集线程,以加速任务的执行并将垃圾收集暂停时间降至最短。该收集程序负责整理Young generation。Concurrent收集程序则负责整理tenured generation,而且会部分地与应用程序并发运行。虽然仍然需要完整的垃圾收集周期,但是整个周期会因之而缩短——虽然这是以牺牲少量处理能力为代 价的,这些处理能力本来是可以由应用程序使用的。   这两个收集程序可以一起使用,因为它们负责的是不同的generation。这将显著缩短由垃圾收集所引发的暂停时间,如图3所示。    图3. 可用的垃圾收集程序 垃圾收集程序设置的优化   由于改变参数后的影响并不总是能够被预料到,所以对垃圾收集的调优并不是一项简单的任务。这方面的白皮书并不少(参见“参考资料”部分),但是 却缺乏对详细的(根据实验得到的)调优结果以及对垃圾收集的实际影响的描述。因此一个测试套件被开发出来,以便评估可能的优化,并暴露在设置不同的调优参 数时可能出现的问题。本部分将探索调优过程,并涉及面向SIP信令特征的优化。 评估详情   测试套件由一个可以生成特定数目的对象的应用程序组成,每个对象的生存期和在内存中的大小都可配置。该应用程序使用72种不同的虚拟机调优选项 组合反复运行,这些选项包括堆大小的调整、generation大小的调整、垃圾收集程序的选择,以及以上各项的组合。我们根据经验并排除无关的参数组合 来选择调优参数。我们使用一个基于开源的gcviewer应用程序的定制应用程序来观察和分析垃圾收集行为,它允许我们确定选项对垃圾收集所产生的精确影响。   关于虚拟机调优,有两个重要的优化是可以做到的:总执行时间的最小化和由垃圾收集所引起的暂停时间的最小化。另一个选项是最小化花在执行垃圾收 集操作上的总时间。在大多数情况下,这基本上等效于最小化总执行时间。然而,测试结果显示,也有例外的情况:虽然花在垃圾收集上的实际时间缩短了,总的执 行时间也可能会变长。当将堆大小设置为一个固定或可变的值时,就会发生这种情况,如图4和5所示。在图中,如果使用单CPU,那么即使固定的堆可以缩短垃 圾收集时间(固定的堆为10秒,可变的堆为17秒),它还是延长了总执行时间(405比403)。    图4. 总垃圾收集时间    图5. 总执行时间   注意,该测试套件不是CPU限制(CPU-bound)型的,但是它按照特定的时间间隔分配对象。这意味着双CPU配置在垃圾收集方面处于有利地位,但是在执行时间方面没有大的区别。 被评估的选项   下面是一个被评估的最重要选项的列表,附有简要描述:     -Xmx -Xms 这两个参数分别指定了虚拟机堆大小的最大值和最小值。通过设置最大值和最小值,可以指定固定的堆大小。有必要设置足够大的堆大小,以便虚拟机不会出现内存不足的情况,但如果堆大小设置得过大,垃圾收集时间就会不必要地延长。 -XX:+UseParNewGC 该选项启用可以与Concurrent收集程序同时运行的Parallel垃圾收集程序。 -XX:+UseConcMarkSweepGC 该选项启用Concurrent收集程序。 -XX:MaxNewSize -XX:NewSize 这两个选项定义young generation的最大值和最小值。将young的大小设置为大于总堆大小的一半时会造成效率低下。如果设置得过小,又会因为young generation收集程序不得不频繁运行而造成瓶颈。 -XX:+UseTLAB 启用该选项时,虚拟机将支持对象的线程逻辑分配。这将允许多个线程并发地分配对象,而不太需要在共用的eden空间中进行锁定。 针对低延迟而进行优化   当针对低延迟而进行优化时,最显而易见的Java垃圾收集程序选择应该是Parallel和Concurrent收集程序的组合,因为它们试图 最小化垃圾收集进程的阻塞暂停时间。设置总的堆大小和young generation将会产生最好的结果,因为无需再花费时间重新调整它们的大小。但是,事实证明,要估算理想的大小是很困难的,而且也不可能找到一个对 所有场景来说都是最优的“普遍适用的”值。因此下面的做法是明智的:只设置总的堆的最大值,确保在需要的时候有足够的内存空间可用。   表1. 针对低延迟的Java虚拟机调优选项 · -Xmx512m · -XX:+UseParNewGC · -XX:+UseConcMarkSweepGC · -XX:+UseTLAB · -XX:+CMSIncrementalMode · -XX:+CMSIncrementalPacing · -XX:CMSIncrementalDutyCycleMin=0 · -XX:CMSIncrementalDutyCycle=10   表1中所示的选项包括对Concurrent收集程序的微调(通过允许它以增量方式完成任务)。最好不要将堆的最大值设置得过大;考虑到平均垃圾收集暂停时间,增加堆大小的开销就不算大了。设置固定的堆大小确实会导致更长的垃圾收集暂停时间,如图6所示。    图6. 经过低延迟优化后的垃圾收集暂停时间要短得多,尤其是当堆大小不固定时   上面列表中最后4个垃圾收集选项只适用于具有一个或两个处理器的系统。这将启用并发标记扫描(concurrent mark and sweep,CMS)增量模式,它将收集程序的并发部分分成一组操作,而不是在一个线程中顺序地执行它们。对于小型系统来说,这需要认真考虑,因为收集程 序的并发部分完全是使用一个CPU。让一个CPU专门用于处理可能很长的并发部分可能会产生严重的性能影响(例如,在响应时间和吞吐量方面)。操作组允许 将CPU分配给夹在中间的应用程序。此外,这些操作组被安排在young收集之间,以便进一步减少垃圾收集的影响。 执行时间的最小化   在优化执行时间时,消除由垃圾收集所引起的暂停就没那么重要了。然而,垃圾收集程序的并行执行仍然是可取的,因为一般情况下,这样会比默认的垃 圾收集程序快。如果想最小化执行时间,通常最好是尽量减少垃圾收集次数,这可以通过设置一个更大的堆大小而做到。这将减少垃圾收集次数,但是会使垃圾收集 时间变长,而且垃圾收集暂停时间会比使用低延迟选项时长。   表2. 最小化执行时间的Java虚拟机调优选项 · -Xmx1024m · -Xms1024m · -XX:+UseParNewGC · -XX:+UseTLAB   表2显示了缩短执行时间的选项组。设置一个较大的固定堆大小会在使用默认的垃圾收集时造成严重的性能损失,但在使用Parallel垃圾收集程序时则会带来性能获益,如图7所示。    图7. 在堆大小固定时,经过执行时间优化后的垃圾收集执行时间较短 特定的调优选项:SIP信令   提出的这些选项组为根据特定的应用需求优化垃圾收集行为提供了一个好的基础。然而,在特定的情况下,还可以考虑使用一些其他的选项。一些应用程 序,比如上面的SIP代理例子,具有允许进一步优化的特定行为:有许多对象的生存期非常短(例如,表示SIP消息的对象),而另一些对象的生存期则相当长 (例如,表示SIP会话的对象)。为此,最好是把生存期较长的对象直接放入tenured空间,减少垃圾收集时对其进行评估的次数,从而加快垃圾收集进 程。这被称为pretenuring(参见“参考资料”部分)。但是这需要来自虚拟机的支持,而这种支持目前无法获得。   通过将对象在survivor空间之间进行复制的次数设置为零,我们可以获得类似的效果。现在对象仍然是在young generation中创建,但是如果它们生存得足够长,就会在进行young generation的第一次垃圾收集时,直接被转入tenured generation。这可以通过设置-XX:MaxTenuringThreshold=0选项来实现。作为一个结果,我们不再需要survivor空 间了,因此survivor空间的大小应该减至最小。这可以通过设置-XX:SurvivorRatio=N选项来实现。因为只可以设置与young generation大小有关的大小,所以选择一个足够大的N值(如128)就可以获得我们想要的效果。要获得最优结果,我们推荐限制Young generation的大小。这将使垃圾收集执行得更为频繁,但是时间较短。 测试设置   在讨论所得到的结果之前,我们将首先概述一下所使用的测试设置。 软件设置   对于基准测试,我们使用SIPp。SIPp 是一个用于SIP协议的免费开源测试工具/流量生成器。它可以生成SIP流量,建立和释放多个呼叫。它还可以阅读定制的XML scenario文件,描述从简单到复杂的呼叫流。它包括一些使用SIPstone定义的基本测试设置。本文中的所有测试使用的都是SIPp,并基于先前 指定的场景。 硬件设置   所运行的所有测试使用的都是dual Opteron 242 HP DL 145,将2GB的内存用作代理。客户端运行在AMD athlonXP 1600+机器上,通过100Mb以太交换网互联。所有平台运行的都是2.6内核的Debian GNU/Linux。使用Sun的JDK 1.4.2。 SIP Servlet性能结果   现在我们可以来看一下测试结果了。前面几节介绍了Java虚拟机的内部结构、如何管理内存,以及当前可用的垃圾收集选项有哪些。此外,我们还介 绍了如何调优这些选项以优化虚拟机和垃圾收集程序的行为。我们特别关注了对由垃圾收集所引起的暂停时间的最小化,因为它可能导致SIP呼叫的建立失败。现 在我们将对BEA WebLogic SIP Servlet实现运行测试,看看这些因素综合作用的结果。   对SIP Servlet容器运行一组测试来评估垃圾收集调优选项。在建立SIP呼叫时使用图1中所指定的场景,95%的呼叫INVITE消息的确认应该在50ms之内到达,而50%的呼叫应该在25ms之内到达。   图8展示了只设置了最大堆大小、没有设置其它调优参数时的SIP Servlet应用服务器性能结果。其中明确显示,95个百分点从50个呼叫每秒(caps)就开始飞快上升。此时,多个SIP消息开始并发到达,这意味着一些消息在进行处理之前首先进行了排队。    图8. 使用默认垃圾收集的SIP Servlet结果   该结果的另一个问题是CPU使用率较低。在较高的呼叫率下,CPU使用率仍然较低,但是一些呼叫却超时了,这显然不是我们希望得到的结果。考虑到95%的呼叫应该在50ms之内响应,服务器的处理能力只能是130 caps。   图9展示了使用表1中经低延迟优化后的选项和pretenuring选项所进行的测试的结果。首先可注意到的区别是,95%百分点的增长要慢得 多了。虽然延迟仍然从70 caps开始增长,但是增长率不那么高了。因为平均垃圾收集暂停时间要比默认选项低得多,所以大部分呼叫的延迟不会太长。使用默认的垃圾收集选项,暂停时 间高达1秒;而使用优化后的选项,暂停时间还不到10ms。    图9. 使用优化后的垃圾收集调优的SIP Servlet结果   其次,我们注意到,CPU使用率变高了,在250 caps时几乎达到100%。从这个呼叫率开始,有一些呼叫就开始超时了。幸运的是,响应时间也随之缩短了。显而易见,更先进的垃圾收集程序算法会需要
/
本文档为【Java_内存溢出错误处理】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索