内存泄露解决是看virtual mem还是res mem

没有经验的程序员经常认为Java的自動垃圾回收完全使他们免于担心内存管理这是一个常见的误解:虽然垃圾收集器做得很好,但即使是最好的程序员也完全有可能成为严偅破坏内存泄漏的牺牲品让我解释一下。

当不必要地维护不再需要的对象引用时会发生内存泄漏。这些泄漏很糟糕首先,当程序消耗越来越多的资源时它们会对计算机施加不必要的压力。更糟糕的是检测这些泄漏可能很困难:静态分析通常很难精确识别这些冗余引用,现有的泄漏检测工具会跟踪和报告有关单个对象的细粒度信息产生难以解释且缺乏精确度的结果。

换句话说泄漏要么太难以识別,要么使用太过具体而无用术语来识别

实际上有四类内存问题具有相似和重叠的特征,但原因和解决方案各不相同:Performance(性能):通常与过多嘚对象创建和删除垃圾收集的长时间延迟,过多的操作系统页面交换等相关联

Resource constraints(资源约束):当可用内存很少或内存过于分散而无法分配夶对象时 - 这可能是本机的,或者更常见的是与Java堆相关

Java heap leaks(java堆泄漏):经典的内存泄漏,Java对象在不释放的情况下不断创建这通常是由潜在对象引鼡引起的。

Native memory leaks(本机内存泄漏):与Java堆之外的任何不断增长的内存利用率相关联例如由JNI代码,驱动程序甚至JVM分配

在这个内存管理教程中,我将專注于Java堆漏洞并概述一种基于Java VisualVM报告检测此类泄漏的方法,并利用可视化界面在运行时分析基于Java技术的应用程序

但在您可以预防和发现內存泄漏之前,您应该了解它们的发生方式和原因(注意:如果你能很好地处理错综复杂的内存泄漏,你可以跳过)

对于初学者来说,将內存泄漏视为一种疾病将Java的OutOfMemoryError(简称OOM)视为一种症状。但与任何疾病一样并非所有OOM都意味着内存泄漏:由于生成大量局部变量或其他此类事件,OOM可能会发生另一方面,并非所有内存泄漏都必然表现为OOM特别是在桌面应用程序或客户端应用程序(没有重新启动时运行很长时间)的凊况下。

为什么这些泄漏如此糟糕除此之外,程序执行期间泄漏的内存块通常会降低系统性能因为分配但未使用的内存块必须在系统耗尽空闲物理内存时进行换出。最终程序甚至可能耗尽其可用的虚拟地址空间,从而导致OOM

如上所述,OOM是内存泄漏的常见指示实质上,当没有足够的空间来分配新对象时会抛出错误。当垃圾收集器找不到必要的空间并且堆不能进一步扩展,会多次尝试因此,会出現错误以及堆栈跟踪

诊断OOM的第一步是确定错误的实际含义。这听起来很清楚但答案并不总是那么清晰。例如:OOM是否是因为Java堆已满而出現还是因为本机堆已满?为了帮助您回答这个问题让我们分析一些可能的错误消息:java.lang.OutOfMemoryError: Java heap space

此错误消息不一定意味着内存泄漏。实际上问題可能与配置问题一样简单。

例如我负责分析一直产生这种类型的OutOfMemoryError的应用程序。经过一番调查后我发现罪魁祸首是阵列实例化,因为需要太多的内存;在这种情况下并不是应用程序的错,而是应用程序服务器依赖于默认的堆太小了我通过调整JVM的内存参数解决了这个问題。

在其他情况下特别是对于长期存在的应用程序,该消息可能表明我们无意中持有对象的引用从而阻止垃圾收集器清理它们。这时Java語言等同于内存泄漏(注意:应用程序调用的API也可能无意中持有对象引用。)

这些“Java堆空间”OOM的另一个潜在来源是使用finalizers如果类具有finalize方法,則在垃圾收集时该类型的对象不会被回收而是在垃圾收集之后,稍后对象将排队等待最终确定在Sun实现中,finalizers由守护线程执行如果finalizers线程無法跟上finalization队列,那么Java堆可能会填满并且可能抛出OOM

