Java中OutOfMemoryError的不同原因及应对措施探究

在Java里 所有对象都存于堆中 它们由新操作符分配 当JVM判定无程序线程能访问它们时 它们会被丢弃 多数情况下 这悄无声息发生 程序员也不再多想 接着 通常在截止日期前一天左右 程序会终止

Java中OutOfMemoryError的不同原因及应对措施探究

Java 堆空间出现内存不足错误

OutOfMemoryError是个让人郁闷的例外。这往往意味着你事儿没做对。要么抓对象时间太长。要么一次处理数据太多。有时。它指的是你控制不了的问题。比如缓存字符串的第三方库。或者部署后不清理的应用程序服务器。有时。它和堆上的对象没关系。

本文会研究OutOfMemoryError的不同缘由。还会研究您能采取的举措。这里以Oracle的HotSpotVM为准。它是我所用的一个JVM。不过大部分内容适用于任何JVM实现。本文是依据网上文献及我自身经验写成的。我不研究JVM内部结构。所以无法站在权威立场发言但我已经面对并解决了很多内存问题。

垃圾收集概述

我已在其他地方详细讲了垃圾收集过程。mark-sweep收集器从“GC ROOT垃圾收集根”出发,遍历整个对象图,标记它碰到的每个对象。没被碰到的对象就是垃圾,会被收集。

Java的垃圾收集方式表明,内存耗尽的唯一途径是持续往图里添加对象,却不删除它们。一般来讲,这是由于把对象添加到了由静态变量引用的集合(通常是映射)中。或者,较少出现的情况是由ThreadLocal或长寿命线程的Runnable所保存的集合导致的。

这跟C以及C++程序里的内存泄漏差异极大。在这些语言当中,当方法调用malloc() 或者new之后,直接返回却不调用对应的free()或者delete,就会出现泄漏情况。这些属于真正的泄漏:要是不借助工具把每个分配和其相应的释放进行匹配,那根本没法恢复内存。

在Java里,“泄漏”的内存只是位置放错了。对于JVM来说,它已被纳入考量。问题在于你,程序员,不清楚其位置。所幸,有办法找到它。

在深入研究这些技术之前,关于垃圾收集器还有最后一件事需知晓:它会尽全力在JVM抛出OutOfMemoryError之前释放内存。这意味着垃圾回收System.gc()无法解决你的问题。你要找到漏洞自行堵住。

设置堆大小

就像学究们常爱指出的,Java语言规范未提及垃圾收集。您能够实现一个永远都不释放内存的JVM(虽说它没什么大用)。Java虚拟机规范留意到堆由垃圾收集器管理,不过它明确保留了实现细节。关于垃圾收集有唯一保证。这一保证是我上面提到的内容。即收集器若存在。它会在JVM抛出OutOfMemoryError之前运行。

实际上 JVM 使用固定大小的堆 它允许在最小和最大界限间按需增长 若不指定这些边界 “client 客户机”JVM 的默认值最小是 2Mb 最大是 64Mb “server 服务器”JVM 则根据可用内存使用默认值2000年时64Mb成为默认值,在此之前默认值是16Mb。当时64Mb看起来肯定很大。然而现代应用程序却极易将其耗尽。

这意味着您通常得使用 -Xms 和 -Xmx 命令行参数来对堆的大小进行显式调整:

使用Java运行MyClass,设置初始堆大小为256兆字节,最大堆大小为512兆字节

设置最小和最大堆大小存在不少经验法则。很明显,最大值得足够高。这样才能容纳程序所需的全部对象。然而,把它设为“刚好足够大”并非良策。因为如此会加大垃圾收集器的工作负载相反 对于长时间运行的应用程序 您应该计划让20至25%的堆保持为空 尽管您的特定应用程序可能需要不同的设置 GC调优是一门超出本文范围的技术

令人惊讶的是 最小堆大小通常比最大值更具重要性 垃圾收集器会尝试维持当前堆的大小 而非增大堆的大小 这有可能致使程序创建并丢弃大量对象 但所需内存永远不会超出初始 也就是最小堆大小 堆会维持在该大小 不过垃圾收集器会持续运行 以使其保持在该大小 在生产环境中 我觉得把最小和最大界限设为相同的值是有道理的

