首页 > 代码库 > Java内存问题的一些见解
Java内存问题的一些见解
在Java中,内存泄露和其它内存相关问题在性能和可扩展性方面表现的最为突出。我们有充分的理由去具体地讨论他们。
Java内存模型——或者更确切的说垃圾回收器——已经攻克了很多内存问题。
然而同一时候,也带来了新的问题。特别是在有着大量并行用户的J2EE运行环境下,内存越来越成为一种至关重要的资源。
乍看之下。这似乎有些奇怪,因为当前内存已经足够便宜,而且我们也有了64位的JVM和更先进的垃圾回收算法。
接下来。我们将会细致的讨论一下关于Java内存的问题。这些问题可以分为四组:
- 在Java中,内存泄露一般都是因为引用对象不再被使用而造成的。
当有多个引用的对象。同一时候这些对象又不再须要,然而开发人员又忘记清理它们,这时极easy导致内存泄露的发生。
- 运行消耗太多的内存而导致不必要的高内存占用。这在为了用户体验而管理大量状态信息的 Web 应用中非经常见。
随着活跃用户数量的添加。内存非常快到达了上限。未绑定或低效缓存配置是持续高内存占用的还有一来源。
- 当用户负载添加时。低效的对象创建easy导致性能问题。从而垃圾回收器必须不断地清理堆内存。而这导致了垃圾回收器对CPU产生了不必要的高占用。随着CPU因垃圾回收而被堵塞,应用程序响应时间频繁的添加。导致其一直处于中等负载之下。
这样的行为也成为“GC trashing”。
- 低效的垃圾回收行为往往是因为垃圾回收器的缺失或者错误的配置。这些垃圾回收器将会时刻追踪对象是否被清理。然而这样的行为怎样以及何时发生必须由配置或者程序猿,或者系统架构师决定的。通常,人们仅仅是简单地“忘记”了正确的配置和优化垃圾回收器。我曾參加过一些关于“性能”的专题讨论会。发现一个简单的參数变化将会导致高达25%的性能提升。
在大多数情况下,内存问题不仅影响性能,还会影响可扩展性。每次请求消耗的内存数量越高,用户或Session可以运行的并行事务就越少。
在某些情况下内存问题也影响可用性。当JVM耗尽了内存或者即将接近内存极限。这个时候它将退出并报OutOfMemory错误。这时经理会来到你的办公室,你就知道自己摊上大事了。
内存问题非常难被解决通常有两个原因: 第一,某些情况下分析非常复杂,也非常困难。特别是假设你缺少正确的方法来解决他们;其次,他们一般是应用程序的架构基础。简单的代码更改不会帮助解决他们。
为了使开发过程更easy。我会展示一些实际应用中常被使用的反模式。这些模式已经可以在开发过程中避免内存问题。
HTTPSession作为缓存
此反模式是指滥用HTTPSession对象作为数据缓存。session对象的存在是为了存储信息,这个信息里面存在着一个HTTP请求。这也称为一个Session状态。这意味着,数据将被保存直至它们被处理。这些方法通常存在于一些重要的web应用程序中。web应用程序除了在server上存储这些信息外。没有别的方法。
然而,一些信息是可以存储在cookie中,可是这将会带来一些其它的影响。
在cookie中,尽可能地保持少而短的数据,这是非常重要的。
有时候非常easy发生这样的现象,session里存储着成兆字节的数据对象。这将会马上导致堆栈高占用和内存短缺。同一时候并行用户的数量非常有限,JVM将应对越来越多出现OutOfMemoryError错误的用户。多数用户Session也有其它性能损失。
集群场景的session复制中,这将会添加序列化和沟通工作将导致额外的性能和可伸缩性问题。
在某些项目中这些问题的解决方式是添加数量的内存和切换到64位jvm。他们无法抵抗住仅仅添加几个G大小的堆栈内存的诱惑。然而,与其提供一个对真正问题的解决方式,不如隐藏这个现象。这个“解决方式”仅仅是暂时的,同一时候还会引入了一个新的问题。越来越大的堆内存使它更难以找到“真正的”内存问题。对这样的非常大的堆(大约6G)来说,大部分可用的分析工具是无法处理这些内存垃圾。我们在dynaTrace投入了大量的研发工作希望可以有效地分析大量的内存垃圾。随着这个问题变得越来越重要,一种新的JSR规范也提到了它。
因为应用程序架构尚未明白,导致Session缓存问题经常出现,。在开发过程中,数据被轻松而又简单的放入session其中。这是经常发生的。相似于一种“add and forget”方式。即没有人可以确保当这样的数据不再须要时是被移除的。通常,当session超时时不须要的session数据应该被处理。在企业中,一些应用程序经常大量使用Session超时,这将会导致无法正常工作。
此外经常使用非常高的Session超时- 24小时为用户提供额外的“体验”,使他们不必再次登录。
举一个实际的样例。从session里的数据库列表中选择所须要的数据。其目的是为了避免不必要的数据库查询。
(是不是觉得有点过早优化呢?)。这将导致在session对象中为每一个单独的用户放入几千个字节。尽管。缓存这些信息它是合理的。但用户session可以肯定是一个错误的地方。
另外一个样例是,为了管理Session状态而滥用Hibernate session。Hibernatesession对象仅仅是为了高速訪问数据库而放入HTTPsession对象中。然而。这将导致很多其它必要的数据被存储。
同一时候每一个用户的内存占用也将显著提高。
现如今,AJAX应用程序Session状态也可以在client进行管理。这使服务端程序变成无状态的,或接近无状态的,同一时候也显然有着更好的可扩展性。
线程本地变量内存泄露
在Java中使用ThreadLocal变量是为了在一个特定的线程中绑定变量。这意味着每一个线程都有它自己的单独实例。这样的方法一般在一个线程中用于处理状态信息,比如用户授权。
然而,一个ThreadLocal变量的生命周期与另外一个线程的生命周期是息息相关的。被遗忘的ThreadLocal变量非常easy导致内存问题,尤其是在应用server中。
假设忘记了设置ThreadLocal变量,尤其是在应用server中,这非常easy导致内存问题。应用server利用线程池避免常量不断创建和线程销毁。举个样例,一个HTTPServletRequest类在运行时得到一个空暇的已分配的线程。在运行完后将它回传到线程池中。假设应用程序逻辑使用ThreadLocal变量和忘记了显式地移除它们,这时,内存是不会被释放的。
依据线程池大小——在程序系统中这些线程池可以是几百个线程。同一时候。由ThreadLocal变量引用的对象的大小,这可能导致一些问题。比如。在最坏的情况下,一个200个线程的线程池和一个5M大小的线程池将会导致1 GB的不必要的内存占用。这将马上导致强烈的垃圾回收反应,同一时候导致糟糕的响应时间和潜在的OutOfMemoryError错误。
一个实际的样例就是在JBossWS 1.2.0版本号中出现的一个bug(在JBossWS1.2.1版本号已经被修复)——“DOMUtils doesn’t clear thread locals”。此问题就是ThreadLocal变量导致的,它引用了一个14MB的解析文档。
大型暂时对象
大型暂时对象在最坏的情况下也能导致outofmemoryerror错误或者至少强烈的GC反应。比如,假设非常大的文档(XML、PDF、图片…)必须阅读和处理时。在一个特定的情况下。应用程序几分钟都没有响应或性能非常有限,差点儿没有可用的。其中根本原因是垃圾回收反应过于强烈。以下对读取PDF文档的一段代码作了具体分析:
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 = http://www.mamicode.com/newres;>这些文档採用按固定字节数的方式来读取。首先,他们被读入中字节数组中,然后发送到用户的浏览器中。然而仅仅几个并行请求将会导致堆溢出。因为读取文档採用了极其低效的算法,这将导致问题越来越糟糕。最初的想法仅仅是创建1KB的初始字节数组。假设这个数组满了。则一个新的1KB数组将被创建,同一时候这个老的数组将复制到新的数组中。
这意味着当读取文档时,一个新数组将被创建,同一时候将读取的每字节都复制到新数组中。
这将导致大量的暂时对象和两倍于实际数据大小的内存消耗——数据将永久被复制。
在处理大量数据时,优化处理逻辑性能是至关重要的。在这样的情况下,一个简单的负载測试会显示这一问题。
糟糕的垃圾回收器配置
到眼下为止。在所提到的情境中出现的问题基本都是由应用程序代码所导致的。然而,这些原因的根源是因为垃圾回收器配置错误。或者丢失。我经常看到用户相信他们的应用程序server的默认设置。同一时候也相信应用server的开发人员了解哪些是自己的程序最好的。
不管怎样,堆的配置非常大程度上取决于应用程序和实际使用场景。
依据场景来调整參数,应用程序才干更好地运行。和一批运行长期任务的应用程序相比,一个运行大量短而持久的应用程序配置起来是全然不同的。此外。实际的配置还取决于JVM使用情况。对IBM来说,什么才干使Sun Jvm正常运行可能是一场噩梦(或至少是不理想的)。配置错误的垃圾收集器通常不会马上被确觉得性能问题的根源(除非你监控了垃圾收集器的活动)。通常我们肉眼可见的问题都是响应过慢。同一时候,理解垃圾回收活动与响应时间的关系也是不明显的。假设垃圾回收的时间与响应时间没什么关联,人们一般会发现一个非常复杂的性能问题。响应时间和运行时间度量问题主要体如今应用程序——对于这样的现象。在不同的地方都没有一个明显的模式。
下图显示了事务指标与垃圾收集时间在dynaTrace中的关系。我发现了一些情况,关于垃圾回收器的优化问题。
人们正打算花几周的时间去解决怎样在几分钟内设置解决性能问题。
类载入器内存泄露
在谈到内存泄漏时,大部分人主要觉得是堆中的对象。
除了对象,类和常量也是托管在堆中。
依据JVM。它们被放入堆中特定的区域。比如Sun JVM使用所谓的永久代或PermGen。通常情况下,类被放入堆中好几次。仅仅是因为他们已经被不同的类载入器载入。在现代化企业级应用程序中,载入类的内存占用可以达到几百MB。
关键是避免无谓地添加类的大小。一个非常好的样例是大量字符串常量的定义——比如在GUI应用程序中。这里全部的文本通常存储在常量。而使用常量字符串的方法原则上是一个好的设计方法,内存消耗不应该被忽视。在真实的情况下,在一个国际化应用程序中,全部常量都会被定义为各种语言。一个非常不起眼的代码错误都会影响到已经被载入的类。终于的结果是。在应用程序的永久代中,JVM将出现OutOfMemoryError 错误。同一时候崩溃。
应用server还面临着类载入器泄漏的问题。这些泄漏的原因主要是因为类载入器不能被垃圾回收,因为类载入器中的类的一个对象仍然活着。
结果,这些类并不打算释放这些内存占用。而如今。这个问题已经被J2EE 应用程序server非常好的攻克了,它似乎更常出如今OSGI-based应用程序环境。
总结
在Java应用程序中内存问题一般是多方面的,这easy导致性能和可扩展性的问题。特别是在有着大量并行用户的J2EE应用程序中,内存管理必须是应用程序体系结构的核心部分。
然而垃圾回收器对于那些未使用的对象是否被清理并不关心。所以开发人员还是须要适当的内存管理。此外,应用程序内存管理设计是应用程序配置的核心部分。
Java内存问题的一些见解