通用技术 Java OOM ERROR

rocl · December 15, 2017 · Last by 槽神 replied at February 26, 2018 · 3537 hits

Java内存溢出(OOM异常完全指南),原文见:http://www.jianshu.com/p/2fdee831ed03
作者CHEN川是从这里翻译来的:https://plumbr.io/outofmemoryerror
笔者后来是独立翻译实验

java.lang.OutOfMemoryError:Java heap space

Java应用程序被允许使用有限的内存。这个限制在程序开始运行的时候就被说明了。为了便于处理,Java内存被分成两个区域,分别称为:堆内存/堆空间(Heap space)和永久代(Permgen, Permanent Generation)。

这两个区域的大小可以在JVM(Java虚拟机)启动时通过参数-Xmx和-XX:MaxPermSize设置。如果你不显示指定大小,将使用特定平台的默认值。

当应用程序试图添加更多的数据到堆空间区域,却没有足够空间,此时java.lang.OutOfMemoryError: Java heap space error将被触发。注:系统可能有许多未使用的物理内存,但是当JVM到达堆空间大小限制的时候,java.lang.OutOfMemoryError: Java heap space error异常仍然会被抛出。
注意(编者加):上面的JVM内存模型是JDK7的模型,在JDK8中已经移除了永久代,取而代之的是MetaSpace(主要存放类的元数据)。

原因分析(What is causing it):

触发java.lang.OutOfMemoryError: Java heap space error异常的常见原因:应用程序需要XXL号的堆内存,但是却提供了一个s号的堆内存。也就是说:应用程序需要比它所能得到的更大的堆内存。其它引发OutOfMemory的原因更加复杂,也有可能是程序原因。

  • 使用/数据量峰值:应用程序在设计之初要考虑处理大量的用户和大量的数据。当大量的用户或者数据突然到达峰值,并且超过了预期的阈值,以前在峰值到达之前功能正常的操作将会停止,并且触发java.lang.OutOfMemoryError异常。
  • 内存泄漏:一种特殊的编程错误将会导致应用程序持续消耗更多的内存。每一次有内存泄漏功能的应用程序使用,都将留下一些对象在java堆空间中,随着时间的推移,泄露的对象消耗越来越多的java堆空间,就触发了我们熟悉的java.lang.OutOfMemoryError异常。

示例

简单示例

第一个应用程序非常简单--下面的java代码试图申请一个2M的数组。当编译并以一个12M(java -Xmx12m OOM)的堆内存运行的时候,将会失败,并且提示:Java.lang.OutOfMemoryError: Java heap space message。如果给与13M的堆内存,程序将运行很好。

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

运行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at OOM_heapspace.main(OOM_heapspace.java:7)

内存泄漏示例

在Java中,当开发者创建和使用新的对象,例如new Integer(5),他们不用自己分配内存--这个工作是Java虚拟机(JVM)来做的。在整个应用程序生命周期中,JVM会定期检查,内存中的哪些对象仍然在使用,哪些对象没有继续使用。不再使用的内存对象会被回收,并被重新分配和再使用。这个过程称之为垃圾回收(Garbage Collection)。JVM执行这个功能的模块被称为Garbage Collector(GC)。

Java的自动内存管理依赖于GC定期的寻找不再使用的对象并且移除他们。 简单来说,我们可以说:在Java中的内存泄漏是这样一种情况,一些对象不再被应用程序使用,但是GC却没办法识别他们。因此这些不再被使用的对象仍然无限期的保留在Java的堆空间中。这样的堆积最终将触发java.lang.OutOfMemoryError: Java heap space异常。
很容易新建一个java程序来满足内存泄露的定义。

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 m = new HashMap();
while (true)
for (int i = 0; i < 10000; i++)
if (!m.containsKey(new Key(i)))
m.put(new Key(i), "Number:" + i);
}
}

