在Java中,内存泄漏和其他与内存相关的问题在性能和可伸缩性方面最为突出。我们有充分的理由详细讨论它们。Java内存模型——或者更准确地说是垃圾收集器——已经解决了许多内存问题。但与此同时,也带来了新的问题。特别是在有大量并行用户的J2EE运行环境中,内存越来越成为至关重要的资源。乍一看,这似乎有点奇怪,因为今天内存已经足够便宜了,而且我们还有64位JVM和更高级的垃圾收集算法。接下来,我们将详细讨论Java内存的问题。这些问题可以分为四组:在Java中,内存泄漏通常是由引用不再使用的对象引起的。当有多个被引用的对象,而这些对象不再需要,而开发者却忘记清理它们时,就很容易造成内存泄漏。执行消耗太多内存,导致不必要的高内存使用率。这在为用户体验管理大量状态信息的Web应用程序中很常见。随着活跃用户数量的增加,很快就会达到内存限制。未绑定或低效的缓存配置是持续高内存使用率的另一个来源。当用户负载增加时,低效的对象创建很容易导致性能问题。因此,垃圾收集器必须不断地清理堆内存。这导致垃圾收集器不必要地高CPU使用率。由于CPU被垃圾收集阻塞,应用程序响应时间频繁增加,导致其保持在中等负载下。此行为也称为“GCtrashing”。低效的垃圾收集行为通常是由于垃圾收集器缺失或配置错误造成的。这些垃圾收集器将跟踪对象是否被清理。但是,这种行为如何以及何时发生必须由配置或程序员或系统架构师决定。通常,人们只是“忘记”正确配置和优化垃圾收集器。我参加过一些关于“性能”的研讨会,发现一个简单的参数更改可以使性能提高高达25%。在大多数情况下,内存问题不仅会影响性能,还会影响可扩展性。每个请求消耗的内存量越高,用户或会话可以执行的并行事务就越少。在某些情况下,内存问题也会影响可用性。当JVM内存不足或即将接近内存限制时,它会退出并报告OutOfMemory错误。那是经理走进你办公室的时候,你知道你遇到了大问题。内存问题通常很难解决有两个原因:第一,有些情况很复杂,难以分析,尤其是如果你缺乏正确的解决方法;其次,它们通常是应用程序的架构基础。简单的代码更改无助于修复它们。为了使开发过程更容易,我将展示一些在实际应用程序中常用的反模式。这些模式已经能够避免开发过程中的内存问题。HTTPSession作为缓存这种反模式指的是将HTTPSession对象滥用为数据缓存。session对象的存在是为了存储信息,在这个信息中有一个HTTP请求。这也称为会话状态。这意味着数据会一直存储到被处理为止。这些方法通常存在于一些重要的网络应用程序中。Web应用程序只能将此信息存储在服务器上。然而,有些信息可以存储在cookies中,但这会产生一些其他的影响。在cookie中,保留尽可能少和简短的数据非常重要。有时这种现象很容易发生,session中存储了兆字节的数据对象。这将立即导致堆栈使用率高和内存不足。同时并发用户数非常有限,JVM会应付越来越多出现OutOfMemoryError错误的用户。大多数用户会话还有其他性能损失。在集群会话复制中,这将增加序列化和通信工作,并导致额外的性能和可伸缩性问题。在某些项目中解决这些问题的方法是增加内存量并切换到64位jvm。他们无法抗拒只添加几GB堆栈内存的诱惑。然而,与其为真正的问题提供解决方案,还不如隐藏这种现象。这个“解决方案”只是暂时的,并且引入了新的问题。越来越大的堆使得更难找到“真正的”内存问题。对于这样一个非常大的heap(大约6G),大部分可用的分析工具都无法处理这些内存垃圾。我们在dynaTrace上投入了大量的研发工作来高效分析大量的内存垃圾。随着这个问题变得越来越重要,新的JSR规范解决了这个问题。由于应用架构还不明确,所以经常会出现Session缓存问题。在开发过程中,数据很容易简单地放入会话中。这种情况经常发生,以一种“添加并忘记”的方式,没有人可以确保在不再需要时删除这些数据。通常,当会话超时时,应该处理不需要的会话数据。在企业中,一些应用程序经常大量使用会话超时,这会导致故障。此外,经常使用非常高的Session超时-24小时为用户提供额外的“体验”,使他们不必再次登录。举一个实际的例子,在会话中从数据库列表中选择需要的数据。其目的是避免不必要的数据库查询。(您认为现在优化有点为时过早吗?)。这将导致在每个用户的会话对象中放置数千字节。虽然,缓存这些信息是合理的,但是用户会话肯定是错误的地方。另一个例子是为了管理Session状态而滥用HibernateSession。Hibernatesession对象只是为了快速访问数据库而放入HTTPsession对象。但是,这将导致存储更多必要的数据。同时,每个用户的内存使用量也会显着增加。今天,AJAX应用程序会话状态也可以在客户端进行管理。这使得服务器程序无状态,或接近无状态,显然具有更好的可扩展性。线程局部变量内存泄漏ThreadLocal变量在Java中用于绑定特定线程中的变量。这意味着每个线程都有自己的独立实例。该方法一般用在线程中处理状态信息,比如用户授权。但是,ThreadLocal变量的生命周期与另一个线程的生命周期密切相关。忘记ThreadLocal变量很容易导致内存问题,尤其是在应用程序服务器中。如果忘记设置ThreadLocal变量,尤其是在应用服务器中,这很容易导致内存问题。应用服务器使用线程池来避免不断地创建和销毁线程。例如,一个HTTPServletRequest类在运行时获得一个空闲分配的线程,并在执行后将其返回到线程池。如果应用程序逻辑使用了ThreadLocal变量而忘记显式删除它们,那么内存将不会被释放。根据线程池的大小——这些线程池可以是一个程序系统中的数百个线程。此外,ThreadLocal变量引用的对象的大小,这可能会导致一些问题。例如,在最坏的情况下,200个线程的线程池和5M大小的线程池将导致1GB的不必要内存使用。这将立即引起强烈的垃圾收集反应,从而导致响应时间变慢和潜在的OutOfMemoryError错误。一个实际的例子是JBossWS1.2.0中的一个错误(已在JBossWS1.2.1中修复)——“DOMUtilsdoesn'tclearthreadlocals”。这个问题是ThreadLocal变量引起的,它引用了一个14MB的解析文档。大型临时对象大型临时对象也可能导致内存不足错误或在最坏的情况下至少导致强烈的GC反应。例如,如果必须读取和处理非常大的文档(XML、PDF、图像...)。在一种特殊情况下,应用程序要么在几分钟内没有响应,要么性能非常有限,几乎无法使用。根本原因是垃圾回收响应太强。下面详细分析一段读取PDF文档的代码:bytetmpData[]=newbyte[1024];intoffs=0;do{intreadLen=bis.read(tmpData,offs,tmpData.length-offs);if(readLen==-1)break;offs+=readLen;if(oofs==tmpData.length){bytenewres[]=newbyte[tmpData.length+1024];System.arraycopy(tmpData,0,newres,0,tmpData.length);tmpData=newres;}}while(true);这些文件按固定字节数读取。首先,它们被读入一个字节数组并发送到用户的浏览器。然而,只有少数并行请求会导致堆溢出。由于用于读取文档的算法效率极低,这将使问题变得越来越糟。最初的想法只是创建一个1KB的初始字节数组。如果数组已满,将创建一个新的1KB数组,并将旧数组复制到新数组中。这意味着当读取文档时,将创建一个新数组并将读取的每个字节复制到新数组中。这将导致大量的临时对象和两倍于实际数据大小的内存消耗——数据将被永久复制。在处理大量数据时,优化处理逻辑性能至关重要。在这种情况下,简单的负载测试将揭示问题。糟糕的垃圾收集器配置到目前为止提到的场景中出现的问题大多是由应用程序代码引起的。然而,这些原因的根本原因是垃圾收集器配置错误或缺失。我经常看到用户相信他们的应用程序服务器的默认设置,同时相信应用程序服务器的开发人员知道什么最适合他们的程序。无论如何,堆的配置在很大程度上取决于应用程序和实际使用场景。通过根据场景调整参数,应用程序可以表现得更好。执行大量短期、长期任务的应用程序的配置与一批执行长期任务的应用程序的配置完全不同。此外,实际配置取决于JVM的使用情况。让SunJvm工作的东西对IBM来说可能是一场噩梦(或者至少不是理想的)。错误配置的垃圾收集器通常不会立即被识别为性能问题的根源(除非您监视垃圾收集器的活动)。通常我们肉眼看到的问题是反应慢。此外,了解垃圾收集活动与响应时间之间的关系并不明显。如果垃圾收集时间与响应时间没有很好的相关性,通常会发现一个非常复杂的性能问题。响应时间和执行时间测量问题大多是特定于应用程序的——这种现象在不同地方没有明显的模式。下图显示了dynaTrace中的事务指标与垃圾收集时间的关系。我发现了一些案例,关于垃圾收集器的优化问题。人们将花费数周的时间弄清楚如何在几分钟内设置和解决性能问题。类加载器内存泄漏提到内存泄漏,大多数人主要会想到堆中的对象。除了对象之外,类和常量也在堆上进行管理。根据JVM,它们被放入堆中的特定区域。例如SunJVM使用所谓的永久代或PermGen。通常,类会被多次放入堆中。仅仅是因为它们被不同的类加载器加载了。在现代企业应用程序中,加载类的内存占用量可达数百兆字节。关键是要避免不必要地增加班级人数。一个很好的例子是大量字符串常量的定义——例如在GUI应用程序中。这里的所有文本通常都存储在常量中。虽然使用常量字符串的方法原则上是一种很好的设计方法,但内存消耗也不容忽视。在实际情况下,在国际化应用程序中,将为每种语言定义所有常量。一个微不足道的代码错误可能会影响已经加载的类。最终结果是JVM将在应用程序的永久生成中崩溃并出现OutOfMemoryError。应用服务器还面临类加载器泄漏的问题。这些泄漏的原因主要是因为类加载器无法进行垃圾回收,因为类加载器中的类的一个对象仍然存在。因此,这些类不会尝试释放这些内存占用。虽然现在J2EE应用服务器很好地解决了这个问题,但它似乎更常出现在基于OSGI的应用程序环境中。总结Java应用程序中的内存问题通常是多方面的,很容易导致性能和可扩展性问题。特别是在有大量并行用户的J2EE应用中,内存管理必须是应用架构的核心部分。然而,垃圾收集器并不关心那些未使用的对象是否被清理,因此开发人员仍然需要适当的内存管理。此外,应用程序内存管理设计是应用程序配置的核心部分。您的经验这些是我在现实世界的应用程序中发现的反模式。如果您有其他反模式或常见问题,我很乐意了解更多。关于作者本文基于我与以代码为中心的作者MirkoNovakovic一起研究的一系列性能反模式。其他感兴趣的博客由于性能反模式是我的爱好,我将定期发布有关反模式的文章。从这些帖子中,他们会选择一个您可能感兴趣的帖子。对Hibernate缓存远程问题的一些见解如果您想了解更多关于如何解决本文中提到的内存问题以及其他Java运行时相关问题,你可能对我同事最近写的关于持久化应用程序的书感兴趣,关于性能管理的白皮书很感兴趣。
