JAVA内存问题(Java Memory Problems)

原文见:http://blog.dynatrace.com/2009/08/13/java-memory-problems/
   内存泄漏与其它内存相关的问题是JAVA中最为显著(prominent)的性能与扩展性问题. 所以我们有足够的理由更加深入的讨论内存这个主题.
    JAVA内存模型-或者更准备地说是垃圾搜集器-已经解决了许多内存问题.在同一时刻,创建了新的对象,特别是在拥有大并发用户的J EE环境下,内存已然成为关键的资源. 便宜的内存,64位的JVM以及现代的垃圾回收算法让内存问题乍一看似乎有些奇怪.
    所以,让我位更加仔细的来剖析一下JAVA内存问题. 这些问题被归类为四组:
   内存泄漏 在JAVA中指的是引用了那些不再使用的对象. 当多个引用指向同一对象并且开发人员忘了清理,而这些对应不再被引用时内存就发生泄漏了.
     无谓的高内存使用 是由占用了大量内存的实现所引起的. 这种问题在一些为了用户体验(user comfort)而大量使用状态信息的WEB应用中更加常见. 当活动的用户增长时, 系统就会非常迅速的达到内存极限.  不受约束或者配置低效的缓存也是长时间高内存使用的另外一个原因.
     低效的对象创建 在用户负责增长时很容易引发性能问题, 因为垃圾回收器必须不断的清理堆. 这会导致大量的CPU资源被GC无谓的占用. 此时CPU会因为GC而阻塞,应用程序在非常温和负载下的响应响应时间增加. 这种现象被称之为GC中断.
     低效的GC行为 是由于没有或者错误的GC配置导致的. GC会负责对应的清理, 但这个清理工作在什么时候发生,怎么清理必须通过程序员或者系统架构师配置来完成. 非常普遍情况是人们总简单的"忘记"恰当的配置并准确调整GC. 我参与过大量的关于性能的讨论会,往往只需要调整一个"简单"的参数,就可以带来25%的性能提升.
     内存问题,在大多数情况下,不仅影响性能而且还影响伸缩性. 在一个请求,用户或者session中,消耗的内存越大,那么同一时间可并发执行的事务也就越少. 在一些安全中,内存问题甚至还会影响有效性. 当JVM内存耗近或者即使接近其极限时,JVM会因为OutOfMemory错误而退出. 这时,领导走进你的办公室你才意识到你遭遇一个很严重的问题.
     一般而言,内存问题总是很难判断,原因有二: 第一. 在某些情况下分析将会变得复杂而且困难--特别是如果你没有合适的方法来剖析的时候. 第二. 问题总是在应用程序的架构中被发现 . 简单的代码对于问题的解决于事无补.
    为了使这一切变得更加简单,我提出一组内存反模式,这些模式在现实世界中被大量使用,它们可以帮助在开发过程中避免内存问题.
    将HTTP Session作为缓存用
     这个反模式是与错误将HTTPSession对象作为一个数据缓存来使用相对应的.这个session对象提供了一种方式可以保存信息,从而可以使这些信息跨越不同的HTTP请求. 这也就是提到的会话状态. 意味着保存下来的数据可以跨越多个请求直至被最终处理. 这种方法存在于任何一个非凡的WEB应用. WEB应用没有其它的方式将数据存储在服务器. 当然,有些信息可以保存在cookie,但这包含了大量其它的隐含前提.
     在session里面保存尽可能少并且尽可以短时间的保存是非常重要的. session里面很容易就存储了数M的对象数据. 这会很快导致高的堆使用与内存短缺. 同时, 并发的用户数量将会十分有限. 如果用户数持续增加,JVM将会用OutOfMemory来回应. 大量用户会话也会造成性能问题. 在集群环境下的session复制就会增加序列化与通信的开销从而引发额外的性能与伸缩性问题.
     在一些项目中,碰到这种问题时,通常会增加内存并且将JVM更换到64位. 他们无法反对临时将堆大小往上增加到几个GB, 但这只是将问题隐藏起来而不是真正的解决. 这种"方案"只是临时的并且会产生一些新的问题.  超大堆内存(超过6G)的清理对于目前绝大多数分析工具而言是无法处理的. 我们在dynaTrace进行了大量的R&D尝试,试图有效的分析大内存的清理过程. 这个问题获取了越来越多的重视, 包括一个新的JSR规范.
     Session缓存问题经常出现,因为应用程序架构并没有很清晰的定义. 在开发过程中,数据简单的被存储到session是很简单舒服的. 但这总是以一种"add and forget"的方式出现, 因为没有人会确保数据在不使用时将其从session中删除. 所以, 正常情况下不再需要的sesseion数据会被session持久,直接session超时. 在企业级应用中(这些应用一直使用session超时), session超时将不起作用.  再则, session超时的时间经常被设置的很高--达到24小时--可以提供一种额外的"体验"给用户,这样他们不再需要再次登录.
     一个实际的例子就是从一个下拉列表中选择选项, 下拉列表的值会从数据库读取, 然后放在session中. 这么做的目的是避免不必要的数据库查询.(听起来感觉像不太成熟的优化--不是吗). 这导致了每一个用户都有几KB的数据添加到了session对象. 虽然缓存这些信息是合理的, 但将信息缓存到用户session绝对是个错误的选择.
     另外一个例子是滥用Hibernate session来管理会话状态.简单的将Hibernate session对象放入HTTPSession从而可以快速访问数据. 但是,这导致了必要时存储了大量的数据并且单个用户的内存消耗会显著上升.
    在现今的AJAX应用中,会话状态可以在客户端进行管理. 从概念上讲, 这将导致一个无状态或者接近无状态的服务器端应用,而这在伸缩性方面也有显著改善.
    ThreadLocal内存泄漏
     在JAVA中, ThreadLocal变量用来将变量绑定到一个特定的线程.  这就意味着每一个线程都有一个属于它自己的实例. 这种方法用来在一个线程中持有状态信息.  一个例子就是用户证件. 一个ThreadLocal变量的生命周期是与相应的线程的生命周期相关联的. 当线程中止或者被GC移除时, ThreadLocal变量会被清理--如果这些变量没有被程序员显式移除的话.
     在一些特别的应用中, 忘记ThreadLocal变量也能轻易的引发内存问题. 应用服务器使用线程池来避免不断的创建与销毁线程. 例如, 一个HTTPServletRequest可以在运行时被分配一个空闲的线程, 而这个线程在执行完成后会被归还到线程池. 如果应用逻辑使用ThreadLocal变量 并且没有显示的移除, 这些变量相关的内存就不会被释放.
     依赖与池的大小--在生产系统中, 可能会存在数百个线程--以及ThreadLocal变量的对象引用的大小, 这可能会导致问题. 一个200线程大小的线程池,每个ThreadLocal的大小为5MB, 在这种情况下, 将会有1GB无谓的内存被占用. 这将很快导致很高的GC活动, 从而导致糟糕的响应时间以及潜在的OutOfMemory.
     一个实际的例子就是jbossws v1.2.0中的一个bug--这个bug在v1.2.1中解决--DOMUtils没有清理线程变量. 这个问题是一个ThreadLocal变量解析一个文档时,占用了14MB的内存.
    大临时对象
     在一个极端环境下, 大临时对象也会导致OutOfMemoryErrors或者至少是高的GC活动. 例如, 当读取并处理一个大的文档(XML,PDF,图片,...)时, 这中情况就会发生. 在一些特殊的情况下, 应用程序几分钟没有响应或者性能影响很大以至于实际上应用程序不可用. 这个最根本的原因就是GC几乎疯狂. 依据下面这些读取一个PDF文档的代码进行一个详尽的分析.