当执行上面的代码,我们可能会认为程序会一直跑下去,没有任何问题,认为存储在缓存中的Map将扩展到10000个元素,再往下,所有的Key都已经在HashKey中存在了。然而,实际情况是Key class的元素并不包含equals这个方法的实现。

因此,随着时间的继续,泄露内存代码的持续运行,会导致消耗大量java堆空间。当持续占满所有可用的堆空间,并且GC不能清除的时候,就会触发java.lang.OutOfMemoryError: Java heap space异常。

解决方案非常简单--添加equals()方法的实现,这样代码就能很好的运行。

@Override
public boolean equals(Object o) {
boolean response = false;
if (o instanceof Key) {
response = (((Key)o).id).equals(this.id);
}
return response;
}

解决方案

某些情况,我们分配给JVM堆空间的内存数量不能满足程序运行的需求。此时,我们应该分配更多的堆空间,看本文的结尾如何做。

然而,在更多情况下,提供更多堆空间并不能解决问题。例如应用程序有内存泄漏,添加更多的堆内存只会推迟java.lang.OutOfMemoryError: Java heap space异常。另外,增加堆空间数量也会增加GC暂停的次数进而影响应用程序的吞吐量或者延迟(latency).

如果希望解决Java堆空间的潜在问题,而不是掩盖问题,那么我们需要搞清楚代码的哪一部分负责申请内存。换句话说,必须回答以下问题:

  • 哪些对象占据了堆空间的大部分
  • 这些对象在源代码的位置

在这一点上,一定要确信搞清楚。下面是一个大致的大纲,这个大纲将帮助我们回答上面的问题:

  • 获得安全许可,以便从JVM执行heap dump。"Dumps"基本上来说是对堆内容的快照,这些内容是我们可以分析的。这些快照包含了关键信息,例如密码,信用卡号码等等,由于安全原因,我们甚至不太可能获取这些快照。
  • 在合适的时刻得到dump文件。错误的时间得到一些dumps文件,堆dump文件包含大量的无用内容。另一方面,每一个堆dump包含了jvm的所有内容,因此不要做太多次的dump操作,否则客户也需要面对性能问题。
  • 找到一台能够读取dump文件的机器。在开始执行JVM问题调查的时候,例如一个8GB的堆,我们需要一台超过8GB去分析堆内容。至于用来分析dump文件的软件(我们推荐Eclipse MAT,当然也有其它许多优秀的软件,例如JProfiler/YourKit)。注:到现在为止Jprofiler10为最新版,找不到合适的license。因此使用Jprofiler9.2,是好用的。
  • 检查堆空间最大消费者的GC根目录的路径。我们已经做过这件事情,有一个单独的文章,请参见这里。对于初学者来说有些困难,但是实践将会使我们理解结构和机制。
  • 接下来,我们需要搞清楚代码中,哪些代码申请了大量内存。如果对自己的应用程序的源代码有很好了解的话,那么几次搜索就能做完这件事情。

解决方案就是增大堆空间
-Xmx1024m

再例如所有下面的配置具有同样功能,因为我们可以使用g/G/m/M/k/K。例如所有如下配置都是相同的,最大堆空间是1GB:

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

java.lang.OutOfMemoryError:GC overhead limit exceeded

JRE(Java Runtime Environment)包含了一个自带/内嵌的GC(Garbage Collection)程序。在许多其它编程语言中,开发者需要自己申请和释放内存。

另一方面,Java程序只需要申请内存即可。当内存中一个空间不再使用的时候,一个独立的称作GC的进程将清除这些不再使用的内存。GC是如何检测内存中特殊区域,详细情况请见:Garbage Collection Handbook,但是我们应该信任GC能做好它的工作(内存回收)。

java.lang.OutOfMemoryError: GC overhead limit exceeded异常将被触发,当应用程序消耗了所有可用的内存,GC还在不停的清除内存,并且清除内存一直失败。

原因分析:

