Java内存溢出(OOM)异常完全指南

澳门新葡亰手机版 6

在Java中,所有对象都存储在堆中。他们通过new关键字来进行分配,JVM会检查是否所有线程都无法在访问他们了,并且会将他们进行回收。在大多数时候程序员都不会有一丝一毫的察觉,这些工作都被静悄悄的执行。但是,有时候在发布前的最后一天,程序挂了。

我的职业生涯中见过数以千计的内存溢出异常均与下文中的8种情况相关。本文分析什么情况会导致这些异常出现,提供示例代码的同时为您提供解决指南。Nikita
Salnikov-Tarnovski
Plumbr Co-Founder and VP of
Engineering本文内容来源于Plumbr,对原文内容有删减和补充

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

这也许是目前最为完整的Java OOM异常的解决指南。

OutOfMemoryError是一个让人很郁闷的异常。它通常说明你干了写错误的事情:没必要的长时间保存一些没必要的数据,或者同一时间处理了过多的数据。有些时候,这些问题并不一定受你的控制,比如说一些第三方的库对一些字符串做了缓存,或者一些应用服务器在部署的时候并没有进行清理。并且,对于堆中已经存在的对象,我们往往拿他们没办法。

1、java.lang.OutOfMemoryError:Java heap space

Java应用程序在启动时会指定所需要的内存大小,它被分割成两个不同的区域:Heap spacePermgen

澳门新葡亰手机版 1JVM内存模型示意图这两个区域的大小可以在JVM启动时通过参数-Xmx-XX:MaxPermSize设置,如果你没有显式设置,则将使用特定平台的默认值。

当应用程序试图向堆空间添加更多的数据,但堆却没有足够的空间来容纳这些数据时,将会触发java.lang.OutOfMemoryError: Java heap space异常。需要注意的是:即使有足够的物理内存可用,只要达到堆空间设置的大小限制,此异常仍然会被触发。

触发java.lang.OutOfMemoryError: Java heap space最常见的原因就是应用程序需要的堆空间是XXL号的,但是JVM提供的却是S号。解决方法也很简单,提供更大的堆空间即可。除了前面的因素还有更复杂的成因:

  • 流量/数据量峰值:应用程序在设计之初均有用户量和数据量的限制,某一时刻,当用户数量或数据量突然达到一个峰值,并且这个峰值已经超过了设计之初预期的阈值,那么以前正常的功能将会停止,并触发java.lang.OutOfMemoryError: Java heap space异常。
  • 内存泄漏:特定的编程错误会导致你的应用程序不停的消耗更多的内存,每次使用有内存泄漏风险的功能就会留下一些不能被回收的对象到堆空间中,随着时间的推移,泄漏的对象会消耗所有的堆空间,最终触发java.lang.OutOfMemoryError: Java heap space错误。

这篇文章分析了导致OutOfMemoryError的不同原因,以及你该怎样应对这种原因的方法。以下分析仅限于Sun
Hotspot虚拟机,但是大多数结论都适用于其他任何的JVM实现。它们大多数基于网上的文章以及我自己的经验。我没有直接做JVM开发的工作,因此结论并不代表JVM的作者。但是我确实曾经遇到过并解决了很多内存相关的问题。

①、简单示例

首先看一个非常简单的示例,下面的代码试图创建2 x 1024 x
1024个元素的整型数组,当你尝试编译并指定12M堆空间运行时(java -Xmx12m
OOM)将会失败并抛出java.lang.OutOfMemoryError: Java heap space错误,而当你指定13M堆空间时,将正常的运行。

class OOM { static final int SIZE=2*1024*1024; public static void main(String[] a) { int[] i = new int[SIZE]; }}

运行如下:

D:>javac OOM.javaD:>java -Xmx12m OOMException in thread "main" java.lang.OutOfMemoryError: Java heap space at OOM.main(OOM.java:4)D:>java -Xmx13m OOM

垃圾回收介绍

我在这篇文章中已经详细介绍了垃圾回收的过程。简单的说,标记-清除算法(mark-sweep
collect)以garbage collection roots作为扫描的起点,并对整个对象图进行扫描,对所有可达的对象进行标记。那些没有被标记的对象会被清除并回收。

Java的垃圾回收算法过程意味着如果出现了OOM,那么说明你在不停的往对象图中添加对象并且没有移除它们。这通常是因为你在往一个集合类中添加了很多对象,比如Map,并且这个集合对象是static的。或者,这个集合类被保存在了ThreadLocal对象中,而这个对应的Thread却又长时间的运行,一直不退出。

这与C和C++的内存泄露完全不一样。在这些语言中,如果一些方法调用了malloc()或者new,并且在方法退出的时候没有调用相应的free()或者delete,那么内存就会产生泄露。这些是真正意义上得泄露,你在这个进程范围内不可能再恢复这些内存,除非使用一些特定的工具来保证每一个内存分配方法都有其对应的内存释放操作相对应。

在java中,“泄露”这个词往往被误用了。因为从JVM的角度来说,所有的内存都是被良好管理的。问题仅仅是作为程序员的你不知道这些内存是被哪些对象占用了。但是幸运的是,你还是有办法去找到和定位它们。

在深入探讨之前,你还有最后一件关于垃圾收集的知识需要了解:JVM会尽最大的能力去释放内存,直到发生OOM。这就意味着OOM不能通过简单的调用System.gc()来解决,你需要找到这些“泄露”点,并自己处理它们。

②、内存泄漏示例

在Java中,当开发者创建一个新对象(比如:new Integer)时,不需要自己开辟内存空间,而是把它交给JVM。在应用程序整个生命周期类,JVM负责检查哪些对象可用,哪些对象未被使用。未使用对象将被丢弃,其占用的内存也将被回收,这一过程被称为垃圾回收。JVM负责垃圾回收的模块集合被称为垃圾回收器(GC)。

Java的内存自动管理机制依赖于GC定期查找未使用对象并删除它们。Java中的内存泄漏是由于GC无法识别一些已经不再使用的对象,而这些未使用的对象一直留在堆空间中,这种堆积最终会导致java.lang.OutOfMemoryError: Java heap space错误。

我们可以非常容易的写出导致内存泄漏的Java代码:

