没有经验的程序员常常认为Java的自动垃圾回收彻底使他们免于担忧内存管理。这是一个常见的误解:虽然垃圾收集器作得很好,但即便是最好的程序员也彻底有可能成为严重破坏内存泄漏的牺牲品。让我解释一下。php
当没必要要地维护再也不须要的对象引用时,会发生内存泄漏。这些泄漏很糟糕。首先,当程序消耗愈来愈多的资源时,它们会对计算机施加没必要要的压力。更糟糕的是,检测这些泄漏可能很困难:静态分析一般很难精确识别这些冗余引用,现有的泄漏检测工具会跟踪和报告有关单个对象的细粒度信息,产生难以解释且缺少精确度的结果。html
换句话说,泄漏要么太难以识别,要么用太具体而没法用术语来识别。java
实际上有四类内存问题具备类似和重叠的特征,但缘由和解决方案各不相同:node
在这个内存管理教程中,我将专一于Java堆漏洞,并概述一种基于Java VisualVM报告检测此类泄漏的方法,并利用可视化界面在运行时分析基于Java技术的应用程序。程序员
但在您能够预防和发现内存泄漏以前,您应该了解它们的发生方式和缘由。 (注意:若是你能很好地处理错综复杂的内存泄漏,你能够跳过。)web
对于初学者来讲,将内存泄漏视为一种疾病,将Java的OutOfMemoryError(简称OOM)视为一种症状。但与任何疾病同样,并不是全部OOM都意味着内存泄漏:因为生成大量局部变量或其余此类事件,OOM可能会发生。另外一方面,并不是全部内存泄漏都必然表现为OOM,特别是在桌面应用程序或客户端应用程序(没有从新启动时运行很长时间)的状况下。数组
将内存泄漏视为疾病,将OutOfMemoryError视为症状。但并不是全部OutOfMemoryErrors都意味着内存泄漏,并不是全部内存泄漏都表现为OutOfMemoryErrors。bash
为何这些泄漏如此糟糕?除此以外,程序执行期间泄漏的内存块一般会下降系统性能,由于分配但未使用的内存块必须在系统耗尽空闲物理内存时进行换出。最终,程序甚至可能耗尽其可用的虚拟地址空间,从而致使OOM。服务器
如上所述,OOM是内存泄漏的常见指示。实质上,当没有足够的空间来分配新对象时,会抛出错误。当垃圾收集器找不到必要的空间,而且堆不能进一步扩展,会屡次尝试。所以,会出现错误以及堆栈跟踪。oracle
诊断OOM的第一步是肯定错误的实际含义。这听起来很清除,但答案并不老是那么清晰。例如:OOM是不是由于Java堆已满而出现,仍是由于本机堆已满?为了帮助您回答这个问题,让咱们分析一些可能的错误消息:
此错误消息不必定意味着内存泄漏。实际上,问题可能与配置问题同样简单。
例如,我负责分析一直产生这种类型的OutOfMemoryError的应用程序。通过一番调查后,我发现罪魁祸首是阵列实例化,由于须要太多的内存;在这种状况下,并非应用程序的错,而是应用程序服务器依赖于默认的堆过小了。我经过调整JVM的内存参数解决了这个问题。
在其余状况下,特别是对于长期存在的应用程序,该消息可能代表咱们无心中持有对象的引用,从而阻止垃圾收集器清理它们。这时Java语言等同于内存泄漏。 (注意:应用程序调用的API也可能无心中持有对象引用。)
这些“Java堆空间”OOM的另外一个潜在来源是使用finalizers。若是类具备finalize方法,则在垃圾收集时该类型的对象不会被回收。而是在垃圾收集以后,稍后对象将排队等待最终肯定。在Sun实现中,finalizers由守护线程执行。若是finalizers线程没法跟上finalization队列,那么Java堆可能会填满而且可能抛出OOM。
此错误消息代表永久代已满。永久代是存储类和方法对象的堆的区域。若是应用程序加载了大量类,则可能须要使用-XX:MaxPermSize选项增长永久代的大小。
Interned 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应用程序请求的存储空间超过运行时堆提供的存储空间,则多是因为设计不佳致使的。例如,若是应用程序建立映像的多个副本或将文件加载到数组中,则当映像或文件很是大时,它将耗尽存储空间。这是正常的资源耗尽。该应用程序按设计工做(虽然这种设计显然是愚蠢的)。
可是,若是应用程序在处理相同类型的数据时稳定地增长其内存利用率,则可能会发生内存泄漏。
断言确实存在内存泄漏的最快方法之一是启用详细垃圾回收。一般能够经过检查verbosegc输出中的模式来识别内存约束问题。
具体来讲,-verbosegc参数容许您在每次垃圾收集(GC)过程开始时生成跟踪。也就是说,当内存被垃圾收集时,摘要报告会打印到标准错误,让您了解内存的管理方式。
这是使用-verbosegc选项生成的一些典型输出:
此GC跟踪文件中的每一个块(或节)按递增顺序编号。要理解这种跟踪,您应该查看连续的分配失败节,并查找随着时间的推移而减小的释放内存(字节和百分比),同时总内存(此处,19725304)正在增长。这些是内存耗尽的典型迹象。
不一样的JVM提供了生成跟踪文件以反映堆活动的不一样方法,这些方法一般包括有关对象类型和大小的详细信息。这称为分析堆。
本文重点介绍Java VisualVM生成的跟踪。跟踪能够有不一样的格式,由于它们能够由不一样的Java内存泄漏检测工具生成,但它们背后的想法老是相同的:在堆中找到不该该存在的对象块,并肯定这些对象是否累积而不是释放。特别感兴趣的是每次在Java应用程序中触发某个事件时已知的临时对象。应该仅存少许,但存在许多对象实例,一般表示应用程序出现错误。
最后,解决内存泄漏须要您完全检查代码。了解对象泄漏的类型可能对此很是有用,而且能够大大加快调试速度。
在咱们开始分析具备内存泄漏问题的应用程序以前,让咱们首先看看垃圾收集在JVM中的工做原理。
JVM使用一种称为跟踪收集器的垃圾收集器,它基本上经过暂停它周围的世界来操做,标记全部根对象(由运行线程直接引用的对象),并遵循它们的引用,标记它沿途看到的每一个对象。
Java基于分代假设-实现了一种称为分代垃圾收集器的东西,该假设代表建立的大多数对象被快速丢弃,而未快速收集的对象可能会存在一段时间。
基于此假设,[Java将对象分为多代](www.oracle.com/technetwork…. Generations|outline)。这是一个视觉解释:
Java足够聪明,能够为每一代应用不一样的垃圾收集方法。使用名为Parallel New Collector的跟踪复制收集器处理年轻代。这个收集器阻止了这个世界,但因为年轻一代一般很小,因此暂停很短暂。
有关JVM代及其工做原理的更多信息,请查阅Memory Management in the Java HotSpot™ Virtual Machine 。
要查找内存泄漏并消除它们,您须要合适的内存泄漏工具。是时候使用Java VisualVM检测并删除此类泄漏。
VisualVM是一种工具,它提供了一个可视化界面,用于查看有关基于Java技术的应用程序运行时的详细信息。
使用VisualVM,您能够查看与本地应用程序和远程主机上运行的应用程序相关的数据。您还能够捕获有关JVM软件实例的数据,并将数据保存到本地系统。
为了从Java VisualVM的全部功能中受益,您应该运行Java平台标准版(Java SE)版本6或更高版本。
Related: Why You Need to Upgrade to Java 8 Already
在生产环境中,一般很难访问运行代码的实际机器。幸运的是,咱们能够远程分析咱们的Java应用程序。
首先,咱们须要在目标机器上授予本身JVM访问权限。为此,请使用如下内容建立名为jstatd.all.policy的文件:
grant codebase "file:${java.home}/../lib/tools.jar" { permission java.security.AllPermission; };
建立文件后,咱们须要使用jstatd - Virtual Machine jstat Daemon工具启用与目标VM的远程链接,以下所示:
jstatd -p <PORT_NUMBER> -J-Djava.security.policy=<PATH_TO_POLICY_FILE>
例如:
jstatd -p 1234 -J-Djava.security.policy=D:\jstatd.all.policy
经过在目标VM中启动jstatd,咱们可以链接到目标计算机并远程分析应用程序的内存泄漏问题。
在客户端计算机中,打开提示并键入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 。
MemLeak类:
package com.post.memory.leak;
import java.util.Map;
public class MemLeak {
public final String key;
public MemLeak(String key) {
this.key =key;
}
public static void main(String args[]) {
try {
Map map = System.getProperties();
for(;;) { map.put(new MemLeak("key"), "value"); } } catch(Exception e) { e.printStackTrace(); } } }
注意:内存泄漏不是因为第14行的无限循环:无限循环可能致使资源耗尽,但不会致使内存泄漏。若是咱们已经正确实现了equals()和hashcode()方法,那么即便使用无限循环,代码也能正常运行,由于咱们在HashMap中只有一个元素。
(对于那些感兴趣的人,这里有一些(故意)产生泄漏的替代方法。)
使用Java VisualVM,咱们能够对Java Heap进行内存监视,并肯定其行为是否存在内存泄漏。
这是刚刚初始化后MemLeak的Java堆分析器的图形表示(回想一下咱们对各代的讨论):
仅仅30秒以后,老年代几乎已满,代表即便使用Full GC,老年代也在不断增加,这是内存泄漏的明显迹象。
检测此泄漏缘由的一种方法以下图所示(单击放大),使用带有heapdump的Java VisualVM生成。在这里,咱们看到50%的Hashtable $ Entry对象在堆中,而第二行指向MemLeak类。所以,内存泄漏是由MemLeak类中使用的哈希表引发的。
最后,在OutOfMemoryError以后观察Java Heap,其中Young和Old代彻底填满。
内存泄漏是最难解决的Java应用程序问题之一,由于症状多种多样且难以重现。在这里,咱们概述了一种逐步发现内存泄漏并肯定其来源的方法。但最重要的是,仔细阅读您的错误消息并注意堆栈跟踪 - 并不是全部泄漏都像它们出现的那样简单。
与Java VisualVM一块儿,还有其余几种能够执行内存泄漏检测的工具。许多泄漏检测器经过拦截对存储器管理例程的调用在库级别操做。例如,HPROF是一个与Java 2平台标准版(J2SE)捆绑在一块儿的简单命令行工具,用于堆和CPU分析。能够直接分析HPROF的输出,或将其用做JHAT等其余工具的输入。当咱们使用Java 2 Enterprise Edition(J2EE)应用程序时,有许多堆转储分析器解决方案更友好,例如IBM Heapdumps for Websphere应用程序服务器。
做者:Jose Ferreirade Souza Filho
译者:Emma