java.lang.OutOfMemoryError: GC overhead limit exceeded异常,是JVM发出的一个信号,表明:应用程序花费了太多时间在做内存回收的工作,回收结果却不好。默认情况下,JVM将会报错,如果花费超过98%时间在执行GC操作,却仅仅回收了不到2%的内存。

如果GC overhead limit不存在,将会发生什么事情?java.lang.OutOfMemoryError: GC overhead limit exceeded异常只有在这种情况下才会被触发,经过几次GC循环操作之后,只释放了2%的内存。这意味着只有少量的堆空间能被清除,这些空间将会被很快再次用掉,强迫GC再次重新开始清除进程。这就形成了一个恶性循环,CPU100%被用于GC操作,没办法做其他事情了。应用的终端用户感觉非常慢-通常毫秒级别完成的操作,现在却需要数分钟才能完成。

于是"java.lang.OutOfMemoryError: GC overhead limit exceeded"提示可以看做"fail fast"规则一个非常棒的例子。

实例

下面的例子,我们将创建一个GC overhead limit exceeded异常,通过初始化一个MAP,通过无限循环添加key-value对到map中。

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

你有可能猜到,这段代码可能不能很好的结束。确实,当用下列配置运行这段代码的时候,

java -Xmx100m -XX:+UseParallelGC Wrapper
java -Xmx10m -XX:+UseParallelGC Wrapper
java -Xmx20m -XX:+UseParallelGC Wrapper

错误信息如下(使用JDK7):
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Hashtable.rehash(Hashtable.java:402)
at java.util.Hashtable.addEntry(Hashtable.java:426)
at java.util.Hashtable.put(Hashtable.java:477)
at GC_ole.main(GC_ole.java:11)
当使用如下参数java -Xmx2m -XX:+UseParallelGC Wrapper,错误信息如下:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.Hashtable.put(Hashtable.java:541)
at GC_ole.main(GC_ole.java:11)

不久,我们就将看到java.lang.OutOfMemoryError: GC overhead limit exceeded异常。但是如果我们配置不同的堆空间大小或者不同的GC算法,结果会有不同。例如,用如下方式在ubuntu16.04,用Hotspot1.7.0_80运行:
java -Xmx10m -XX:+UseParallelGC Wrapper
我们将看到如下错误:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Hashtable.rehash(Hashtable.java:471)
at java.util.Hashtable.put(Hashtable.java:532)
at GC_ole.main(GC_ole.java:11)

使用以下GC算法:-XX:+UseConcMarkSweepGC 或者-XX:+UseG1GC,启动命令如下:
java -Xmx100m -XX:+UseConcMarkSweepGC Wrapper
java -Xmx100m -XX:+UseG1GC Wrapper
得到的结果是这样的:
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
错误已经被默认的异常处理程序捕获,并且没有任何错误的堆栈信息输出。
以上这些变化可以说明,在资源有限的情况下,你根本无法无法预测你的应用是怎样挂掉的,什么时候会挂掉,所以在开发时,你不能仅仅保证自己的应用程序在特定的环境下正常运行。

解决方案

如果我们仅仅想去掉java.lang.OutOfMemoryError: GC overhead limit exceeded这个消息,添加下列到启动脚本里面就可以做到:

-XX:-UseGCOverheadLimit

烈建议不要这样做--应该修复解决这个问题,而不是将这个不可避免的问题推迟或者延后;因为应用程序将会把内存用尽。错误信息也变成了更加熟悉的java.lang.OutOfMemoryError: Java heap space而已。

某些情况下,GC overhead limit exceeded异常被触发,因为申请的内存不能满足程序运行的需要。此时,应该申请更多的内存--看本文末尾如何做到这一点。例如应用程序存在内存泄漏,将推迟java.lang.OutOfMemoryError: Java heap space异常。另外增加内存将增加GC暂停的时间长度,影响到应用程序的吞吐量和延迟等。