public class KeylessEntry { static class Key { Integer id; Key(Integer id) { this.id = id; } @Override public int hashCode() { return id.hashCode(); } } public static void main(String[] args) { Map<Key,String> m = new HashMap<Key,String>(); while { for(int i=0;i<10000;i++) { if(!m.containsKey(new Key { m.put(new Key, "Number:" + i); } } } }}

代码中HashMap为本地缓存,第一次while循环,会将10000个元素添加到缓存中。后面的while循环中,由于key已经存在于缓存中,缓存的大小将一直会维持在10000。但事实真的如此吗?由于Key实体没有实现equals()方法,导致for循环中每次执行m.containsKey(new Key结果均为false,其结果就是HashMap中的元素将一直增加。

随着时间的推移,越来越多的Key对象进入堆空间且不能被垃圾收集器回收(m为局部变量,GC会认为这些对象一直可用,所以不会回收),直到所有的堆空间被占用,最后抛出java.lang.OutOfMemoryError:Java heap space

上面的代码直接运行可能很久也不会抛出异常,可以在启动时使用-Xmx参数,设置堆内存大小,或者在for循环后打印HashMap的大小,执行后会发现HashMap的size一直再增长。

解决方法也非常简单,只要Key实现自己的equals方法即可:

Overridepublic boolean equals { boolean response = false; if (o instanceof Key) { response = .id).equals; } return response;}

澳门新葡亰手机版,第一个解决方案是显而易见的,你应该确保有足够的堆空间来正常运行你的应用程序,在JVM的启动配置中增加如下配置:

-Xmx1024m

上面的配置分配1024M堆空间给你的应用程序,当然你也可以使用其他单位,比如用G表示GB,K表示KB。下面的示例都表示最大堆空间为1GB:

java -Xmx1073741824 com.mycompany.MyClassjava -Xmx1048576k com.mycompany.MyClassjava -Xmx1024m com.mycompany.MyClassjava -Xmx1g com.mycompany.MyClass

然后,更多的时候,单纯地增加堆空间不能解决所有的问题。如果你的程序存在内存泄漏,一味的增加堆空间也只是推迟java.lang.OutOfMemoryError: Java heap space错误出现的时间而已,并未解决这个隐患。除此之外,垃圾收集器在GC时,应用程序会停止运行直到GC完成,而增加堆空间也会导致GC时间延长,进而影响程序的吞吐量。

如果你想完全解决这个问题,那就好好提升自己的编程技能吧,当然运用好Debuggers, profilers, heap dump analyzers等工具,可以让你的程序最大程度的避免内存泄漏问题。

设置堆大小

学院派的人非常喜欢说Java语言规范并没有对垃圾收集器进行任何约定,你甚至可以实现一个从来不释放内存的JVM(实际是毫无意义的)。Java虚拟机规范中提到堆是由垃圾回收器进行管理,但是却没有说明任何相关细节。仅仅说了我刚才提到的那句话:垃圾回收会发生在OOM之前。

实际上,Sun
Hotspot虚拟机使用了一个固定大小的堆空间,并且允许在最小空间和最大空间之间进行自动增长。如果你没有指定最小值和最大值,那么对于’client’模式将会默认使用2Mb最为最小值,64Mb最为最大值;对于’server’模式,JVM会根据当前可用内存来决定默认值。2000年后,默认的最大堆大小改为了64M,并且在当时已经认为足够大了(2000年前的时候默认值是16M),但是对于现在的应用程序来说很容易就用完了。

这意味着你需要显示的通过JVM参数来指定堆的最小值和最大值:

java -Xms256m -Xmx512m MyClass

这里有很多经验上得法则来设定最大值和最小值。显然,堆的最大值应该设定为足以容下整个应用程序所需要的全部对象。但是,将它设定为“刚刚好足够大”也不是一个很好的注意,因为这样会增加垃圾回收器的负载。因此,对于一个长时间运行的应用程序,你一般需要保持有20%-25%的空闲堆空间。(你得应用程序可能需要不同的参数设置,GC调优是一门艺术,并且不在该文章讨论范围内)

让你奇怪的时,设置合适的堆的最小值往往比设置合适的最大值更加重要。垃圾回收器会尽可能的保证当前的的堆大小,而不是不停的增长堆空间。这会导致应用程序不停的创建和回收大量的对象,而不是获取新的堆空间,相对于初始(最小)堆空间。Java堆会尽量保持这样的堆大小,并且会不停的运行GC以保持这样的容量。因此,我认为在生产环境中,我们最好是将堆的最小值和最大值设置成一样的。

你可能会困惑于为什么Java堆会有一个最大值上限:操作系统并不会分配真正的物理内存,除非他们真的被使用了。并且,实际使用的虚拟内存空间实际上会比Java堆空间要大。如果你运行在一个32位系统上,一个过大的堆空间可能会限制classpath中能够使用的jar的数量,或者你可以创建的线程数。

另外一个原因是,一个受限的最大堆空间可以让你及时发现潜在的内存泄露问题。在开发环境中,对应用程序的压力往往是不够的,如果你在开发环境中就拥有一个非常大得堆空间,那么你很有可能永远不会发现可能的内存泄露问题,直到进入产品环境。

2、java.lang.OutOfMemoryError:GC overhead limit exceeded

Java运行时环境(JRE)包含一个内置的垃圾回收进程,而在许多其他的编程语言中,开发者需要手动分配和释放内存。

Java应用程序只需要开发者分配内存,每当在内存中特定的空间不再使用时,一个单独的垃圾收集进程会清空这些内存空间。垃圾收集器怎样检测内存中的某些空间不再使用已经超出本文的范围,但你只需要相信GC可以做好这些工作即可。

默认情况下,当应用程序花费超过98%的时间用来做GC并且回收了不到2%的堆内存时,会抛出java.lang.OutOfMemoryError:GC overhead limit exceeded错误。具体的表现就是你的应用几乎耗尽所有可用内存,并且GC多次均未能清理干净。

java.lang.OutOfMemoryError:GC overhead limit exceeded错误是一个信号,示意你的应用程序在垃圾收集上花费了太多时间但却没有什么卵用。默认超过98%的时间用来做GC却回收了不到2%的内存时将会抛出此错误。那如果没有此限制会发生什么呢?GC进程将被重启,100%的CPU将用于GC,而没有CPU资源用于其他正常的工作。如果一个工作本来只需要几毫秒即可完成,现在却需要几分钟才能完成,我想这种结果谁都没有办法接受。

所以java.lang.OutOfMemoryError:GC overhead limit exceeded也可以看做是一个fail-fast实战的实例。

下面的代码初始化一个map并在无限循环中不停的添加键值对,运行后将会抛出GC overhead limit exceeded错误:

public class Wrapper { public static void main(String args[]) throws Exception { Map map = System.getProperties(); Random r = new Random(); while  { map.put(r.nextInt(), "value"); } }}

正如你所预料的那样,程序不能正常的结束,事实上,当我们使用如下参数启动程序时:

java -Xmx100m -XX:+UseParallelGC Wrapper

我们很快就可以看到程序抛出java.lang.OutOfMemoryError: GC overhead limit exceeded错误。但如果在启动时设置不同的堆空间大小或者使用不同的GC算法,比如这样:

java -Xmx10m -XX:+UseParallelGC Wrapper

我们将看到如下错误:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Hashtable.rehash(Unknown Source) at java.util.Hashtable.addEntry(Unknown Source) at java.util.Hashtable.put(Unknown Source) at cn.moondev.Wrapper.main(Wrapper.java:12)

使用以下GC算法:-XX:+UseConcMarkSweepGC
或者-XX:+UseG1GC,启动命令如下:

java -Xmx100m -XX:+UseConcMarkSweepGC Wrapperjava -Xmx100m -XX:+UseG1GC Wrapper

得到的结果是这样的:

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

错误已经被默认的异常处理程序捕获,并且没有任何错误的堆栈信息输出。

以上这些变化可以说明,在资源有限的情况下,你根本无法无法预测你的应用是怎样挂掉的,什么时候会挂掉,所以在开发时,你不能仅仅保证自己的应用程序在特定的环境下正常运行。

首先是一个毫无诚意的解决方案,如果你仅仅是不想看到java.lang.OutOfMemoryError:GC overhead limit exceeded的错误信息,可以在应用程序启动时添加如下JVM参数:

-XX:-UseGCOverheadLimit

但是强烈建议不要使用这个选项,因为这样并没有解决任何问题,只是推迟了错误出现的时间,错误信息也变成了我们更熟悉的java.lang.OutOfMemoryError: Java heap space而已。

另一个解决方案,如果你的应用程序确实内存不足,增加堆内存会解决GC overhead limit问题,就如下面这样,给你的应用程序1G的堆内存:

java -Xmx1024m com.yourcompany.YourClass

但如果你想确保你已经解决了潜在的问题,而不是掩盖java.lang.OutOfMemoryError: GC overhead limit exceeded错误,那么你不应该仅止步于此。你要记得还有profilersmemory dump analyzers这些工具,你需要花费更多的时间和精力来查找问题。还有一点需要注意,这些工具在Java运行时有显著的开销,因此不建议在生产环境中使用。

在运行时跟踪垃圾回收

所有的JVM实现都提供了-verbos:gc选项,它可以让垃圾回收器在工作的时候打印出日志信息:

java -verbose:gc com.kdgregory.example.memory.SimpleAllocator
[GC 1201K->1127K(1984K), 0.0020460 secs]
[Full GC 1127K->103K(1984K), 0.0196060 secs]
[GC 1127K->1127K(1984K), 0.0006680 secs]
[Full GC 1127K->103K(1984K), 0.0180800 secs]
[GC 1127K->1127K(1984K), 0.0001970 secs]
...

Sun的JVM提供了额外的两个参数来以内存带分类输出,并且会显示垃圾收集的开始时间:

java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps com.kdgregory.example.memory.SimpleAllocator
0.095: [GC 0.095: [DefNew: 177K->64K(576K), 0.0020030 secs]0.097: [Tenured: 1063K->103K(1408K), 0.0178500 secs] 1201K->103K(1984K), 0.0201140 secs]
0.117: [GC 0.118: [DefNew: 0K->0K(576K), 0.0007670 secs]0.119: [Tenured: 1127K->103K(1408K), 0.0392040 secs] 1127K->103K(1984K), 0.0405130 secs]
0.164: [GC 0.164: [DefNew: 0K->0K(576K), 0.0001990 secs]0.164: [Tenured: 1127K->103K(1408K), 0.0173230 secs] 1127K->103K(1984K), 0.0177670 secs]
0.183: [GC 0.184: [DefNew: 0K->0K(576K), 0.0003400 secs]0.184: [Tenured: 1127K->103K(1408K), 0.0332370 secs] 1127K->103K(1984K), 0.0342840 secs]
...

从上面的输出我们可以看出什么?首先,前面的几次垃圾回收发生的非常频繁。每行的第一个字段显示了JVM启动后的时间,我们可以看到在一秒钟内有上百次的GC。并且,还加入了每次GC执行时间的开始时间(在每行的最后一个字段),可以看出垃圾搜集器是在不停的运行的。

但是在实时系统中,这会造成很大的问题,因为垃圾搜集器的执行会夺走很多的CPU周期。就像我之前提到的,这很可能是由于初始堆大小设置的太小了,并且GC日志显示了:每次堆的大小达到了1.1Mb,它就开始执行GC。如果你得系统也有类似的现象,请在改变自己的应用程序之前使用-Xms来增大初始堆大小。

对于GC日志还有一些很有趣的地方:除了第一次垃圾回收,没有任何对象是存放在了新生代(“DefNew”)。这说明了这个应用程序分配了包含大量数据的数组,在显示世界里这是很少出现的。如果在一个实时系统中出现这样的状况,我想到的第一个问题是“这些数组拿来干什么用?”。

3、java.lang.OutOfMemoryError:Permgen space

Java中堆空间是JVM管理的最大一块内存空间,可以在JVM启动时指定堆空间的大小,其中堆被划分成两个不同的区域:新生代和老年代,新生代又被划分为3个区域:EdenFrom SurvivorTo Survivor,如下图所示。

澳门新葡亰手机版 2图片来源:并发编程网

java.lang.OutOfMemoryError: PermGen space错误就表明持久代所在区域的内存已被耗尽。

要理解java.lang.OutOfMemoryError: PermGen space出现的原因,首先需要理解Permanent Generation Space的用处是什么。持久代主要存储的是每个类的信息,比如:类加载器引用运行时常量池(所有常量、字段引用、方法引用、属性)字段数据方法数据方法代码方法字节码等等。我们可以推断出,PermGen的大小取决于被加载类的数量以及类的大小。

因此,我们可以得出出现java.lang.OutOfMemoryError: PermGen space错误的原因是:太多的类或者太大的类被加载到permanent generation

堆转储(Heap Dumps)

一个堆转储可以显示你在应用程序说使用的所有对象。从基础上讲,它仅仅反映了对象实例的数量和类文件所占用的字节数。当然你也可以将分配这些内存的代码一起dump出来,并且对比历史存货对象。但是,如果你要dump的数据信息越多,JVM的负载就会越大,因此这些技术仅仅应该使用在开发环境中。

①、最简单的示例

正如前面所描述的,PermGen的使用与加载到JVM类的数量有密切关系,下面是一个最简单的示例:

import javassist.ClassPool;public class MicroGenerator { public static void main(String[] args) throws Exception { for (int i = 0; i < 100_000_000; i++) { generate("cn.moondev.User" + i); } } public static Class generate(String name) throws Exception { ClassPool pool = ClassPool.getDefault(); return pool.makeClass.toClass(); }}

运行时请设置JVM参数:-XX:MaxPermSize=5m,值越小越好。需要注意的是JDK8已经完全移除持久代空间,取而代之的是元空间(Metaspace),所以示例最好的JDK1.7或者1.6下运行。

代码在运行时不停的生成类并加载到持久代中,直到撑满持久代内存空间,最后抛出java.lang.OutOfMemoryError:Permgen space。代码中类的生成使用了javassist库。

怎样获得一个内存转储

命令行参数-XX:+HeapDumpOnOutOfMemoryError是最简单的方式生成内存转储。就像它的名字所说的,它会在内存被用完的时候(发生OOM)进行转储,这在产品环境非常好用。但是由于这个是一种事后转储(已经发生了OOM),它只能提供一种历史性的数据。它会产生一个二进制文件,你可以使用jhat来操作该文件(这个工具在JDK1.6中已经提供,但是可以读取JDK1.5产生的文件)。

你可以使用jmap(JDK1.5之后就自带了)来为一个运行中得java程序产生堆转储,可以产生一个在jhat中使用的dump文件,或者是一个存文本的统计文件。统计图可以在进行分析时优先使用,特别是你要在一段时间内多次转储堆并进行分析和对比历史数据。

从转储内容和JVM的负荷的扩展性上考虑的话,可以使用profilers。Profiles使用JVM的调试接口(debuging
interface)来搜集对象的内存分配信息,包括具体的代码行和方法调用栈。这个是非常有用的:不仅仅可以知道你分配了一个数GB的数组,你还可以知道你在一个特定的地方分配了950MB的对象,并且直接忽略其他的对象。当然,这些结果肯定会对JVM有开销,包括CPU的开销和内存的开销(保存一些原始数据)。你不应该在产品环境中使用profiles。

②、Redeploy-time

更复杂和实际的一个例子就是Redeploy(重新部署,你可以想象一下你开发时,点击eclipse的reploy按钮或者使用idea时按ctrl
+
F5时的过程)。在从服务器卸载应用程序时,当前的classloader以及加载的class在没有实例引用的情况下,持久代的内存空间会被GC清理并回收。如果应用中有类的实例对当前的classloader的引用,那么Permgen区的class将无法被卸载,导致Permgen区的内存一直增加直到出现Permgen space错误。

不幸的是,许多第三方库以及糟糕的资源处理方式(比如:线程、JDBC驱动程序、文件系统句柄)使得卸载以前使用的类加载器变成了一件不可能的事。反过来就意味着在每次重新部署过程中,应用程序所有的类的先前版本将仍然驻留在Permgen区中,你的每次部署都将生成几十甚至几百M的垃圾。

就以线程和JDBC驱动来说说。很多人都会使用线程来处理一下周期性或者耗时较长的任务,这个时候一定要注意线程的生命周期问题,你需要确保线程不能比你的应用程序活得还长。否则,如果应用程序已经被卸载,线程还在继续运行,这个线程通常会维持对应用程序的classloader的引用,造成的结果就不再多说。多说一句,开发者有责任处理好这个问题,特别是如果你是第三方库的提供者的话,一定要提供线程关闭接口来处理清理工作

让我们想象一个使用JDBC驱动程序连接到关系数据库的示例应用程序。当应用程序部署到服务器上的时:服务器创建一个classloader实例来加载应用所有的类(包含相应的JDBC驱动)。根据JDBC规范,JDBC驱动程序(比如:com.mysql.jdbc.Driver)会在初始化时将自己注册到java.sql.DriverManager中。该注册过程中会将驱动程序的一个实例存储在DriverManager的静态字段内,代码可以参考:

// com.mysql.jdbc.Driver源码package com.mysql.jdbc;public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver; } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } }}// // // // // // // // // //// 再看下DriverManager对应代码private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();public static synchronized void registerDriver(java.sql.Driver driver,DriverAction da) throws SQLException { if(driver != null) { registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); } else { throw new NullPointerException(); }}

现在,当从服务器上卸载应用程序的时候,java.sql.DriverManager仍将持有那个驱动程序的引用,进而持有用于加载应用程序的classloader的一个实例的引用。这个classloader现在仍然引用着应用程序的所有类。如果此程序启动时需要加载2000个类,占用约10MB永久代内存,那么只需要5~10次重新部署,就会将默认大小的永久代塞满,然后就会触发java.lang.OutOfMemoryError: PermGen space错误并崩溃。

堆转储分析:live objects

Java中的内存泄露是这样定义的:你在内存中分配了一些对象,但是并没有清除掉所有对它们的引用,也就是说垃圾搜集器不能回收它们。使用堆转储直方图可以很容易的查找这些泄露对象:它不仅仅可以告诉你在内存中分配了哪些对象,并且显示了这些对象在内存中所占用的大小。但是这种直方图最大的问题是:对于同一个类的所有对象都被聚合(group)在一起了,所以你还需要进一步做一些检测来确定这些内存在哪里被分配了。

使用jmap并且加上-histo参数可以为你产生一个直方图,它显示了从程序运行到现在所有对象的数量和内存消耗,并且包含了已经被回收的对象和内存。如果使用-histo:live参数会显示当前还在堆中得对象数量及其内存消耗,不论这些对象是否要被垃圾搜集器进行回收。

也就是说,如果你要得到一个当前时间下得准确信息,你需要在使用jmap之前强制执行一次垃圾回收。如果你的应用程序是运行在本地,最简单的方式是直接使用jconsole:在’Memory’标签下,有一个’Perform
GC’的按钮。如果应用程序是运行在服务端环境,并且JMX
beans被暴露了,MemoryMXBean有一个gc()操作。如果上述的两种方案都没办法满足你得要求,你就只有等待JVM自己触发一次垃圾搜集过程了。如果你有一个很严重的内存泄露问题,那么第一次major
collection很可能预示着不久后就会OOM。

有两种方法使用jmap产生的直方图。其中最有效的方法,适用于长时间运行的程序,可以使用带live的命令行参数,并且在一段时间内多次使用该命令,检查哪些对象的数量在不断增长。但是,根据当前程序的负载,该过程可能会花费1个小时或者更多的时间。

另外一个更加快速的方式是直接比较当前存活的对象数量和总的对象数量。如果有些对象占据了总对象数量的大部分,那么这些对象很有可能发生内存泄露。这里有一个例子,这个应用程序已经连续几周为100多个用户提供了服务,结果列举了前12个数量最多的对象。据我所知,这个程序没有内存泄露的问题,但是像其他应用程序一样做了常规性的内存转储分析操作。

~, 510> jmap -histo 7626 | more