Java代码
                                  byte tmpData[] = new byte [1024];int offs = 0;do{  int readLen = bis.read (tmpData, offs, tmpData.length - offs);  if (readLen == -1)      break;  offs+= readLen;  if (oofs == tmpData.length){    byte newres[] = new byte[tmpData.length + 1024];    System.arraycopy(tmpData, 0, newres, 0, tmpData.length);  tmpData = newres;  }} while (true);
--------------------


     上面这段代码读取的文档有数MB. 首先将数据读取到一个字节数组里, 然后发送到用户浏览器. 多个用户并发时, 将迅速导致满堆(full heap). 这个问题甚至会因为读取文档算法的低效而变得更加糟糕.  这个想法是首先创建一个1KB的初始化的字节数组. 如果这个数组满了, 则再创建一个新的1KB大小的字节数组,同时将旧数组复制到新数组中. 这意味着, 当读取文档的时候一个新的数据会被创建同时复制每KB已经读取的数据. 这将导致大量的临时对象和2倍于实际数据大小的内存消耗--因为数据被永久拷贝了.
     当遇上海量数据时, 处理逻辑的优化对于性能而言是极为关键的. 在这种情况下, 一个简单的压力测试就会暴露这个问题
    错误的GC配置
     截止到目前为止, 所有问题出现的情景都是由于应用程序代码所导致. 但很多情况下, 根本原因却是错误--或者没有--对GC进行配置. 我经常看到人们相信他们应用服务器的默认配置, 并且相应这些服务器人员知道什么才是应用程序的最佳配置. 而堆的配置却特别依赖于应用程序 以及确切的应用场景. 那些参数的设置必须依赖于这些应用场景才能使应用程序有一个好的性能. 一个处理大量持续时间短的请求与一个批处理应用程序--执行较长的时间--的配置就截然不同. 实际的配置还依赖于所使用的JVM. 在Sun JVM下跑得很好的配置到了IBM JVM下面可以就是一场恶梦(至少不是理想的). 没有配置GC并不经常就能迅速的定位为性能问题的根本原因(除非你监控GC的活动).  问题的虚拟表示形式经常是糟糕的响应时间. 要明白了GC活动与响应时间之间的关系不是显然的. 如果GC时间不能与响应时间相关系, 人们就会发现他们自己正寻找一个十分复杂的性能问题. 响应时间与执行时间公制问题将通过应用程序来描述--在不同的地方没有一个显然的模式隐藏在这个现象背后.
     下面这幅图片展示了与dynaTrace中GC时间相关的事务metrics. 我发现在一些案例中, 人们花了数周的时间去查找性能问题的原因, 而仅仅只是花了几分钟进行了GC设计的优化,就解决了.