如果希望解决Java堆空间的潜在问题,而不是掩盖问题,那么我们需要搞清楚代码的那一部分负责申请内存。换句话说,必须回答以下问题:

  • 哪些对象占据了堆空间的大部分
  • 这些对象在源代码的位置

在这一点上,一定要确信搞清楚。下面是一个大致的大纲,这个大纲将帮助我们回答上面的问题:

  • 通过安全检查,以从JVM执行heap dump。"Dumps"基本上来说是对内容的快照。这些快照包含了关键信息,例如密码,信用卡号码等等,由于安全原因,我们甚至不太可能获取这些快照。
  • 在合适的时刻得到dump文件。错误的时间得到一些dumps文件,堆dump文件包含大量的无用内容。另一方面,每一个堆dump包含了jvm的所有内容,因此不要做太多次的dump操作,否则客户也需要面对性能问题。
  • 找到一台能够读取dump文件的机器。在开始执行JVM问题调查的时候,例如一个8GB的堆,我们需要一台超过8GB去分析对内容。至于用来分析dump文件的软件(我们推荐Eclipse MAT,当然也有其它许多优秀的软件,例如JProfiler/YourKit)。注:到现在为止Jprofiler10为最新版,找不到合适的license。因此使用Jprofiler9.2,是好用的。具体请参见百度云盘jprofiler目录。
  • 检查堆空间最大消费者的GC根目录的路径。我们这个分析,有一个单独的文章,请参见这里。对于初学者来说有些困难,但是实践将会是我们理解结构和机制。
  • 接下来,我们需要搞清楚代码中,哪些代码申请了大量内存。如果对自己的应用程序的源代码有很好的了解的话,那么几次搜索就能做完这件事情。

java.lang.OutOfMemoryError:Permgen space

Java应用程序只被允许使用有限的内存。确切使用的内存数量只能在程序开始运行时才能知道。Java内存被分成不同的区域,如下图所示:

注:此为JDK7的jvm内存模型。
上图所有的区域包括永久代都是在JVM开始运行的时候被设定。如果不设置,将会使用特定平台的默认值。
java.lang.OutOfMemoryError: PermGen space消息表明:内存中的永久代区域已经被用完了。

原因分析

要想理解java.lang.OutOfMemoryError: PermGen space的原因,我们需要知道这块内存区域的用途:
为了实践目的,永久代包含大多数的类定义,也就是类/方法的名字和字段,全局不可变变量池,对象数组,类相关的对象数组,实时编译优化等。
从上面的定义,我们可以看出,永久代的大小取决于类申明的数量和装载的类的数量。因为,我们可以说:java.lang.OutOfMemoryError: PermGen space的主要原因:要么太多的类,要么太大的类被分配到了永久带空间。

实例

简单实例

综上所述,永久带空间的使用是和加载到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("com.myown.demo.Generated" + i);
}
}
public static Class generate(String name) throws Exception {
ClassPool pool = ClassPool.getDefault();
return pool.makeClass(name).toClass();
}
}

在这个示例中,通过循环逐个生成运行时类。类的生成是javassist库负责。

运行上面的代码将持续生成新类并把他们的定义装载到永久代空间,直到空间被全部使用,java.lang.OutOfMemoryError: PermGen space异常就会被触发。
实际测试发现(Ubuntu16.04 JDK7,设置-XX:MaxPermSize=512m,否则内存空间占据太多),报错如下:
Exception in thread "main"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
为什么不是java.lang.OutOfMemoryError: PermGen space异常,从监控看,PermGen空间确实满了,heap有剩余。

重新部署示例

再说一个更加复杂和更加实际的例子,在程序重新部署发布的时候,我们经历一次java.lang.OutOfMemoryError: PermGen space异常的发生。当我们重新发布应用程序的时候,我们希望GC能清除以前的旧的已经装载的类,并重新装载新版本的类。

