科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道Java EE 常见性能问题解决手册

Java EE 常见性能问题解决手册

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

这篇文章,是PRO JAVA EE 5 Performance Management and Optimization 的一个章节,作者Steven Haines分享了他在调优企业级JAVA应用时所遇到的常见问题。

来源:IT专家网 2008年4月15日

关键字: 问题 3977 常见 java

  • 评论
  • 分享微博
  • 分享邮件
这篇文章,是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 堆的家族和空间的详细分类

  对象在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强。增加足够的堆空间以使它能够存储所有应该保存的有效值,也意味着你必须有足够的内存去存储所有访问你站点的用户的有效会话。如果商业规则允许的话最好能缩短会话超时的时间,以减少堆占用空间的冲突。

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章