您或许想知道我为何要煞费苦心限制最大大小。毕竟,除非物理内存被实际使用,否则操作系统不会分配物理内存。部分缘由是虚拟地址空间必须容纳的不只是Java堆。要是您在32位系统上运行,那么较大的最大堆大小可能会限制类路径上jar的数量。或者会限制您能创建的线程数量。

限制最大堆大小还有一个原因。它能帮您发现代码中的内存泄漏。开发环境通常不会给应用程序造成压力。要是您在开发时用了很大的最大堆大小。可能直到进入生产环境才会意识到内存泄漏。

观察垃圾收集器工作

所有JVM都提供-verbose:gc选项。该选项能让垃圾收集器在控制台运行时,向控制台写入日志消息。

运行com.kdgregory.example.memory.SimpleAllocator类
[GC从1201K变为1127K(共1984K),耗时0.0020460秒]
耗时0.0196060秒
[GC 从 1127K 变为 1127K(共 1984K),耗时 0.0006680 秒]
Full GC,从1127K变为103K(共1984K),耗时0.0180800秒
[垃圾收集 1127K 变为 1127K(1984K),耗时 0.0001970 秒]
...

Oracle JVM提供了两个附加选项。这些选项能够按代显示细分情况。还能收集开始的时间。

使用java运行com.kdgregory.example.memory.SimpleAllocator,添加-XX:+PrintGCDetails参数,添加-XX:+PrintGCTimeStamps参数
0.095: [垃圾回收 0.095: [新生代: 177K变为64K(576K), 耗时0.0020030秒]0.097: [老年代: 1063K变为103K(1408K), 耗时0.0178500秒] 1201K变为103K(1984K), 耗时0.0201140秒]
0.117: [垃圾回收 0.118: [新生代: 0千字节变为0千字节(576千字节), 耗时0.0007670秒]0.119: [老年代: 1127千字节变为103千字节(1408千字节), 耗时0.0392040秒] 1127千字节变为103千字节(1984千字节), 耗时0.0405130秒]
0.164: [垃圾回收 0.164: [新生代: 0千字节变为0千字节(576千字节), 耗时0.0001990秒]0.164: [老年代: 1127千字节变为103千字节(1408千字节), 耗时0.0173230秒] 1127千字节变为103千字节(1984千字节), 耗时0.0177670秒]
0.183: [垃圾回收 0.184: [新生代: 0千字节变为0千字节(576千字节), 耗时0.0003400秒]0.184: [老年代: 1127千字节变为103千字节(1408千字节), 耗时0.0332370秒] 1127千字节变为103千字节(1984千字节), 耗时0.0342840秒]
...

这能告诉我们什么呢?嗯,首先,垃圾收集频繁发生。每行首个字段是JVM启动后的秒数。我们每隔几百分之一秒就能看到一次集合。实际上,把集合开始时间加到其执行时间(显示在行尾)后,好像收集器一直在运作。

在实际应用程序里 这会成为一个问题 因为收集器会占用程序的CPU周期 如我上面所讲 这可能意味着初始堆大小过小 日志证实了这点 每当堆达到1.1 Mb时 它就会被收集 若看到这种情况 请在更改应用程序前增加-Xms值

这个日志存在一处有趣之处。除首个集合外,年轻代“DefNew”内未存储任何对象。这意味着程序正在分配大数组,除此之外别无其他。这是任何真实世界程序都不应有的情况。若在实际应用程序中见到此情形,我的首要问题会是“这些数组用途何在?”

heap dump 堆转储

堆转储能呈现应用程序正在使用的对象。在最基础情形下,它仅按类统计实例数量与字节数量。您还能够获取显示分配内存代码的转储,并且对历史计数和活动计数加以比较。然而,收集的信息越多,给运行中的JVM增添的开销就越大,所以其中一些技术仅适用于开发环境

如何获得堆转储dump文件

-XX:+