不幸的是,许多第三方类和资源的处理,例如线程,JDBC驱动或者文件系统处理,不能卸载旧类。这也意味着:每一次重新发布,所有先前版本的类仍然存在于永久代空间,并且每次重新发布都会生成几十M的垃圾数据。

想象一个例子,应用使用JDBC连接到一个关系型数据库。当应用开始的时候,代码初始化JDBC驱动去连接数据库。根据说明,JDBC驱动会注册他自己为java.sql.DriverManager。注册会存储一个引用,这个引用指向到一个DrvierManager实例的静态字段。

现在,当从服务器上卸载应用程序的时候,java.sql.DriverManager仍将持有那个驱动程序的引用,进而持有用于加载应用程序的classloader的一个实例的引用,通常会占有数十兆的永久代空间。这个classloader现在仍然引用着应用程序的所有类。这意味着经历过几次重新部署,就会触发java.lang.OutOfMemoryError: PermGen space错误。

解决方案

  • .解决初始化时的OutOfMemoryError

当OutOfMemoryError因永久代耗尽被触发的时候(应用程序启动的时候),解决方案非常简单。应用程序需要更多的空间去家在所有的类到永久代区域。我们增大永久代的大小,提示应用程序增大永久带空间。-XX:MaxPermSize相似的参数:

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

上面的配置告诉JVM,永久代最大空间可以到512M。

  • 解决重新发布的OutOfMemoryError

当重新发布应用的时候,OutOfMemoryError恰好发生了,说明应用程序遭遇了classloader泄露。此时我们应该执行堆dump分析--是用类似如下命令去执行堆dump的工作:

jmap -dump:format=b,file=dump.hprof

然后用熟悉的工具去分析dump文件。如果是第三方库的原因,可以去Google/StackOverflow检查下是否是一个已知问题,如是已知问题,可以下载一个补丁或者解决方案。如果是自己代码的问题,需要及时修改。

  • 解决运行时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

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

java.lang.OutOfMemoryError:Metaspace

Java应用程序只被允许使用有限的内存。确切使用的内存数量只能在程序开始运行时才能知道。Java内存被分成不同的区域,如下图所示:

上面所有的区域,包括元数据区域,都是在JVM启动的时候被设定。如果没有指定,那么特定平台的默认数据将被使用。
java.lang.OutOfMemoryError: Metaspace预示着内存中的元数据空间消耗殆尽。

原因分析

如果不是Java新手,那么你可能熟悉另外一个称作PermGen的Java内存管理的概念。从Java8开始,内存模型有显著的改变。一个新的被称为Metaspace的内存区域被引进,PermGen被移除了。这个改变有多种原因,包括但不限于以下:

  • 永久代(PermGen)的大小很难预测。这直接导致了要么触发java.lang.OutOfMemoryError: Permgen size异常,要么浪费资源。
  • GC效率的提升改进,使得并发类数据重新申请内存不再进行GC暂停(GC pause)和指定元数据遍历。
  • 支持进一步的优化,例如G1并发类卸载。 如果熟悉PermGen,那么也知道它的作用--在java8以前,所有类的名字、字段、方法,字节方法,变量池,JIT优化等都是存储在PermGen中,现在都在Metaspace中。

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

实例

正如前面解释的,Metaspace是和加载的类的数量密切相关。下面的代码是一个简单明了的说明:

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("com.myown.demo.Generated" + i).toClass();
}
}
}

在这个例子中,源代码在运行时遍历循环生成类。所有生成的类定义都位于Metaspace中。类生成的工作交由javassist负责。

代码持续生成新类,并把它们的定义装载到Metaspace中,直到元数据空间被全部使用,java.lang.OutOfMemoryError: Metaspace异常被触发。当用-XX:MaxMetaspaceSize=64m在Ubuntu16.04.3, java 1.8.0_151,一共加载了66126个类,程序才挂掉。试验得出的完整错误信息如下:

Exception in thread "main" javassist.CannotCompileException: by java.lang.OutOfMemoryError: Metaspace
at javassist.ClassPool.toClass(ClassPool.java:1085)
at javassist.ClassPool.toClass(ClassPool.java:1028)
at javassist.ClassPool.toClass(ClassPool.java:986)
at javassist.CtClass.toClass(CtClass.java:1079)
at Metaspace.main(Metaspace.java:8)
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at javassist.ClassPool.toClass2(ClassPool.java:1098)
at javassist.ClassPool.toClass(ClassPool.java:1079)
... 4 more

解决方案

第一个解决方案,毫无疑问:OutOfMemoryError是因为Metaspace。如果应用程序将metaspace消耗殆尽,我们可以增大Metaspace的空间。更改程序配置,按照如下进行:

-XX:maxMetaspaceSize=512m

上面的配置告诉JVM:Metaspace空间最大允许到512MB。

另外一个解决方案初一看更加简单。我们可以移除Metaspace的大小限制(删除这个参数即可)。但是这样做,将会导致沉重的交换负担,会导致内存分配失败。

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

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

Java应用程序天然支持多线程的。这意味着java实现的程序能一次做几件事情(几乎同时)。甚至只有一个cpu--在从一个窗口拉数据到另外一个窗口的时候,视频同时也在后台不停的播放,因为可以同时执行多个操作。

一个思考多线程的方式就是把他们认为是我们能提交任务给他们执行的工人。如果只有一个工人,那么他/她一次只能做一件事情。但是如果有很多工人的话,他们就能根据你的命令同时做事。

就像这些工人都在物理世界,JVM中的线程完成自己的工作也是需要一些空间的,当有足够多的线程却没有那么多的空间时就会像这样:

java.lang.OutOfMemoryError: Unable to create new native thread意味着: Java应用已经到了它所能启动的线程的极限。

原因分析

当JVM请求操作系统去创建一个新的线程,操作系统却不能再申请一个新的线程,此时OutOfMemoryError(java.lang.OutOfMemoryError: Unbale to crate new thread))就被触发。线程数目的确切数字是依赖于平台的。如果想找到线程数目的限制,可以运行下面将要提到的例子。

一般来说, 引发java.lang.OutOfMemoryError: Unable to create new native thread异常有以下几种情况:

  • 运行在jvm中的应用程序需要一个新的java线程。
  • JVM向OS请求创建一个新的线程。
  • OS试图创建一个新的线程,这个线程需要申请内存。
  • OS拒绝分配内存给线程,因为32位Java进程已经耗尽内存地址空间(2-4GB内存地址已被命中)或者OS的虚拟内存已经完全耗尽。
  • java.lang.OutOfMemoryError: Unable to create new native thread异常被触发。

实例

下面的代码循环创建和开始线程。当运行代码时,很快就到达操作系统的限制,java.lang.OutOfMemoryError: Unable to create new native thread异常被触发。

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

确切的线程数目限制是依赖于平台的,例如Windows,Linux和Mac OS X如下:

64-bit Mac OS X 10.9, Java 1.7.0_45 – JVM dies after #2031 threads have been created
64-bit Ubuntu Linux, Java 1.7.0_45 – JVM dies after #31893 threads have been created
64-bit Windows 7, Java 1.7.0_45 – due to a different thread model used by the OS,250000,交换文件到10G,程序非常慢

通过一个小测试可以知道线程的极限数目。
实际Ubuntu16.04.3 x64, Java 1.8.0_151–JVM dies after 11658 threads have been created
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at OOM_TestThread.main(OOM_TestThread.java:9)
实际的ulimit限制有6w+(63812),也修改过idea的VM的Xmx,由原先的750m调整到6168,效果也是一样。
lj@lj-HP-ProBook-640-G1:~/Downloads/source/java-study/javaUnversailTest$ ulimit -a
max user processes (-u) 63812
也使用命令行运行,实际数值差不多,都在116xx左右,差别不大。

解决方案