JAVA内存问题(Java Memory Problems)

    ClassLoader泄漏

     当谈到内存泄漏的时候, 大多数人首先想到的是堆中的对象. 然而, 除了对象, 类与常量也在堆中管理.  他们被放在堆中的一个特别的区域, 这依据于JVM. 比如, Sun的JVM就使用叫做永久代或者PermGen的区域. 一种十分常见的情况是类会被放入堆中多次. 简单的讲是因为这些类被不同的类加载器(classloader)加载. 在当今的企业级应用, 这些已加载的类占用的内存会达到数百MB.
     关键是要避免类数量无谓的增加. 一个好的例子就是定义大量的String型的常量--例如在GUI应用中, 所有的文本都是以常量形式存储. 虽然这些常量占用的内存不会被释放(neglected), 但是使用String型常量的方式原则上是一个好的设计方法. 在一个现实的国际化应用的案例中, 每一种语言使用到的常量都在一个类中定义.  一个不明显的编码错误会导致所有的类进行加载. 这也最终会导致JVM因为PermGen的OutOfMemory而崩溃.
      应用服务器承受着类加载泄漏带来的额外的问题. 这些泄漏是由类加载器不能被GC而引起--因为这个类加载器加载的类的实例仍然存活. 结果是这些类占用的内存不能释放. 当然, 现在这个问题已经由J EE应用服务器很好的解决了, 看起来这种问题在基于OSGI的应用环境中出现的更加频繁.
    结论
     内存问题是JAVA应用中非常普通并且很容易引发性能与伸缩性的问题. 特别是在高并发用户数的J EE应用中, 内存管理性能作为应用架构的一个核心部分来对待.
     GC负责那些清理那些没有被引用的对象, 然而开发人员仍然需要对适当的内存管理负责   . 特别是在内存管理作为应用配置的中心部分的应用设计中尤为如此.

    你的经验
    这些反模式是我在实际的应用中发现的. 如果你有其它的反模式或者普遍的问题, 我很乐意更多的去了解.
     Credits
     这篇文件是我与Mirko Novakovic of codecentric合作推出的基于性能反模式系列.
    Interesting Other Blogs
     因为性能反模式是我的爱好, 我会经常发布一些关于性能反模式的文章.  这有一些你可能比较感兴趣的文章.
  Some insights into Hibernate Caching: http://blog.dynatrace.com/tag/hibernate-java/

Remoting Problems:http://blog.dynatrace.com/2008/07/09/performance-antipattern-logical-structure-vs-physical-deployment/
声明:JavaEye文章版权属于作者,受法律保护。没有作者书面许可不得转载。

你可能感兴趣的:(java,jvm,应用服务器,配置管理,企业应用)