HeapDumpOnOutOfMemoryError命令行参数是生成堆转储dump文件的最简便方式。从名称就能知道,只有在程序内存不够用时,它才会生成转储。这让它适用于生产环境。不过,由于它是事后转储,所以只能提供对象的直方图。此外 它创建了一个二进制文件 您必须使用jhat工具来检查该文件 jhat工具是jdk1.6发行版的一部分 但能读取jdk1.5 jvms生成的文件

jmap命令自1.5起开始提供。它能让您从运行的JVM生成堆转储dump文件、jhat的dump文件或简单的文本直方图。直方图是很好的第一行分析方式。特别是在您于一段较长时间内多次运行它时。或者当您将活动对象计数与历史分配计数进行比较时。

在规模最末端 信息方面和开销方面 都是探查器 探查器利用JVM调试接口 收集对象分配详细信息 涵盖代码行和调用堆栈 这很有用 能看到在一处分配950MB 而非仅知分配了1GB阵列 还能忽略其他位置当然 这些信息存在代价 包括CPU消耗 以及存储原始数据的内存 不允许您在生产环境运行探查器

堆转储分析:活动对象

Java内存泄漏的定义为,分配对象后却不清除对这些对象的所有引用。这就意味着垃圾收集器无法回收它们。堆直方图是开始查找此类泄漏的一种简便方法。它不但能显示堆中的对象,还能显示这些对象所消耗的内存量。简单直方图存在主要缺点。同一类的所有对象会被分组到一起。所以必须做一些检测工作,以此找出它们的分配位置。

调用jmap并使用-histo选项,会得到一个直方图。它能显示自程序启动以来所创建的所有对象(含已收集的对象)的计数及内存消耗情况。使用-histo:live可获取仍在堆上的对象的计数,不管它们是否符合收集条件。

这意味着要获得准确计数。需在调用jmap前强制运行垃圾收集器。若在本地运行应用程序。最简单的方法是用jconsole。在“内存”选项卡顶部。有个标记为“执行GC”的按钮。若在服务器环境运行且暴露了JMX bean。在java.lang组有个gc()操作要是这两个选项都无法使用,那就能够一直等待正常的垃圾回收。可是,要是您存在一个严重的泄漏情况,那么第一个主要的收集很可能会成为OutOfMemoryError的直接先兆。

显示所有对象(包括已收集的对象)的直方图对查找“热门”对象很有用:即那些常被分配又丢弃的对象。若用O(N2)算法创建临时对象,它会立刻在直方图中显现。

使用jmap生成的直方图有两种方法。最有用的技术尤其适用于长时间运行的服务器应用程序。那就是在较长时间段内多次调用“live”选项。然后调查那些计数不断增加的对象。不过根据服务器负载不同。可能需要一小时或更长时间才能获得好的信息。

一种更快的办法是把活动对象和总对象作比较。那些带电计数在总数里占比很大的物体或许存在泄漏情况。下面的示例展示了一个存储库管理器的前十几个条目,这个存储库管理器在近2500个条目中,已为100多个用户服务了几个星期据我了解 这个程序不存在内存泄漏 但其正常操作会引发堆转储 这和有内存泄漏的程序相似