 num     #instances         #bytes  class name
----------------------------------------------
   1:        339186       63440816  [C
   2:         84847       18748496  [I
   3:         69678       15370640  [Ljava.util.HashMap$Entry;
   4:        381901       15276040  java.lang.String
   5:         30508       13137904  [B
   6:        182713       10231928  java.lang.ThreadLocal$ThreadLocalMap$Entry
   7:         63450        8789976  <constMethodKlass>
   8:        181133        8694384  java.lang.ref.WeakReference
   9:         43675        7651848  [Ljava.lang.Object;
  10:         63450        7621520  <methodKlass>
  11:          6729        7040104  <constantPoolKlass>
  12:        134146        6439008  java.util.HashMap$Entry

~, 511> jmap -histo:live 7626 | more

 num     #instances         #bytes  class name
----------------------------------------------
   1:        200381       35692400  [C
   2:         22804       12168040  [I
   3:         15673       10506504  [Ljava.util.HashMap$Entry;
   4:         17959        9848496  [B
   5:         63208        8766744  <constMethodKlass>
   6:        199878        7995120  java.lang.String
   7:         63208        7592480  <methodKlass>
   8:          6608        6920072  <constantPoolKlass>
   9:         93830        5254480  java.lang.ThreadLocal$ThreadLocalMap$Entry
  10:        107128        5142144  java.lang.ref.WeakReference
  11:         93462        5135952  <symbolKlass>
  12:          6608        4880592  <instanceKlassKlass>

当我们要尝试寻找内存泄露问题,可以从消耗内存最多的对象着手。这听上去很明显,但是往往它们并不是内存泄露的根源。但是,它们任然是应该最先下手的地方,在这个例子中,最占用内存的是一些char[]的数组对象(总大小是60MB,基本上没有任何问题)。但是很奇怪的是当前存货(live)的对象竟然占了历史分配的总对象大小的三分之二。

一般来说,一个应用程序会分配对象,并且在不久之后就会释放它们。如果保存一些对象的应用过长的时间,就很有可能会导致内存泄露。但是虽然是这么说的,实际上还是要具体情况具体分析,主要还是要看这个程序到底在做什么事情。字符数组对象(char[])往往和字符串对象(String)同时存在,大部分的应用程序都会在整个运行过程中一直保持着一些字符串对象的引用。例如,基于JSP的web应用程序在JSP页面中定义了很多HTML字符串表达式。这种特殊的应用程序提供HTML服务,但是它们需要保持字符串引用的需求却不一定那么清晰:它们提供的是目录服务,并不是静态文本。如果我遇到了OOM,我就会尝试找到这些字符串在哪里被分配,为什么没有被释放。

另一个需要关注的是字节数组([B)。在JDK中有很多类都会使用它们(比如BufferedInputStream),但是却很少在应用程序代码中直接看到它们。通常它们会被用作缓存(buffer),但是缓存的生命周期不会很长。在这个例子中我们看到,有一半的字节数组任然保持存活。这个是令人担忧的,并且它凸显了直方图的一个问题:所有的对象都按照它的类型被分组聚合了。对于应用程序对象(非JDK类型或者原始类型,在应用程序代码中定义的类),这不是一个问题,因为它们会在程序的一个部分被集中分配。但是字节数组有可能会在任何地方被定义,并且在大多数应用程序中都被隐藏在一些库中。我们是否应当搜索调用了new byte[]或者new ByteArrayOutputStream()的代码?

① 解决初始化时的OutOfMemoryError

当在应用程序启动期间触发由于PermGen耗尽引起的OutOfMemoryError时,解决方案很简单。
应用程序需要更多的空间来加载所有的类到PermGen区域,所以我们只需要增加它的大小。
为此,请更改应用程序启动配置,并添加-XX:MaxPermSize参数,类似于以下示例:

java -XX:MaxPermSize=512m com.yourcompany.YourClass

堆转储分析:相关的原因和影响分析

为了找到导致内存泄露的最终原因,仅仅考虑按照类别(class)的分组的内存占用字节数是不够的。你还需要将应用程序分配的对象和内存泄露的对象关联起来考虑。一个方法是更加深入查看对象的数量,以便将具有关联性的对象找出来。下面是一个具有严重内存问题的程序的转储信息:

num     #instances         #bytes  class name
----------------------------------------------
   1:       1362278      140032936  [Ljava.lang.Object;
   2:         12624      135469922  [B
  ...
   5:        352166       45077248  com.example.ItemDetails
  ...
   9:       1360742       21771872  java.util.ArrayList
  ...
  41:          6254         200128  java.net.DatagramPacket

如果你仅仅去看信息的前几行,你可能会去定位Object[]或者byte[],这些都是徒劳的。真正的问题出在ItemDetails和DatagramPacket上:前者分配了大量的ArrayList,进而又分配了大量的Object[];后者使用了大量的byte[]来保存从网络上接收到的数据。

第一个问题,分配了大量的数组,实际上不是内存泄露。ArrayList的默认构造函数会分配容量是10的数组,但是程序本身一般只使用1个或者2个槽位,这对于64位JVM来说会浪费62个字节的内存空间。一个更好的涉及方案是仅仅在有需要的时候才使用List,这样对每个实例来说可以节约额外的48个字节。但是,对于这种问题也可以很轻易的通过加内存来解决,因为现在的内存非常便宜。

但是对于datagram的泄露就比较麻烦(如同定位这个问题一样困难):这表明接收到的数据没有被尽快的处理掉。

为了跟踪问题的原因和影响,你需要知道你的程序是怎样在使用这些对象。不多的程序才会直接使用Object[]:如果确实要使用数组,程序员一般都会使用带类型的数组。但是,ArrayList会在内部使用。但是仅仅知道ArrayList的内存分配是不够的,你还需要顺着调用链往上走,看看谁分配了这些ArrayList。

其中一个方法是对比相关的对象数量。在上面的例子中,byte[]和DatagramPackage的关系是很明显的:其中一个基本上是另外一个的两倍。但是ArrayList和ItemDetails的关系就不那么明显了。(实际上一个ItemDetails中会包含多个ArrayList)

这往往是个陷阱,让你去关注那么数量最多的一些对象。我们有数百万的ArrayList对象,并且它们分布在不同的class中,也有可能集中在一小部分class中。尽管如此,数百万的对象引用是很容易被定位的。就算有10来个class可能会包含ArrayList,那么每个class的实体对象也会有十万个,这个是很容易被定位的。

从直方图中跟踪这种引用关系链是需要花费大量精力的,幸运的是,jmap不仅仅可以提供直方图,它还可以提供可以浏览的堆转储信息。

② 解决Redeploy时的OutOfMemoryError

分析dump文件:首先,找出引用在哪里被持有;其次,给你的web应用程序添加一个关闭的hook,或者在应用程序卸载后移除引用。你可以使用如下命令导出dump文件:

jmap -dump:format=b,file=dump.hprof <process-id>

如果是你自己代码的问题请及时修改,如果是第三方库,请试着搜索一下是否存在”关闭”接口,如果没有给开发者提交一个bug或者issue吧。

堆转储分析:跟踪引用链

浏览堆转储引用链具有两个步骤:首先需要使用-dump参数来使用jmap,然后需要用jhat来使用转储文件。如果你确定要使用这种方法,请一定要保证有足够多的内存:一个转储文件通常都有数百M,jhat需要好几个G的内存来处理这些转储文件。

tmp, 517> jmap -dump:live,file=heapdump.06180803 7626
Dumping heap to /home/kgregory/tmp/heapdump.06180803 ...
Heap dump file created

tmp, 518> jhat -J-Xmx8192m heapdump.06180803
Reading from heapdump.06180803...
Dump file created Sat Jun 18 08:04:22 EDT 2011
Snapshot read, resolving...
Resolving 335643 objects...
Chasing references, expect 67 dots...................................................................
Eliminating duplicate references...................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

提供给你的默认URL显示了所有加载进系统的class,但是我觉得并不是很有用。相反,我直接使用http://localhost:7000/histo/,这个地址是一个直方图的视角来进行显示,并且是按照对象数量和占用的内存空间进行排序了的。

澳门新葡亰手机版 3

这个直方图里的每个class的名称都是一个链接,点击这个链接可以查看关于这个类型的详细信息。你可以在其中看到这个类的继承关系,它的成员变量,以及很多指向这个类的实体变量信息的链接。我不认为这个详细信息页面非常有用,而且实体变量的链接列表很占用很多的浏览器内存。

为了能够跟踪你的内存问题,最有用的页面是’Reference by
Type’。这个页面含有两个表格:入引用和出引用,他们都被引用的数量进行排序了。点击一个类的名字可以看到这个引用的信息。

你可以在类的详细信息(class details)页面中找到这个页面的链接。

③ 解决运行时OutOfMemoryError

首先你需要检查是否允许GC从PermGen卸载类,JVM的标准配置相当保守,只要类一创建,即使已经没有实例引用它们,其仍将保留在内存中,特别是当应用程序需要动态创建大量的类但其生命周期并不长时,允许JVM卸载类对应用大有助益,你可以通过在启动脚本中添加以下配置参数来实现:

-XX:+CMSClassUnloadingEnabled

默认情况下,这个配置是未启用的,如果你启用它,GC将扫描PermGen区并清理已经不再使用的类。但请注意,这个配置只在UseConcMarkSweepGC的情况下生效,如果你使用其他GC算法,比如:ParallelGC或者Serial GC时,这个配置无效。所以使用以上配置时,请配合:

-XX:+UseConcMarkSweepGC

如果你已经确保JVM可以卸载类,但是仍然出现内存溢出问题,那么你应该继续分析dump文件,使用以下命令生成dump文件:

jmap -dump:file=dump.hprof,format=b <process-id>

当你拿到生成的堆转储文件,并利用像Eclipse Memory Analyzer
Toolkit这样的工具来寻找应该卸载却没被卸载的类加载器,然后对该类加载器加载的类进行排查,找到可疑对象,分析使用或者生成这些类的代码,查找产生问题的根源并解决它。

堆转储分析:内存分配情况

在大多数情况下,知道了是哪些对象消耗了大量的内存往往就可以知道它们为什么会发生内存泄露。你可以使用jhat来找到所有引用了他们的对象,并且你还可以看到使用了这些对象的引用的代码。但是在有些时候,这样还是不够的。

比如说你有关于字符串对象的内存泄露问题,那么就很有可能会花费你好几天的时间去检查所有和字符串相关的代码。要解决这种问题,你就需要能够显示内存在哪里被分配的堆转储。但是需要注意的是,这种类型的堆转储会对你的应用程序产生更多的负载,因为负责转储的代理需要记录每一个new操作符。

有许多交互式的程序可以做到这种级别的数据记录,但是我找到了一个更简单的方法,那就是使用内置的hprof代理来启动JVM。

java -Xrunhprof:heap=sites,depth=2 com.kdgregory.example.memory.Gobbler

hprof有许多选项:不仅仅可以用多种方式输出内存使用情况,它还可以跟踪CPU的使用情况。当它运行的时候,我指定了一个事后的内存转储,它记录了哪些对象被分配,以及分配的位置。它的输出被记录在了java.hprof.txt文件中,其中关于堆转储的部分如下:

SITES BEGIN (ordered by live bytes) Tue Sep 29 10:43:34 2009
          percent          live          alloc'ed  stack class
 rank   self  accum     bytes objs     bytes  objs trace name
    1 99.77% 99.77%  66497808 2059  66497808  2059 300157 byte[]
    2  0.01% 99.78%      9192    1     27512    13 300158 java.lang.Object[]
    3  0.01% 99.80%      8520    1      8520     1 300085 byte[]
SITES END

这个应用程序没有分配多种不同类型的对象,也没有将它们分配到很多不同的地方。一般的转储有成百上千行的信息,显示了每一种类型的对象被分配到了哪里。幸运的是,大多数问题都会出现在开头的几行。在这个例子中,最突出的是64M的存活着的字节数组,并且每一个平均32K。

大多数程序中都不会一直持有这么大得数据,这就表明这个程序没有很好的抽取和处理这些数据。你会发现这常常发生在读取一些大的字符串,并且保存了substring之后的字符串:很少有人知道String.substring()后会共享原始字符串对象的字节数组。如果你按照一行一行地读取了一个文件,但是却使用了每行的前五个字符,实际上你任然保存的是整个文件在内存中。

转储文件也显示出这些数组被分配的数量和现在存活的数量完全相等。这是一种典型的泄露,并且我们可以通过搜索’trace’号来找到真正的代码:

TRACE 300157:
    com.kdgregory.example.memory.Gobbler.main(Gobbler.java:22)

好了,这下就足够简单了:当我在代码中找到指定的代码行时,我发现这些数组被存放在了ArrayList中,并且它也一直没有出作用域。但是有时候,堆栈的跟踪并没有直接关联到你写的代码上:

TRACE 300085:
    java.util.zip.InflaterInputStream.<init>(InflaterInputStream.java:71)
    java.util.zip.ZipFile$2.<init>(ZipFile.java:348)

在这个例子中,你需要增加堆栈跟踪的深度,并且重新运行你的程序。但是这里有一个需要平衡的地方:当你获取到了更多的堆栈信息,你也同时增加了profile的负载。默认地,如果你没有指定depth参数,那么默认值就会是4。我发现当堆栈深度为2的时候就可以发现和定位我程序中得大部分问题了,当然我也使用过深度为12的参数来运行程序。

另外一个增大堆栈深度的好处是,最后的报告结果会更加细粒度:你可能会发现你泄露的对象来自两到三个地方,并且它们都使用了相同的方法。

4、java.lang.OutOfMemoryError:Metaspace

前文已经提过,PermGen区域用于存储类的名称和字段,类的方法,方法的字节码,常量池,JIT优化等,但从Java8开始,Java中的内存模型发生了重大变化:引入了称为Metaspace的新内存区域,而删除了PermGen区域。请注意:不是简单的将PermGen区所存储的内容直接移到Metaspace区,PermGen区中的某些部分,已经移动到了普通堆里面。

澳门新葡亰手机版 4OOM-example-metaspace,图片来源:Plumbr

Java8做出如此改变的原因包括但不限于:

  • 应用程序所需要的PermGen区大小很难预测,设置太小会触发PermGen OutOfMemoryError错误,过度设置导致资源浪费。
  • 提升GC性能,在HotSpot中的每个垃圾收集器需要专门的代码来处理存储在PermGen中的类的元数据信息。从PermGen分离类的元数据信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空间,因此MetaspaceJava Heap可以无缝的管理,而且简化了FullGC的过程,以至将来可以并行的对元数据信息进行垃圾收集,而没有GC暂停。
  • 支持进一步优化,比如:G1并发类的卸载,也算为将来做准备吧

正如你所看到的,元空间大小的要求取决于加载的类的数量以及这种类声明的大小。
所以很容易看到java.lang.OutOfMemoryError: Metaspace主要原因:太多的类或太大的类加载到元空间。

正如上文中所解释的,元空间的使用与加载到JVM中的类的数量密切相关。
下面的代码是最简单的例子:

public class Metaspace { static javassist.ClassPool cp = javassist.ClassPool.getDefault(); public static void main(String[] args) throws Exception{ for (int i = 0; ; i++) { Class c = cp.makeClass("eu.plumbr.demo.Generated" + i).toClass(); System.out.println; } }}

程序运行中不停的生成新类,所有的这些类的定义将被加载到Metaspace区,直到空间被完全占用并且抛出java.lang.OutOfMemoryError:Metaspace。当使用-XX:MaxMetaspaceSize = 32m启动时,大约加载30000多个类时就会死机。

3102331024Exception in thread "main" javassist.CannotCompileException: by java.lang.OutOfMemoryError: Metaspace at javassist.ClassPool.toClass(ClassPool.java:1170) at javassist.ClassPool.toClass(ClassPool.java:1113) at javassist.ClassPool.toClass(ClassPool.java:1071) at javassist.CtClass.toClass(CtClass.java:1275) at cn.moondev.book.Metaspace.main(Metaspace.java:12) .....

第一个解决方案是显而易见的,既然应用程序会耗尽内存中的Metaspace区空间,那么应该增加其大小,更改启动配置增加如下参数:

// 告诉JVM:Metaspace允许增长到512,然后才能抛出异常-XX:MaxMetaspaceSize = 512m

另一个方法就是删除此参数来完全解除对Metaspace大小的限制。默认情况下,对于64位服务器端JVM,MetaspaceSize默认大小是21M,一旦达到这个限制值,FullGC将被触发进行类卸载,并且这个限制值将会被重置,新的限制值依赖于Metaspace的剩余容量。如果没有足够空间被释放,这个限制值将会上升,反之亦然。在技术上Metaspace的尺寸可以增长到交换空间,而这个时候本地内存分配将会失败(更具体的分析,可以参考:Java
PermGen 去哪里了?)。

你可以通过修改各种启动参数来“快速修复”这些内存溢出错误,但你需要正确区分你是否只是推迟或者隐藏了java.lang.OutOfMemoryError的症状。如果你的应用程序确实存在内存泄漏或者本来就加载了一些不合理的类,那么所有这些配置都只是推迟问题出现的时间而已,实际也不会改善任何东西。

堆转储分析:位置、地点

当很多对象在分配的不久后就被丢弃时,分代垃圾搜集器就会开始运行。你可以使用同样的原则来找发现内存泄露:使用调试器,在对象被分配的地方打上断点,并且运行这段代码。在大多数时候,当它们被分配不久后就会加入到长时间存活(long-live)的集合中。

5、java.lang.OutOfMemoryError:Unable to create new native thread

一个思考线程的方法是将线程看着是执行任务的工人,如果你只有一个工人,那么他同时只能执行一项任务,但如果你有十几个工人,就可以同时完成你几个任务。就像这些工人都在物理世界,JVM中的线程完成自己的工作也是需要一些空间的,当有足够多的线程却没有那么多的空间时就会像这样:

澳门新葡亰手机版 5图片来源:Plumbr

出现java.lang.OutOfMemoryError:Unable to create new native thread就意味着Java应用程序已达到其可以启动线程数量的极限了。

当JVM向OS请求创建一个新线程时,而OS却无法创建新的native线程时就会抛出Unable to create new native thread错误。一台服务器可以创建的线程数依赖于物理配置和平台,建议运行下文中的示例代码来测试找出这些限制。总体上来说,抛出此错误会经过以下几个阶段:

  • 运行在JVM内的应用程序请求创建一个新的线程
  • JVM向OS请求创建一个新的native线程
  • OS尝试创建一个新的native线程,这时需要分配内存给新的线程
  • OS拒绝分配内存给线程,因为32位Java进程已经耗尽内存地址空间(2-4GB内存地址已被命中)或者OS的虚拟内存已经完全耗尽
  • Unable to create new native thread错误将被抛出

下面的示例不能的创建并启动新的线程。当代码运行时,很快达到OS的线程数限制,并抛出Unable to create new native thread错误。

while{ new Thread(new Runnable(){ public void run() { try { Thread.sleep; } catch(InterruptedException e) { } } }).start();}

有时,你可以通过在OS级别增加线程数限制来绕过这个错误。如果你限制了JVM可在用户空间创建的线程数,那么你可以检查并增加这个限制:

// macOS 10.12上执行$ ulimit -u709

当你的应用程序产生成千上万的线程,并抛出此异常,表示你的程序已经出现了很严重的编程错误,我不觉得应该通过修改参数来解决这个问题,不管是OS级别的参数还是JVM启动参数。更可取的办法是分析你的应用是否真的需要创建如此多的线程来完成任务?是否可以使用线程池或者说线程池的数量是否合适?是否可以更合理的拆分业务来实现…..

永久代

除了JVM中的新生代和老年代外,JVM还管理着一片叫‘永久代’的区域,它存储了class信息和字符串表达式等对象。通常,你不会观察到永久代中的垃圾回收;大多数的垃圾回收发生在应用程序堆中。但是不像它的名字,在永久代中的对象不会是永久不变的。举个例子,被应用程序classloader加载的class,当不再被classloader引用时就会被清理掉。当应用程序服务被频繁的热部署时就可能会发生:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

这一这个信息:这个不管应用程序堆的事。当应用程序堆中还有很多空间时,也有可能用完永久代的空间。通常,这发生在重新部署EAR和WAR文件时,并且永久代还不够大到可以同时容纳新的class信息和老的class信息(老的class会一直被保存着直到所有的请求在使用完它们)。当在运行处于开发状态的应用时更容易发生。

解决永久代错误的第一个方法就是增大永久大的空间,你可以使用-XX:MaxPermSize命令行参数。默认是64M,但是web应用程序或者IDE一般都需要256M。

java -XX:MaxPermSize=256m

但是在通常情况下并不是这么简单的。永久代的内存泄露一般都和在应用堆中的内存泄露原因一样:在一些地方的对象引用了并不该再引用的对象。以我的经验,很有可能有些对象直接引用了一些Class对象,或者在java.lang.reflect包下面的对象,而不是某些类的实例对象。正式因为web引用的classloader的组织方式,通常罪魁祸首都出现在服务的配置当中。

例如,你使用了Tomcat,并且有一个目录里面有很多共享的jars:shared/lib。如果你在一个容器里同时运行好几个web应用,将一些公用的jar放在这个目录是很有道理的,因为这样的话这些class仅仅被加载一次,可以减少内存的使用量。但是,如果其中的一些库具有对象缓存的话,会发生什么事情呢?

答案是这些被缓存了的对象的类永远不会被卸载,直到缓存释放了这些对象。解决方案就是将这些库移动到WAR或者EAR中。但是在某些时候情况也不会像这么简单:JDKs
bean
introspector会缓存住由root
classloader加载的BeanInfo对象。并且任何使用了反射的库也会缓存这些对象,这样就导致你不能直到真正的问题所在。

解决永久代的问题通常都是比较痛苦的。一般可以先考虑加上-XX:+TraceClassLoading-XX:+TraceClassUnloading命令行选项以便找出那些被加载了但是没有被卸载的类。如果你加上了-XX:+TraceClassResolution命令行选项,你还可以看到哪些类访问了其他类,但是没有被正常卸载。

这里有针对这三个选项的一个实例。第一行显示了MyClassLoader类从classpath中被加载了。因为它又从URLClassLoader继承,因此我们看到了接下来的’RESOLVE’消息,紧跟着又是一条’RESOLVE’消息,说明Class类也被解析了。

[Loaded com.kdgregory.example.memory.PermgenExhaustion$MyClassLoader from file:/home/kgregory/Workspace/Website/programming/examples/bin/]
RESOLVE com.kdgregory.example.memory.PermgenExhaustion$MyClassLoader java.net.URLClassLoader
RESOLVE java.net.URLClassLoader java.lang.Class URLClassLoader.java:188

所有的信息都在这里的,但是通常情况下将一些共享库移动到WAR/EAR中往往可以很快速的解决问题。

6、java.lang.OutOfMemoryError:Out of swap space?

Java应用程序在启动时会指定所需要的内存大小,可以通过-Xmx和其他类似的启动参数来指定。在JVM请求的总内存大于可用物理内存的情况下,操作系统会将内存中的数据交换到磁盘上去。

澳门新葡亰手机版 6图片来源:plumbr

Out of swap space?表示交换空间也将耗尽,并且由于缺少物理内存和交换空间,再次尝试分配内存也将失败。

当应用程序向JVM native heap请求分配内存失败并且native
heap也即将耗尽时,JVM会抛出Out of swap space错误。该错误消息中包含分配失败的大小和请求失败的原因。

Native Heap
Memory是JVM内部使用的Memory,这部分的Memory可以通过JDK提供的JNI的方式去访问,这部分Memory效率很高,但是管理需要自己去做,如果没有把握最好不要使用,以防出现内存泄露问题。JVM
使用Native Heap
Memory用来优化代码载入,临时对象空间申请,以及JVM内部的一些操作。

这个问题往往发生在Java进程已经开始交换的情况下,现代的GC算法已经做得足够好了,当时当面临由于交换引起的延迟问题时,GC暂停的时间往往会让大多数应用程序不能容忍。

java.lang.OutOfMemoryError:Out of swap space?往往是由操作系统级别的问题引起的,例如:

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

还有可能是本地内存泄漏导致应用程序失败,比如:应用程序调用了native
code连续分配内存,但却没有被释放。

解决这个问题有几个办法,通常最简单的方法就是增加交换空间,不同平台实现的方式会有所不同,比如在Linux下可以通过如下命令实现:

# 原作者使用,由于我手里并没有Linux环境,所以并未测试# 创建并附加一个大小为640MB的新交换文件swapoff -a dd if=/dev/zero of=swapfile bs=1024 count=655360mkswap swapfileswapon swapfile

Java
GC会扫描内存中的数据,如果是对交换空间运行垃圾回收算法会使GC暂停的时间增加几个数量级,因此你应该慎重考虑使用上文增加交换空间的方法。

如果你的应用程序部署在JVM需要同其他进程激烈竞争获取资源的物理机上,建议将服务隔离到单独的虚拟机中

但在许多情况下,您唯一真正可行的替代方案是:

  • 升级机器以包含更多内存
  • 优化应用程序以减少其内存占用

当您转向优化路径时,使用内存转储分析程序来检测内存中的大分配是一个好的开始。