4年前 (2020-11-30)  jvm |   抢沙发  330 
文章评分 0 次,平均分 0.0

缺乏经验的程序员通常认为Java的自动垃圾收集完全可以让他们从内存管理的担忧中解脱出来。这是一种常见的误解:当垃圾收集器尽其所能时,即使是最好的程序员也完全有可能成为严重内存泄漏的牺牲品。让我解释一下。

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

换言之,泄漏要么太难识别,要么就太具体而没有用处。

实际上有四类记忆问题有相似和重叠的症状,但原因和解决办法各不相同:

  • 性能:通常与过度的对象创建和删除、垃圾收集的长时间延迟、操作系统页面交换过多等有关。
  • 资源约束:当可用内存太少或内存太零碎而无法分配大对象时,会发生这种情况,这可能是本机的,或者更常见的是Java堆相关的。
  • Java堆泄漏:典型的内存泄漏,在这种情况下,Java对象是连续创建而不被释放的。这通常是由潜在的对象引用引起的。
  • 本机内存泄漏:与Java堆之外任何持续增长的内存利用率有关,例如JNI代码、驱动程序甚至JVM分配。

在这个内存管理教程中,我将重点介绍Java堆泄漏,并概述一种基于java visualvm报告的检测此类泄漏的方法,并利用一个可视化界面在基于Java技术的应用程序运行时对其进行分析。

但是,在预防和发现内存泄漏之前,您应该了解它们是如何发生的以及为什么发生的。(注意:如果您能很好地处理复杂的内存泄漏问题,可以跳过前面的步骤。)

内存泄漏

首先,将内存泄漏视为一种疾病,将Java的OutOfMemoryErrorOOM,简而言之)视为一种症状。但与任何疾病一样,并非所有OOM都必然意味着内存泄漏:OOM可能是由于大量局部变量或其他类似事件的生成而发生的。另一方面,并不是所有的内存泄漏都必须表现为oom,尤其是在桌面应用程序或客户端应用程序(如果不重新启动,它们运行的时间不会很长)。

把内存泄漏看作一种疾病,把OutOfMemoryError看作一种症状。但并非所有OutOfMemoryErrors都意味着内存泄漏,也不是所有内存泄漏都显示为OutOfMemoryErrors。

为什么这些泄漏这么严重?除此之外,在程序执行过程中内存块的泄漏通常会随着时间的推移而降低系统性能,因为一旦系统耗尽了可用的物理内存,则必须将已分配但未使用的内存块替换掉。最终,程序甚至可能耗尽其可用的虚拟地址空间,从而导致OOM。

破译OutOfMemoryError

如上所述,OOM是内存泄漏的常见指示。本质上,当没有足够的空间分配新对象时,会抛出错误。尽管可以尝试,但垃圾回收器找不到必要的空间,堆也无法进一步扩展。因此,将出现一个错误,同时出现堆栈跟踪。

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

  • java.lang.OutOfMemoryError:Java堆空间
  • java.lang.OutOfMemoryError:永久空间
  • java.lang.OutOfMemoryError:请求的数组大小超过VM限制
  • java.lang.OutOfMemoryError:为<reason>请求<size>字节。交换空间不足?
  • java.lang.OutOfMemoryError:<reason><stack trace>(本机方法)

Java堆空间

此错误消息并不一定意味着内存泄漏。事实上,问题可以简单到配置问题。

例如,我负责分析一个始终产生这种OutOfMemoryError类型的应用程序。经过一番调查,我发现罪魁祸首是数组实例化,它需要太多内存;在本例中,这不是应用程序的错误,而是应用程序服务器依赖于默认的堆大小,这太小了。我通过调整JVM的内存参数来解决这个问题。

在其他情况下,尤其是对于长时间运行的应用程序,该消息可能表示我们无意中保留了对对象的引用,从而阻止垃圾回收器清理它们。这是相当于内存泄漏的Java语言。(注意:应用程序调用的API也可能无意中保存了对象引用。)

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

PermGen 永久空间

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

java.lang.String对象也存储在永久生成中。这个java.lang.String类维护字符串池。调用intern方法时,该方法检查池以查看是否存在等效字符串。如果是,则由intern方法返回;如果不是,则将字符串添加到池中。更确切地说java.lang.String.intern方法返回字符串的规范表示形式;结果是对同一个类实例的引用,如果该字符串以文本形式出现,则将返回该实例。如果应用程序实习了大量字符串,则可能需要增加永久生成的大小。

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

请求的数组大小超过VM限制

此错误表示应用程序(或该应用程序使用的API)试图分配大于堆大小的数组。例如,如果应用程序试图分配512MB的数组,但最大堆大小是256MB,则将抛出一个OOM并显示此错误消息。在大多数情况下,问题要么是配置问题,要么是应用程序试图分配大型阵列时产生的bug。

请求<size>字节用于<reason>。交换空间不足?

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

如果抛出这种类型的OOM,则可能需要使用操作系统上的故障排除实用程序来进一步诊断问题。在某些情况下,问题甚至可能与应用程序无关。例如,如果出现以下情况,您可能会看到此错误:

  • 操作系统配置的交换空间不足。
  • 系统上的另一个进程正在消耗所有可用的内存资源。

也有可能是由于本机泄漏导致应用程序失败(例如,如果某个应用程序或库代码在不断地分配内存,但未能将其释放到操作系统)。

Native method 本机方法

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

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

没有OOM的应用程序崩溃

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

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

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

诊断泄漏

在大多数情况下,诊断内存泄漏需要对相关应用程序有非常详细的了解。警告:这个过程可能很长,而且是反复的。

我们查找内存泄漏的策略将相对简单:

  1. 识别症状
  2. 启用详细垃圾回收
  3. 启用分析
  4. 分析轨迹