有时,我们可以通过增加OS级限制,绕过Unable to create new native thread issue错误。例如你限制了JVM可在用户空间创建的线程数,那么你可以检查并增加这个限制:
lj@lj-HP-ProBook-640-G1:~/Downloads$ ulimit -a
core file size (blocks, -c) 0
...... ...... ...... ...... ......
max user processes (-u) 63812
到达线程限制预示着程序错误。当应用产生数以千计的线程,有时会产生异常可怕的错误--并没有太多的应用需要巨量的线程数目。
解决这个问题的一个方式:执行线程dump,可以理解当时的状况。

java.lang.OutOfMemoryError:Out of swap space

java程序在启动的时候是有内存限制的。这个限制是用参数-Xmx和其他相似的参数说明。有些情况下,JVM要求的内存比可用的物理内存还大,OS(操作系统)开始交换内存的内容到硬盘。

java.lang.OutOfMemoryError: Out of swap space异常表明:交换空间同样耗尽,并且新的内存申请失败,因为缺少物理内存和交换空间。

原因分析

当从堆空间申请字节内存失败,并且堆空间也耗尽的时候,java.lang.OutOfMemoryError: Out of swap space异常将被触发。该错误消息中包含分配失败的大小(以字节为单位)和请求失败的原因。

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

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

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

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

解决方案

解决这个问题有几个办法,通常最简单的方法就是增加交换空间,不同平台实现的方式会有所不同,比如在Linux下可以通过如下命令实现:
先用free -m查看交换空间定义的是多少
swapoff -a --关闭交换区
dd if=/dev/zero of=swapfile bs=1024 count=655360--根目录下创建一个名为swapfile,大小640M
mkswap swapfile--将swapfile设置为swap区
swapon swapfile--启用交换区

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

如果你的应用程序部署在JVM需要同其他进程激烈竞争获取资源的物理机上,建议将服务隔离到单独的虚拟机中
但在许多情况下,您唯一真正可行的替代方案是:

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

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

java.lang.OutOfMemoryError:Requested array size exceeds VM limit

Java对应用程序可以分配的最大数组大小有限制。不同平台限制有所不同,但通常在1到21亿个元素之间。

当你遇到Requested array size exceeds VM limit错误时,意味着你的应用程序试图分配大于Java虚拟机可以支持的数组。

原因分析

该错误由JVM中的native code抛出。 JVM在为数组分配内存之前,会执行特定于平台的检查:分配的数据结构是否在此平台中是可寻址的。

你很少见到这个错误是因为Java数组的索引是int类型。 Java中的最大正整数为2 ^ 31 - 1 = 2,147,483,647。 并且平台特定的限制可以非常接近这个数字,例如:我的环境上(64位macOS,运行Jdk1.8)可以初始化数组的长度高达2,147,483,645(Integer.MAX_VALUE-2)。如果再将数组的长度增加1到Integer.MAX_VALUE-1会导致熟悉的OutOfMemoryError:
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
但是,在使用OpenJDK 6的32位Linux上,在分配具有大约11亿个元素的数组时,您将遇到Requested array size exceeded VM limit的错误。 要理解你的特定环境的限制,运行下文中描述的小测试程序。

示例

for (int i = 3; i >= 0; i--) {
try {
int[] arr = new int[Integer.MAX_VALUE-i];
System.out.format("Successfully initialized an array with %,d elements.\n", Integer.MAX_VALUE-i);
} catch (Throwable t) {
t.printStackTrace();
}
}

该示例重复四次,并在每个回合中初始化一个长原语数组。 该程序尝试初始化的数组的大小在每次迭代时增加1,最终达到Integer.MAX_VALUE。 现在,当使用Hotspot 7在64位Mac OS X上启动代码片段时,应该得到类似于以下内容的输出:
java.lang.OutOfMemoryError: Java heap space
at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)
java.lang.OutOfMemoryError: Java heap space
at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)
注意,在出现Requested array size exceeded VM limit之前,出现了更熟悉的java.lang.OutOfMemoryError: Java heap space。 这是因为初始化2 ^ 31-1个元素的数组需要腾出8G的内存空间,大于JVM使用的默认值。

