`
苦逼的程序员
  • 浏览: 28503 次
  • 性别: Icon_minigender_1
  • 来自: 青岛
社区版块
存档分类
最新评论

Java 程序里的内存泄漏

 
阅读更多

译序:Java 的内存泄漏,这不是一个新话题。Jim Patrick 的这篇文章早在 2001 年就写出来了。但这并不意味着 Java 的内存泄漏是一个过时了的甚至不重要的话题。相反, Java 的内存泄漏应当是每一个关心程序健壮性、高性能的程序员所必须了解的知识
        本文将揭示什么时候需要关注内存泄漏以及如何进行防止
        摘要:Java 程序里也存在内存泄漏?当然。和流行的看法相反,内存管理仍然是 Java 编程时应该考虑的事情。在这篇文章里,你会了解到是什么原因导致了 Java 内存泄漏以及什么时候需要对这些泄漏进行关注。你也将会学到一个快速实用的课程以应对自己项目中的内存泄漏。
        Java 程序里的内存泄漏是如何表现的
        大多数程序员都知道使用类似于 Java 的编程语言的好处之一就是他们无需再为内存的分配和释放所担心了。你只需要简单地创建对象,当它们不再为程序所需要时 Java 会自行通过一个被称为垃圾收集的机制将其移除。这个过程意味着 Java 已经解决了困扰其他编程语言的一个棘手的问题 -- 可怕的内存泄漏。果真是这样的吗?
        在进行深入讨论之前,让我们先回顾一下垃圾收集是如何进行实际工作的。 垃圾收集器的工作就是找到程序不再需要的对象并在当它们不再被访问或引用时将它们移除掉 垃圾收集器从贯穿整个程序生命周期的类这个根节点开始,扫描所有引用到的节点。在遍历节点时,它跟踪那些被活跃引用着的对象。那些不再被引用的对象就满足了垃圾回收的条件 。当这些对象被移除时被它们占用的内存资源会交还给 Java 虚拟机(JVM)。

        因此 Java 代码的确不需要程序员负责内存管理的清理工作,它自行对不再使用的对象进行垃圾收集。然而,需要记住的是,垃圾收集的关键在于一个对象在不再被引用时才被统计为不再使用 。下图对这一概念进行了说明。

没有使用却仍被引用的对象

        上图表示在一个 Java 程序执行时具有不同的生命周期的两个类。类 A 首先被实例化,它存在的时间比较长,几乎贯穿整个进程的生命周期。在某个时间点,类 B 被创建,类 A 添加了一个对这个新建类的引用。我们假设类 B 是某个用于显示并返回用户指令的用户界面部件。尽管类 B 不再被使用,如果类 A 对类 B 的引用未被清除,类 B 将继续存在并占据内存空间,即使下一次垃圾收集被执行。
        什么时候需要注意内存泄漏
        如果在你的程序执行一段时间之后遇到 java.lang.OutOfMemoryError 的话,内存泄漏无疑是最值得怀疑的。除了这种明显的情况之外,什么时候需要考虑内存泄漏?完美主义的程序员会回答说所有的内存泄漏都需要进行审查和更改。 然而,在跳到这一结论之前还需要考虑其他几点因素,包括程序的生命周期以及内存泄漏的大小。
        考虑一下在一个程序的生命周期里垃圾收集器可能从未执行的情况。 无法保证什么时候 JVM 会调用垃圾收集 -- 即使程序显式调用 System.gc() 通常情况下,垃圾收集器不会自动运行,直到程序需要比目前可用内存还要多的内存。此时,JVM 会首先尝试调用垃圾收集器以获取更多可用内存。如果这个尝试仍旧不能够释放出足够的资源,JVM 将会从操作系统获取更多内存,直到达到所允许内存的最大值
        举个例子来说,一个小型的 Java 应用程序,用来显示一些简单的配置修改的用户界面元素,出现了内存泄漏。垃圾收集器可能在程序关闭之前都不会被调用到,因为 JVM 可能总是有足够的内存来创建程序所需要的所有对象。因此,在这种情况下,即便是一些已死对象在程序运行的时候仍旧占据着内存,但这并不影响实际应用。
        如果开发中的 Java 代码将以每天 24 小时运行在服务器上,这时内存泄漏将会比上面的那个配置工具程序要明显的多了。即便是代码中最小的内存泄漏,在持续运行的情况下最终也将耗尽所有可用内存。
        相反的情况下,即使一个程序只是短暂存活,却分配了大量临时对象(或者少量的占用大量内存的对象),在这些对象不再需要时没有取消引用,这样的 Java 代码也会达到内存限制。
        最后一个值得注意的问题是,不必过于担心(Java 程序所造成的)内存泄漏。Java 内存泄漏不应该被认为是像其他语言中所发生的那样危险,比如 C++ 的内存丢失将永远不会返回给操作系统。Java 应用程序中,我们把不再需要的却占据着内存资源的对象都交给 JVM。所以在理论上来说, 一旦 Java 程序和它的 JVM 关闭掉,所有分配的内存都将归还给操作系统
        如何断定程序具有内存泄漏
        查看一个运行在 Windows NT 平台上的 Java 程序是否具有内存泄漏,你可以简单地在程序运行的时候去观察任务管理器中的内存设置。然而,在观察一些运行中的 Java 程序之后,你会发现,它们跟本地应用程序相比使用更多内存。我开发过的一些 Java 项目会启用 10 到 20 MB 的系统内存。与这个数字相比,本地的操作系统自带的 Windows Explorer 程序使用到 5 MB。
        另外一个关于 Java 程序的内存使用要注意的是典型的运行在 IBM JDK1.1.8 JVM 上的程序似乎在其运行时不断吞噬了越来越多的系统内存。 程序似乎永远不会返回一些内存给操作系统,直到一个非常大的物理内存分配给它。这会不会就是内存泄漏的迹象
        要明白是怎么回事,我们需要熟悉 JVM 是如何将系统内存使用作自己的堆的。在运行 java.exe 时,你 可以使用一些特定的选项来控制垃圾收集的堆的启动容量和最大容量(分别是 -ms 和 -mx) 。Sun 的 JDK 1.1.8 默认使用 1 MB 的启动设置和 16  MB 的最大设置。IBM JDK 1.1.8 默认使用机器物理内存容量的一半作为最大设置。 这些内存设置对 JVM 发生内存溢出时的做法具有直接影响,这时 JVM 可能会继续增长堆内存,而不是等待一个垃圾回收的结束
        因此为了寻找并最终消除内存泄漏,我们需要比任务监视程序更好的工具。当你想检测内存泄漏的时候 内存调试程序 (参见下文的参考资料)可以派上用场了。这些程序 通常会给你关于堆内存里对象的数量、每个对象实例的个数以及对象使用中的内存等一些信息 。此外,它们 还会提供很有用的视图,这些视图可以显示每个对象的引用和引用者,以便你跟踪内存漏洞的来源
        接下来,我将展示如何使用 Sitraka Software 的 JProbe 调试工具来检测和消除内存泄漏,希望会对你就如何部署这些工具并成功消除内存泄漏产生一些启发。
        一个内存泄漏的例子
        这个示例主要展示了我们部门开发的一个商业版应用的一个问题,这个问题在 JDK 1.1.8 上工作了几个小时后被测试人员找出来。这个 Java 应用程序的相关代码和包是由几个不同团队的程序员开发出来的。程序里出现的内存泄漏的原因,我怀疑,是由一些没有真正理解其他(团队)开发的代码的程序员 所引起。讨论中的 Java 代码允许用户不必去写 Palm OS 本地代码来创建 Palm 个人数码助理应用。通过使用图形界面,用户可以创建表单,使用控件对它们进行填充,然后连接控件事件来创建 Palm 应用程序。测试人员发现,这个 Java 应用最终发生了内存溢出——表单和控件的创建和删除延时。开发人员并没有发现这个问题存在,因为他们的机器(相对 Palm)拥有着更多的物理内存。
        为了讨论这个问题,我使用了 JProbe 来断定问题的存在。即使拥有 JProde 提供的强大工具和内存快照,调查仍然是一个繁琐的、反复的过程,它涉及先确定内存泄漏的原因,然后做出代码更改并验证其效果。

        JProbe 有几个选项来控制在一次调试回话期间什么样的信息会被记录。经过一些试验后,我判定获取所需信息的最有效的方式是关掉性能数据收集,专注于捕获的堆数据JProbe 提供了一个叫做运行时堆摘要 的视图来显示 Java 应用程序在一段时间内使用的堆内存的数量。它同时也提供了一个工具栏按钮用来在需要时强制 JVM 执行垃圾收集 --在想要看一下一个类的给定实例不再为 Java 应用程序需要时是否会被垃圾收集,这个功能是很有用的 。下图显示了在一段时间内使用的堆存储量。

运行时堆摘要

        在堆使用情况图中,蓝色部分表示已分配的堆空间量。我启动 Java 程序之后它达到了一个稳定点,我强制垃圾收集器执行,这由绿线之前的蓝色曲线的一个骤降表示(这条绿线表示一个检查点被插入)。接下来,我先是添加而后删 掉了四个表单并再次调用垃圾收集器。检查点之后的蓝色曲线的水平线比检查点之前的蓝色曲线的水平线高的事实告诉我们很可能出现了内存泄漏,因为该程序已经 回归其只有一个简单可见的表单的初始状态。我检查实例确认了泄漏。总之,结果表明 FormFrame 类(表单的主 UI 类)的数量在检查点之后增加了四个。
        寻找原因
        要想将测试人员提交的问题隔离出来,第一步就是提供一些简单的、重复的测试用例。以上面那个例子为例,我发现简单地添加一个表单,删除这个表单,然后强制垃圾收集器的结果是一些关联到已经删除掉的表单的实例仍然存活着。这种问题通过 JProbe 实例摘要视图 来看是显而易见的,视图中统计了堆内存中每个类的实例的个数。
        要定位垃圾收集器工作时具体实例的引用,我使用了 JProbe 的引用画面 ,如下图所示,来断定哪些类仍然在引用已被删除掉的 FormFrame 类。 这是调试这种问题的巧妙地方法之一,我通过它发现了很多不同的对象仍然在引用那些无用的对象 。而通过试错来查明究竟是哪个引用者真正造成这个问题的过程却是相当耗时的。

        在这个案例中,根类(左上角红色的那个)是出现问题的起源。右侧用蓝色突出的那个类就是追踪到的 FormFrame 类。

使用引用画面跟踪内存泄漏

        对于这个具体的例子,找到的罪魁祸首是一个包含一个静态的哈希表的字体管理类。通过引用列表追踪后,我发现根节点是一个静态的哈希表,这个哈希表保存了每 个表单使用的字体。各种表单可以被独立地放大或缩小,所以哈希表包含了一个具有每个指定的表单的所有字体的向量。当表单的缩放视图改变时,带有字体的向量 被获取并选择合适的缩放因素来适应字体大小。
        这个字体管理器的问题是,在创建表单时,当代码将字体向量放进哈希表时,却没有定义表单删除时对向量的移除。因此,这个在整个应用程序的生命周期都存在的 静态的哈希表,却从来没有移除指向每个表单的键值。所以,所有的表单和其相关联的类被遗留在了内存中。
        问题修正

        对于这个问题的简单解决方案就是字体管理器增加一个方法,来允许哈希表的 remove() 方法会在用户删除表单时被调用到。增加的 removeKeyFromHashtables() 方法如下所示:

Java代码  收藏代码
  1. public   void  removeKeyFromHashtables(GraphCanvas graph) {  
  2.   if  (graph !=  null ) {  
  3.     viewFontTable.remove(graph);     // remove key from hashtable   
  4.                                      // to prevent memory leak   
  5.   }  
  6. }  

 

        然后,我在 FormFrame 类里添加了对这个方法的一个调用。FormFrame 使用 Swing 的内部框架来实现表单 UI,因此对于字体管理器的调用被添加到当内部框架完全关闭时所执行的方法,如下所示:

Java代码  收藏代码
  1. /**  
  2. * Invoked when a FormFrame is disposed. Clean out references to prevent   
  3. * memory leaks.  
  4. */   
  5. public   void  internalFrameClosed(InternalFrameEvent e) {  
  6.   FontManager.get().removeKeyFromHashtables(canvas);  
  7.   canvas = null ;  
  8.   setDesktopIcon(null );  
  9. }  


        在我对代码做出修改以后,我使用 调试工具 来确认在相同的测试用例被执行时删除表单所关联到的对象的数目。
        内存泄漏的防止
        可以通过对一些常见问题的注意来防止内存泄漏。 容器类,比如哈希表和向量,是找到引起内存泄漏的常见的地方尤其是当这些类被声明为静态的并存活于应用程序的整个生命周期之中时
        另一个常见(导致内存泄漏的)问题是当你 将一个类注册为事件监听器,却没考虑到当这个类不再需要时将其注销 。还有, 指向其他类的成员变量在恰当的时候要设置为 null
        结束语
        寻找内存泄漏的原因可能是一个繁琐的过程,还没有提到的一点是这将需要特殊的调试工具。然而, 一旦你熟悉了追踪对象引用的工具和模式,你将能够跟踪内存泄漏 。此外, 你还会获得一些有价值的技能,不仅可以节省项目编程投入,而且在以后的项目中你将拥有找出可以防止发生内存泄漏的编程做法的眼光
        相关资料

      
分享到:
评论

相关推荐

    java内存泄露、溢出检查方法和工具

    3. **分析内存泄漏**:MAT提供多种视图来帮助定位问题,其中饼状图是最直观的一种。通过查看" Dominator Tree "视图,可以看到内存消耗最大的对象和它们之间的引用关系。此外,"Leak Suspects"报告会自动分析可能的...

    如何解决Java内存泄漏

    Java内存泄漏是软件开发中一个常见的问题,它不仅会影响应用程序的性能,还可能导致系统崩溃。通过深入了解Java的内存管理机制,并借助于专业的工具如OptimizeIt,可以有效地检测和解决内存泄漏问题。此外,开发者还...

    JAVA程序有内存泄露。

    Java程序中的内存泄露是一个复杂而重要的主题,许多人误以为由于其自动垃圾回收(Garbage Collection, GC)机制的存在,Java应用程序就不会出现内存问题。然而,事实并非如此简单。垃圾回收虽然能够有效地管理内存,...

    java内存泄漏分析工具

    Java内存泄漏是一个严重的问题,它会导致程序性能下降,甚至可能导致应用程序崩溃。为了有效地诊断和解决这类问题,开发者需要借助特定的分析工具。本篇将详细探讨Java内存泄漏及其相关的分析工具。 内存泄漏是指...

    java内存泄漏解决

    在Java开发过程中,经常会遇到内存泄漏的问题,尤其是在长时间运行的应用程序中更为常见。本文将详细介绍如何解决Java内存泄漏问题,帮助开发者更好地理解和应对这一挑战。 #### 二、Java内存模型与内存区域 Java...

    java程序中的内存分配问题

    Java程序在运行时的内存分配是一个关键的概念,它关乎到程序的性能、稳定性和资源管理。这个主题主要涉及以下几个方面: 1. **JVM内存结构**:Java虚拟机(JVM)将内存划分为几个区域,包括堆(Heap)、栈(Stack)...

    JAVA程序内存泄漏综述

    ### JAVA程序内存泄漏综述 #### 一、Java内存泄漏基本概念 在程序开发中,内存管理是一项重要的任务。不同的编程语言采用了不同的内存管理机制。本文重点讨论Java内存泄漏问题,并将其与C/C++的内存泄漏进行对比...

    Java内存泄露_JVM监控工具介绍

    jconsole是一个基于Java Management Extensions(JMX)的实时图形化监测工具,这个工具利用了内建到JVM里面的JMX指令来提供实时的性能和资源的监控,包括了Java程序的内存使用、Heap size、线程的状态、类的分配状态...

    java之内存泄露

    在Java中,内存泄露特指那些虽然仍然可达但已被废弃的对象,即程序不再需要这些对象,但它们依然占据着内存空间。这类对象不会被GC自动回收。而在C++中,如果一个对象被分配了内存但无法访问,则会直接构成内存泄露...

    java 内存泄漏

    内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次又一次地泄露,最终可能耗尽所有可用内存。在Java中,内存泄漏主要发生在Java堆内存中,其主要原因有: - **对象引用不再使用**:当一个对象不再被...

    java内存泄漏问题追踪

    4. "java内存泄露专题研究和应用_石麟.docx"可能提供了更深入的研究和实际案例,包括如何识别特定类型的内存泄漏,以及针对不同场景下的解决方案。而"ha450.jar"可能是一个示例应用或者工具,用于演示内存泄漏问题...

    java使用JMAP定位代码内存泄漏在哪

    Java编程语言在处理大型应用...总的来说,结合JMAP和MAT,我们可以有效地定位和解决Java应用程序中的内存泄漏问题。确保定期监控和分析内存使用情况,可以预防和早期发现潜在的内存问题,从而保持应用的高效稳定运行。

    JAVA内存泄漏分析工具

    MAT提供了一种可视化的界面,通过分析heap dump文件,能够帮助我们深入理解Java应用程序的内存使用情况,找出内存泄漏的源头。 使用MAT,你可以进行以下操作: 1. **内存概述**:查看总体内存分配情况,包括最大的...

    基于Java的内存泄露分析及定位

    Java内存管理是一个关键的议题,尤其对于开发大型和长期运行的应用程序来说,内存泄漏可能导致性能下降,甚至引发严重的系统故障。内存泄漏通常发生在程序错误地管理内存,导致某些不再使用的对象无法被垃圾收集器...

    java内存分析-内存泄露问题.rar

    总的来说,Java内存分析和内存泄露问题是优化应用程序性能的关键环节。理解内存模型、掌握内存分析工具的使用、以及熟悉代码优化技巧,都是Java开发者必备的能力。只有这样,才能确保程序高效、稳定地运行。

    java避免内存泄露

    ### Java避免内存泄露的关键知识点 #### 一、内存泄露的概念及原因 内存泄露是指程序在申请内存后未能释放,导致这部分内存无法再次被利用。在Java中,由于具备垃圾回收机制(GC),理论上开发者不必担心内存泄露...

    java内存分配 内存泄漏

    Java 内存分配和内存泄漏是Java编程中关键的概念,涉及到Java虚拟机(JVM)的工作原理和程序性能优化。 Java 内存分配主要发生在JVM的三个主要区域:堆(Heap)、栈(Stack)和方法区(Method Area)。堆是Java对象...

    Java内存泄露及内存无法回收解决方案

    Java内存管理是编程中至关重要的一个环节,尤其是对于大型、长...理解内存泄漏的原因,掌握排查和解决方法,是提高Java应用程序稳定性和性能的重要途径。通过上述讨论,希望对大家在实际开发中遇到的内存问题有所帮助。

    从 Java 代码到 Java 堆 理解和优化您的应用程序的内存使用

    - **引用类型**:了解软引用、弱引用和虚引用,合理使用它们可以控制对象的生命周期,防止内存泄漏。 4. **Java垃圾收集器**: - **垃圾回收的基本概念**:Java通过垃圾收集器自动回收不再使用的对象,以释放内存...

Global site tag (gtag.js) - Google Analytics