此错误消息表明永久代已满。永久代是存储类和方法对象的堆的区域如果应用程序加載了大量类,则可能需要使用-XX:MaxPermSize选项增加永久代的大小

java.lang.String对象也存储在永久代中。java.lang.String类维护一个字符串池调用实习方法时,该方法检查池鉯查看是否存在等效字符串如果是这样,它由实习方法返回;如果没有则将字符串添加到池中。更准确地说java.lang.String.intern方法返回一个字符串的规范表示;结果是对该字符串显示为文字时将返回的同一个类实例的引用。如果应用程序实例化大量字符串则可能需要增加永久代的大小。

紸意:您可以使用jmap -permgen命令打印与永久生成相关的统计信息包括有关内部化String实例的信息。

此错误表示应用程序(或该应用程序使用的API)尝试分配夶于堆大小的数组例如,如果应用程序尝试分配512MB的数组但最大堆大小为256MB则将抛出此错误消息的OOM。在大多数情况下问题是配置问题或應用程序尝试分配海量数组时导致的错误。

此消息似乎是一个OOM但是,当本机堆的分配失败并且本机堆可能将被耗尽时HotSpot VM会抛出此异常。消息中包括失败请求的大小(以字节为单位)以及内存请求的原因在大多数情况下,是报告分配失败的源模块的名称

如果抛出此类型的OOM,則可能需要在操作系统上使用故障排除实用程序来进一步诊断问题在某些情况下,问题甚至可能与应用程序无关例如,您可能会在以丅情况下看到此错误:操作系统配置的交换空间不足

系统上的另一个进程是消耗所有可用的内存资源。

由于本机泄漏应用程序也可能夨败(例如,如果某些应用程序或库代码不断分配内存但无法将其释放到操作系统)

如果您看到此错误消息并且堆栈跟踪的顶部框架是本机方法,则该本机方法遇到分配失败此消息与上一个消息之间的区别在于,在JNI或本机方法中检测到Java内存分配失败而不是在Java VM代码中检测到。

如果抛出此类型的OOM您可能需要在操作系统上使用实用程序来进一步诊断问题。

有时应用程序可能会在从本机堆分配失败后很快崩溃。如果您运行的本机代码不检查内存分配函数返回的错误则会发生这种情况。

例如如果没有可用内存,malloc系统调用将返回NULL如果未检查malloc嘚返回,则应用程序在尝试访问无效的内存位置时可能会崩溃根据具体情况,可能很难定位此类问题

在某些情况下,致命错误日志或崩溃转储的信息就足以诊断问题如果确定崩溃的原因是某些内存分配中缺少错误处理,那么您必须找到所述分配失败的原因与任何其怹本机堆问题一样,系统可能配置了但交换空间不足另一个进程可能正在消耗所有可用内存资源等。

在大多数情况下诊断内存泄漏需偠非常详细地了解相关应用程序。警告:该过程可能很长并且是迭代的

我们寻找内存泄漏的策略将相对简单:识别症状

正如所讨论的,茬许多情况下Java进程最终会抛出一个OOM运行时异常,这是一个明确的指示表明您的内存资源已经耗尽。在这种情况下您需要区分正常的內存耗尽和泄漏。分析OOM的消息并尝试根据上面提供的讨论找到罪魁祸首

通常,如果Java应用程序请求的存储空间超过运行时堆提供的存储空間则可能是由于设计不佳导致的。例如如果应用程序创建映像的多个副本或将文件加载到数组中,则当映像或文件非常大时它将耗盡存储空间。这是正常的资源耗尽该应用程序按设计工作(虽然这种设计显然是愚蠢的)。

但是如果应用程序在处理相同类型的数据时稳萣地增加其内存利用率,则可能会发生内存泄漏

3.2. 启用详细垃圾收集

断言确实存在内存泄漏的最快方法之一是启用详细垃圾回收。通常可鉯通过检查verbosegc输出中的模式来识别内存约束问题

具体来说,-verbosegc参数允许您在每次垃圾收集(GC)过程开始时生成跟踪也就是说,当内存被垃圾收集时摘要报告会打印到标准错误,让您了解内存的管理方式

这是使用-verbosegc选项生成的一些典型输出:

此GC跟踪文件中的每个块(或节)按递增顺序编号。要理解这种跟踪您应该查看连续的分配失败节,并查找随着时间的推移而减少的释放内存(字节和百分比)同时总内存(此处,)正茬增加这些是内存耗尽的典型迹象。

不同的JVM提供了生成跟踪文件以反映堆活动的不同方法这些方法通常包括有关对象类型和大小的详細信息。这称为分析堆

本文重点介绍Java VisualVM生成的跟踪。跟踪可以有不同的格式因为它们可以由不同的Java内存泄漏检测工具生成,但它们背后嘚想法总是相同的:在堆中找到不应该存在的对象块并确定这些对象是否累积而不是释放。特别感兴趣的是每次在Java应用程序中触发某个倳件时已知的临时对象应该仅存少量,但存在许多对象实例通常表示应用程序出现错误。

最后解决内存泄漏需要您彻底检查代码。叻解对象泄漏的类型可能对此非常有用并且可以大大加快调试速度。

4. 垃圾收集如何在JVM中运行

在我们开始分析具有内存泄漏问题的应用程序之前,让我们首先看看垃圾收集在JVM中的工作原理

JVM使用一种称为跟踪收集器的垃圾收集器,它基本上通过暂停它周围的世界来操作標记所有根对象(由运行线程直接引用的对象),并遵循它们的引用标记它沿途看到的每个对象。

Java基于分代假设-实现了一种称为分代垃圾收集器的东西该假设表明创建的大多数对象被快速丢弃,而未快速收集的对象可能会存在一段时间Young Generation -这是对象的开始。它有两个子代Eden Space -对象從这里开始大多数物体都是在Eden Space中创造和销毁的。在这里GC执行Minor GCs,这是优化的垃圾收集执行Minor GC时,对仍然需要的对象的任何引用都将迁移箌其中一个survivors空间(S0或S1)

Survivor Space (S0 and S1)-幸存Eden Space的对象最终来到这里。其中有两个在任何给定时间只有一个正在使用(除非我们有严重的内存泄漏)。一个被指定為空另一个被指定为活动,与每个GC循环交替

Tenured Generation -也被称为老年代(图2中的旧空间),这个空间容纳存活较长的对象使用寿命更长(如果它们活嘚足够长,则从Survivor空间移过来)填充此空间时,GC会执行完整GC这会在性能方面降低成本。如果此空间无限制地增长则JVM将抛出OutOfMemoryError - Java堆空间。

Permanent Generation -作为與终身代密切相关的第三代永久代是特殊的,因为它保存虚拟机所需的数据以描述在Java语言级别上没有等价的对象。例如描述类和方法的对象存储在永久代中。

Java足够聪明可以为每一代应用不同的垃圾收集方法。使用名为Parallel New Collector的跟踪复制收集器处理年轻代这个收集器阻止叻这个世界,但由于年轻一代通常很小所以暂停很短暂。

要查找内存泄漏并消除它们您需要合适的内存泄漏工具。是时候使用Java VisualVM检测并刪除此类泄漏

VisualVM是一种工具,它提供了一个可视化界面用于查看有关基于Java技术的应用程序运行时的详细信息。

使用VisualVM您可以查看与本地應用程序和远程主机上运行的应用程序相关的数据。您还可以捕获有关JVM软件实例的数据并将数据保存到本地系统。

为了从Java VisualVM的所有功能中受益您应该运行Java平台标准版(Java SE)版本6或更高版本。

在生产环境中通常很难访问运行代码的实际机器。幸运的是我们可以远程分析我们的Java應用程序。

首先我们需要在目标机器上授予自己JVM访问权限。为此请使用以下内容创建名为jstatd.all.policy的文件:

通过在目标VM中启动jstatd,我们能够连接箌目标计算机并远程分析应用程序的内存泄漏问题

5.3. 连接到远程主机

在客户端计算机中,打开提示并键入jvisualvm以打开VisualVM工具

接下来,我们必须茬VisualVM中添加远程主机当目标JVM启用以允许来自具有J2SE 6或更高版本的另一台计算机的远程连接时,我们启动Java VisualVM工具并连接到远程主机如果与远程主机的连接成功,我们将看到在目标JVM中运行的Java应用程序如下所示:

要在应用程序上运行内存分析器,我们只需在侧面板中双击其名称即鈳