1. 识别症状

如前所述,在许多情况下,Java进程最终会抛出一个OOM运行时异常,这清楚地表明内存资源已经用尽。在这种情况下,您需要区分正常的内存耗尽和泄漏。分析OOM的信息,并根据上面提供的讨论找出罪魁祸首。

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

但是,如果应用程序在处理相同类型的数据时稳定地提高其内存利用率,则可能存在内存泄漏。

2. 启用详细垃圾回收

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

具体来说,-verbosegc参数允许您在每次垃圾收集(GC)进程开始时生成跟踪。也就是说,当内存被垃圾回收时,摘要报告将被打印为标准错误,从而让您了解内存是如何管理的。

以下是使用–verbosegc选项生成的一些典型输出:

java 内存泄露

此GC跟踪文件中的每个块(或节)按递增顺序编号。为了理解这个跟踪,您应该查看连续的分配失败节,并寻找空闲内存(字节和百分比)随着时间的推移而减少,而总内存(这里是19725304)在增加。这些都是记忆枯竭的典型症状。

3. 启用分析

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

4. 分析轨迹

本文主要讨论java visualvm生成的跟踪。但在释放对象的过程中,总是会有不同的对象被释放出来,而不是在内存块中发现不同的对象。特别值得注意的是,在Java应用程序中,每次触发某个事件时都会分配这些临时对象。许多应该只少量存在的对象实例的存在通常表明存在应用程序错误。

最后,解决内存泄漏需要彻底检查代码。了解对象泄漏的类型非常有帮助,可以大大加快调试速度。

垃圾回收在JVM中是如何工作的?

在我们开始分析存在内存泄漏问题的应用程序之前,让我们先看看垃圾回收在JVM中是如何工作的。

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

Java基于分代假设实现了一种称为分代垃圾回收器的功能,该假设指出,创建的大多数对象都会被快速丢弃,而未被快速收集的对象可能会存在一段时间。

基于这个假设,Java将对象划分为多个代。以下是视觉解读:

java 内存泄露

年轻一代

这是物体开始的地方。它有两个子代:

  • Eden伊甸园空间-物体从这里开始。大多数物体都是在伊甸园空间中创造和毁灭的。在这里,GC执行较小的GC,这是优化的垃圾收集。当执行一个小GC时,对仍然需要的对象的任何引用都将迁移到一个幸存者空间(S0或S1)。
  • 幸存者空间(S0和S1)-在伊甸园中幸存下来的物体在这里结束。其中有两个,并且在任何给定的时间只有一个在使用(除非我们有严重的内存泄漏)。一个被指定为空,另一个被指定为活动的,与每个GC循环交替进行。

Tenured Generation 终身一代

也被称为旧一代(图2中的旧空间),这个空间容纳寿命更长的旧物体(从幸存者空间移过来,如果他们活得够长的话)。当这个空间被填满时,GC执行一个完整的GC,这在性能方面的成本更高。如果这个空间无限增长,JVM将抛出OutOfMemoryError-Java堆空间。

Permanent Generation 永久代

与永久生成密切相关的第三代,永久生成非常特殊,因为它保存了虚拟机所需的数据,以描述在Java语言级别上不具有等价性的对象。例如,描述类和方法的对象存储在永久生成中。

Java足够聪明,可以对每一代应用不同的垃圾收集方法。年轻的一代是使用一个称为并行新收集器的跟踪复制收集器来处理的。这个收藏家阻止了世界,但因为年轻一代一般都很小,停顿时间很短。

有关JVM生成及其工作方式的详细信息,请访问Java热点中的内存管理虚拟机文档。

检测内存泄漏

要找到内存泄漏并消除它们,您需要适当的内存泄漏工具。现在是时候使用java visualvm来检测和删除这样的泄漏了。

使用Java VisualVM远程分析堆

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

你可以查看本地运行的应用程序的数据。您还可以捕获有关JVM软件实例的数据并将数据保存到本地系统。

为了从JavaVisualVM的所有特性中获益,您应该运行Java平台,Standard Edition(JavaSE)V6或更高版本。

为JVM启用远程连接

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

首先,我们需要授予自己在目标机器上的JVM访问权限。为此,请创建一个名为jstatd.all.policy公司包括以下内容:

grant codebase "file:${java.home}/../lib/tools.jar" {

   permission java.security.AllPermission;

};

创建文件后,我们需要使用jstatd-Virtual Machine jstat守护程序工具启用到目标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允许从另一台使用j2se6或更高版本的机器进行远程连接,我们启动javavisualvm工具并连接到远程主机。如果与远程主机的连接成功,我们将看到在目标JVM中运行的Java应用程序。

要在应用程序上运行内存探查器,只需在侧面板中双击其名称。

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

MemLeak

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

HashMap是Map接口的哈希表实现,因此它定义了key和value的基本概念:每个值都与一个惟一的key相关,所以如果给定的key-value对的key已经存在于HashMap中,那么它的当前值将被替换。

我们的key类必须提供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

使用javavisualvm,我们可以对Java堆进行内存监视,并确定其行为是否指示内存泄漏。

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

java 内存泄露

仅仅过了30秒,老一代就快满了,这表明,即使有完整的GC,老一代也在不断增长,这是内存泄漏的明显迹象。

下面的图显示了检测此泄漏原因的一种方法,该图像使用带有heap dump的java visualvm生成。在这里,我们看到50%的Hashtable$Entry对象在堆中,而第二行指向MemLeak类。因此,内存泄漏是由MemLeak类中使用的哈希表引起的。

java 内存泄露

最后,观察一下在OutOfMemoryError之后的Java堆,在这个堆中,年轻代和老一代都完全满了。

java 内存泄露

结论

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

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/919.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册