你提供的内容似乎并不是一个完整的、有明确语义的句子,“510> jmap -histo 7626 | more”这样的表述不太清晰其确切含义,难以按照要求进行改写,请你检查或补充更准确的信息。
编号 实例数量 字节数 类名
请你提供一下具体的句子内容,以便我按照要求进行改写。
请你明确一下具体需求,比如对这段内容进行润色、提取关键信息等,以便我更准确地按照要求改写。目前仅给出“339186 63440816 [C]”,不太清楚具体要怎么处理。
请你明确一下具体需求,比如对这段内容进行润色、翻译等,以便我更准确地为你提供服务。仅给出“84847 18748496 [I]”这样的内容,不太清楚具体要怎么改写。
381901  15276040  java语言中的字符串类型
不太明确你具体的需求,仅给出的“5: 30508 13137904 [B”这些内容较难理解其确切要表达的意思,所以无法准确按照要求改写。请补充更详细准确的信息以便能更好地完成任务。
请你明确一下具体的改写要求,仅给出这串数字和“7:         63450        8789976”,不太清楚要改写成什么样的句子呢。
8: 181133 8694384 弱引用 属于Java语言中的引用类型
9: 43675 7651848 一个包含Java语言中对象的数组类型
请你明确一下具体需求,比如对这段内容进行润色、根据其进行拓展等等,以便我更准确地按照要求改写。仅给出“10:         63450        7621520”这样的内容,不太清楚具体要达成什么样的改写效果。
请你明确一下具体需求,仅给出“11: 6729 7040104”这样的内容不太清楚要进行怎样的改写呢。请补充更详细的说明以便我准确为你改写。
12: 134146 6439008 用于Java的哈希映射的条目
请你明确一下问题哦,仅给出“~, 511> jmap -histo:live 7626 | more”这样的内容,不太清楚具体要对它进行怎样的改写要求呢,比如是对其进行翻译、解释含义还是其他什么操作呀?请详细说明一下以便我准确作答。
 num     #instances         #bytes  class name
----------------------------------------------
请你明确一下问题哦,比如对这段内容进行润色、提取关键信息、根据其进行拓展等等,目前不太清楚具体需求,无法准确完成改写。仅给出“200381 35692400 [C]”这样的内容不太明确要怎么改写呢。
请你明确一下具体需求呀,仅给出“22804 12168040 [I]”,不太清楚要对其进行怎样的改写呢。你可以详细说明一下改写的具体要求,比如是根据这些数字和字母进行描述、扩写、润色等等,以便我能更准确地为你提供改写后的内容。
不太明确你具体的需求,仅给出的“17959 9848496 [B]”这些内容较难理解准确意图并进行改写,请补充更详细的背景或要求等信息。
请你明确一下改写要求,仅给出这串数字,不太清楚具体要如何进行改写呢。
请你明确一下具体需求,仅给出这串数字,不太清楚具体要进行怎样的改写,你可以补充更详细的要求或说明。

9: 93830 5254480  Java语言中的线程局部变量类的线程局部变量映射类的条目
10: 107128 5142144 弱引用 属于Java语言中的引用类型
请你明确一下具体的改写要求,仅给出这串数字不太清楚要如何进行改写呢。
不太明确你提供的这串数字具体要表达什么意思以及改写的具体要求呢,可以进一步说明一下,以便我更准确地为你改写。

查找内存泄漏时 要从消耗内存最多的对象着手 这听着挺明显 但有时它们并非泄漏源头 不过这是最佳开端 在此情形下 char[]实例消耗内存最多 虽说总内存是60Mb 但这不成问题 令人担忧的是 “实时”计数近乎“分配”计数的三分之二

一个普通程序会分配对象,接着释放它们。要是它长时间持有对象,这就可能是泄漏。不过话说回来,一个特定程序所经历的“搅动”数量,取决于程序在做什么。字符数组几乎总是和字符串有关联。有些程序在其生命周期中会保留大量字符串。比如,基于JSP的应用服务器会为JSP里的每个HTML块定义字符串。这个特殊程序的确提供HTML。但其对字符串的需求不那么明确。它提供目录列表。而非大量静态文本。若内存不足。我会尝试找出这些字符串被分配到何处。以及为何未丢弃它们。

另一个值得留意的领域是字节数组(“[B”)。JDK里有不少类会用到它们(比如BufferedInputStream)。然而在应用程序代码中却很少能见到它们。一般来说它们被用作缓冲区。不过缓冲区应当是短期的。在此处我们发现超过一半的字节数组仍被视作活动对象这令人担忧 它突出了简单直方图的一个问题 单个类的所有对象都被分组在一起 对于应用程序对象 这不一定是问题 因为它们通常分配在程序的一个部分中 但是字节数组是到处分配的 而且大多数分配都隐藏在库中我们是该搜索调用new byte[]的代码呢?还是该搜索调用new ByteArrayOutputStream()的代码呢?

堆转储输出采用类名的“内部形式”。多数情形下,名称是您所期待的。除了JVM内部的数组和类。后面这些以“

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

23阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

27阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

23阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读