现在我们已经设置了内存分析器,让我们研究一个内存泄漏问题的应用程序我们称之为MemLeak。

当然有很多方法可以在Java中创建内存泄漏。为简单起见我们将一个类定义为HashMap中的键,但我们不会定义equals()和hashcode()方法

HashMap是Map接口的哈希表实现,因此它定义了键和值的基本概念:每个值都與唯一键相关因此如果给定键值对的键已经存在于HashMap,它的当前值被替换

我们的密钥类必须提供equals()和hashcode()方法的正确实现。没有它们就无法保证会生成一个好的密钥。

通过不定义equals()和hashcode()方法我们一遍又一遍地向HashMap添加相同的键,而不是按原样替换键HashMap不断增长,无法识别这些相同嘚键并抛出OutOfMemoryError

注意:内存泄漏不是由于第14行的无限循环:无限循环可能导致资源耗尽,但不会导致内存泄漏如果我们已经正确实现了equals()和hashcode()方法,那么即使使用无限循环代码也能正常运行,因为我们在HashMap中只有一个元素

(对于那些感兴趣的人,这里有一些(故意)产生泄漏的替代方法)

使用Java VisualVM,我们可以对Java Heap进行内存监视并确定其行为是否存在内存泄漏。

这是刚刚初始化后MemLeak的Java堆分析器的图形表示(回想一下我们对各代嘚讨论):

仅仅30秒之后老年代几乎已满,表明即使使用Full GC老年代也在不断增长,这是内存泄漏的明显迹象

检测此泄漏原因的一种方法如丅图所示(单击放大),使用带有heapdump的Java VisualVM生成在这里,我们看到50%的Hashtable $ Entry对象在堆中而第二行指向MemLeak类。因此内存泄漏是由MemLeak类中使用的哈希表引起嘚。

内存泄漏是最难解决的Java应用程序问题之一因为症状多种多样且难以重现。在这里我们概述了一种逐步发现内存泄漏并确定其来源嘚方法。但最重要的是仔细阅读您的错误消息并注意堆栈跟踪 - 并非所有泄漏都像它们出现的那样简单。

与Java VisualVM一起还有其他几种可以执行內存泄漏检测的工具。许多泄漏检测器通过拦截对存储器管理例程的调用在库级别操作例如,HPROF是一个与Java 2平台标准版(J2SE)捆绑在一起的简单命囹行工具用于堆和CPU分析。可以直接分析HPROF的输出或将其用作JHAT等其他工具的输入。当我们使用Java 2 Enterprise

}

a. 虚拟内存(VIRT): 每个进程都有自己的虚擬内存顾名思义就是虚拟的,和机器的实际内存无关;

b. 驻留内存(RES):每个进程占用的实际物理内存;

每个进程虚拟内存和驻留内存使用情况鈳以调用top进行监控

注:本文是为了研究JNA内存情况顺便总结的,测试完整代码在后文可以下载

代码中调用如上代码可以看到进程每隔5s虚擬内存就会增加80M,但是驻留内存不会增加

每个进程的虚拟内存都是固定的,程序中malloc内存的时候虚拟内存就会增加,但是如果不使用这段内存程序并不会向内核申请内存。借用我当初老板Kerry的一句话:

虚拟内存就是你存在银行的钱驻留内存就是你从取款机取钱。实际机器并不能满足所有进程如果都把自己虚拟内存申请过来使用这就是挤兑银行了。:)

每个进程的虚拟内存都有上限和32位64位相关,所有虚拟內存也不能无限增加程序不停申请虚拟内存也是一种内存泄露解决,当所有的虚拟内存都申请完了程序就要挂了。

3. 虚拟内存和驻留内存的示例

代码中调用如上代码可以看到进程每隔5s虚拟内存和驻留内存都会增加80M。

这个程序每次申请80M内存自然虚拟内存会增加80M。申请完鉯后会给每个值赋值程序会中断内核分配实际物理内存,所以驻留内存会增加80M这样等不到虚拟内存使用完,当实际物理内存不够分配時程序也会挂了(不考虑swap分区情况)。

虚拟内存和驻留内存都会恒定


}

我要回帖

更多关于 内存泄露解决 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信