解决方案

java.lang.OutOfMemoryError:Requested array size exceeds VM limit可能会在以下任一情况下出现:
数组增长太大,最终大小在平台限制和Integer.MAX_INT之间
你有意分配大于2 ^ 31-1个元素的数组
在第一种情况下,检查你的代码库,看看你是否真的需要这么大的数组。也许你可以减少数组的大小,或者将数组分成更小的数据块,然后分批处理数据。
在第二种情况下,记住Java数组是由int索引的。因此,当在平台中使用标准数据结构时,数组不能超过2 ^ 31-1个元素。事实上,在编译时就会出错:error:integer number too large。

java.lang.OutOfMemoryError:Kill process or sacrifice child

为了理解这个错误,我们需要补充一点操作系统的基础知识。操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,名叫“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer被激活,然后选择一个进程杀掉。哪一个进程这么倒霉呢?选择的算法和想法都很朴实:谁占用内存最多,谁就被干掉。如果你对OOM Killer感兴趣的话,建议你阅读参考资料2中的文章。

OOM Killer,
当可用虚拟虚拟内存(包括交换空间)消耗到让整个操作系统面临风险时,就会产生Out of memory:Kill process or sacrifice child错误。在这种情况下,OOM Killer会选择“流氓进程”并杀死它。

原因分析

默认情况下,Linux内核允许进程请求比系统中可用内存更多的内存,但大多数进程实际上并没有使用完他们所分配的内存。这就跟现实生活中的宽带运营商类似,他们向所有消费者出售一个100M的带宽,远远超过用户实际使用的带宽,一个10G的链路可以非常轻松的服务100个(10G/100M)用户,但实际上宽带运行商往往会把10G链路用于服务150人或者更多,以便让链路的利用率更高,毕竟空闲在那儿也没什么意义。

Linux内核采用的机制跟宽带运营商差不多,一般情况下都没有问题,但当大多数应用程序都消耗完自己的内存时,麻烦就来了,因为这些应用程序的内存需求加起来超出了物理内存(包括 swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。就如同上面的例子中,如果150人都占用100M的带宽,那么总的带宽肯定超过了10G这条链路能承受的范围。

示例

当你在Linux上运行如下代码:

public static void main(String[] args){
List<int[]> l = new java.util.ArrayList();
for (int i = 10000; i < 100000; i++) {
try {
l.add(new int[100000000]);
} catch (Throwable t) {
t.printStackTrace();
}
}
}

在Linux的系统日志中/var/log/kern.log会出现以下日志:
Jun 4 07:41:59 plumbr kernel: [70667120.897649] Out of memory: Kill process 29957 (java) score 366 or sacrifice child
Jun 4 07:41:59 plumbr kernel: [70667120.897701] Killed process 29957 (java) total-vm:2532680kB, anon-rss:1416508kB, file-rss:0kB
注意:你可能需要调整交换文件和堆大小,否则你将很快见到熟悉的Java heap space异常。在作者的测试用例中,使用-Xmx2g指定的2g堆,并具有以下交换配置:

解决方案

解决这个问题最有效也是最直接的方法就是升级内存,其他方法诸如:调整OOM Killer配置、水平扩展应用,将内存的负载分摊到若干小实例上..... 我们不建议的做法是增加交换空间。当您回想起 Java 是一种垃圾收集的语言时, 这个解决方案似乎已经不那么有利可图了。现代 GC 算法在物理内存中运行时效率很高, 但是在处理交换空间分配时, 效率很差。交换可以增加几个数量级的GC暂停的长度, 因此在跳转到此解决方案之前, 您应该三思而后行。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 3 条回复 时间 点赞

好文,值得细品

很不错,最近工作上遇到了这个问题,研究一下

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up