关于java:Java中的OutOfMemoryError的各种情况及解决和JVM内存结构

7次阅读

共计 7861 个字符,预计需要花费 20 分钟才能阅读完成。

在 JVM 中内存一共有 3 种:Heap(堆内存),Non-Heap(非堆内存)[3] 和 Native(本地内存)。[1]

堆内存是运行时调配所有类实例和数组的一块内存区域。非堆内存蕴含办法区和 JVM 外部解决或优化所需的内存,寄存有类构造(如运行时常量池、字段及办法构造,以及办法和构造函数代码)。本地内存是由操作系统治理的虚拟内存。当一个利用内存不足时就会抛出 java.lang.OutOfMemoryError 异样。[1]

问题 表象 诊断工具
内存不足 OutOfMemoryError Java Heap Analysis Tool(jhat) [4]
Eclipse Memory Analyzer(mat) [5]
内存透露 应用内存增长,频繁 GC Java Monitoring and Management Console(jconsole) [6]
JVM Statistical Monitoring Tool(jstat) [7]
  一个类有大量的实例 Memory Map(jmap) – “jmap -histo” [8]
  对象被误援用 jconsole [6] 或 jmap -dump + jhat 8
Finalizers 对象期待完结 jconsole [6] 或 jmap -dump + jhat 8

OutOfMemoryError 在开发过程中是司空见惯的,遇到这个谬误,老手程序员都晓得从两个方面动手来解决:一是排查程序是否有 BUG 导致内存透露;二是调整 JVM 启动参数增大内存。OutOfMemoryError 有好几种状况,每次遇到这个谬误时,察看 OutOfMemoryError 前面的提示信息,就能够发现不同之处,如:

java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: unable to create new native thread
java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Requested array size exceeds VM limit

尽管都叫 OutOfMemoryError,但每种谬误背地的成因是不一样的,解决办法也要视状况而定,不能一概而论。只有深刻理解 JVM 的内存构造并仔细分析错误信息,才有可能做到隔靴搔痒,手到病除。

JVM 标准

JVM 标准对 Java 运行时的内存划定了几块区域(详见这里),有:JVM 栈(Java Virtual Machine Stacks)、堆(Heap)、办法区(Method Area)、常量池(Runtime Constant Pool)、本地办法栈(Native Method Stacks),但对各块区域的内存布局和地址空间却没有明确规定,而留给各 JVM 厂商施展的空间。

HotSpot JVM

Sun 自家的 HotSpot JVM 实现对堆内存构造有绝对明确的阐明。依照 HotSpot JVM 的实现,堆内存分为 3 个代:Young Generation、Old(Tenured) Generation、Permanent Generation。家喻户晓,GC(垃圾收集)就是产生在堆内存这三个代下面的。Young 用于调配新的 Java 对象,其又被分为三个局部:Eden Space 和两块 Survivor Space(称为 From 和 To),Old 用于寄存在 GC 过程中从 Young Gen 中存活下来的对象,Permanent 用于寄存 JVM 加载的 class 等元数据。详情参见 HotSpot 内存治理白皮书。堆的布局图示如下:

依据这些信息,咱们能够推导出 JVM 标准的内存分区和 HotSpot 实现中内存区域的对应关系:JVM 标准的 Heap 对应到 Young 和 Old Generation,办法区和常量池对应到 Permanent Generation。对于 Stack 内存,HotSpot 实现也没有具体阐明,但 HotSpot 白皮书上提到,Java 线程栈是用宿主操作系统的栈和线程模型来示意的,Java 办法和 native 办法共享雷同的栈。因而,能够认为在 HotSpot 中,JVM 栈和本地办法栈是一回事。

操作系统

因为一个 JVM 过程首先是一个操作系统过程,因而会遵循操作系统过程地址空间的规定。32 位零碎的地址空间为 4G,即最多示意 4GB 的虚拟内存。在 Linux 零碎中,高地址的 1G 空间(即 0xC0000000~0xFFFFFFFF)被零碎内核占用,低地址的 3G 空间(即 0×00000000~0xBFFFFFFF)为用户程序所应用(显然 JVM 过程运行在这 3G 的地址空间中)。这 3G 的地址空间从低到高又分为多个段;Text 段用于存放程序二进制代码;Data 段用于寄存编译时已初始化的动态变量;BSS 段用于寄存未初始化的动态变量;Heap 即堆,用于动态内存调配的数据结构,C 语言的 malloc 函数申请的内存即是从此处调配的,Java 的 new 实例化的对象也是自此调配。不同于后面三个段,Heap 空间是可变的,其上界由低地址向高地址增长。内存映射区,加载的动态链接库位于这个区中;Stack 即栈空间,线程的执行即是占用栈内存,栈空间也是可变的,但它是通过下界从高地址向低地址挪动而增长的。详情参见这里。图示如下:

JVM 自身是由 native code 所编写的,所以 JVM 过程同样具备 Text/Data/BSS/Heap/MemoryMapping/Stack 等内存段。而 Java 语言的 Heap 该当是建设在操作系统过程的 Heap 之上的,Java 语言的 Stack 应该也是建设操作系统过程 Stack 之上的。综合 HotSpot 的内存区域和操作系统过程的地址空间,能够大抵失去下列图示:

Java 线程的内存是位于 JVM 或操作系统的栈(Stack)空间中,不同于对象——是位于堆(Heap)中。这是很多老手程序员容易误会的中央。留神,“Java 线程的内存”这个用词不是指 Java.lang.Thread 对象的内存,java.lang.Thread 对象自身是在 Heap 中调配的,当调用 start() 办法之后,JVM 会创立一个执行单元,最终会创立一个操作系统的 native thread 来执行,而这个执行单元或 native thread 是应用 Stack 内存空间的。

通过上述铺垫,能够得悉,JVM 过程的内存大抵分为 Heap 空间和 Stack 空间两局部。Heap 又分为 Young、Old、Permanent 三个代。Stack 分为 Java 办法栈和 native 办法栈(不做辨别),在 Stack 内存区中,能够创立多个线程栈,每个线程栈占据 Stack 区中一小部分内存,线程栈是一个 LIFO 数据结构,每调用一个办法,会在栈顶创立一个 Frame,办法返回时,相应的 Frame 会从栈顶移除(通过挪动栈顶指针)。在这每一部分内存中,都有可能会呈现溢出谬误。回到结尾的 OutOfMemoryError,上面一一阐明谬误起因和解决办法(每个 OutOfMemoryError 都有可能是程序 BUG 导致,因而解决办法不包含对 BUG 的排查)。

OutOfMemoryError

1.java.lang.OutOfMemoryError: Java heap space
起因:Heap 内存溢出,意味着 Young 和 Old generation 的内存不够。
解决:调整 java 启动参数 -Xms -Xmx 来减少 Heap 内存。

 堆内存溢出时,首先判断以后最大内存是多少(参数:-Xmx 或 -XX:MaxHeapSize=),能够通过命令 jinfo -flag MaxHeapSize 查看运行中的 JVM 的配置,如果该值曾经较大则应通过 mat 之类的工具查找问题,或 jmap -histo 查找哪个或哪些类占用了比拟多的内存。参数 -verbose:gc(-XX:+PrintGC) -XX:+PrintGCDetails 能够打印 GC 相干的一些数据。如果问题比拟难排查也能够通过参数 -XX:+HeapDumpOnOutOfMemoryError 在 OOM 之前 Dump 内存数据再进行剖析。此问题也能够通过 histodiff 打印屡次内存 histogram 之前的差值,有助于查看哪些类过多被实例化,如果过多被实例化的类被定位到后能够通过 btrace 再跟踪。上面代码可再现该异样:List<String> list = new ArrayList<String>();
while(true) list.add(new String("Consume more memory!"));

2.java.lang.OutOfMemoryError: unable to create new native thread
起因:Stack 空间不足以创立额定的线程,要么是创立的线程过多,要么是 Stack 空间的确小了。
解决:因为 JVM 没有提供参数设置总的 stack 空间大小,但能够设置单个线程栈的大小;而零碎的用户空间一共是 3G,除了 Text/Data/BSS/MemoryMapping 几个段之外,Heap 和 Stack 空间的总量无限,是此消彼长的。因而遇到这个谬误,能够通过两个路径解决:1. 通过 -Xss 启动参数缩小单个线程栈大小,这样便能开更多线程(当然不能太小,太小会呈现 StackOverflowError);2. 通过 -Xms -Xmx 两参数缩小 Heap 大小,将内存让给 Stack(前提是保障 Heap 空间够用)。

 在 JVM 中每启动一个线程都会调配一块本地内存,用于寄存线程的调用栈,该空间仅在线程完结时开释。当没有足够本地内存创立线程时就会呈现该谬误。通过以下代码能够很容易再现该问题:[2]
 while(true){new Thread(new Runnable(){public void run() {
            try {Thread.sleep(60*60*1000);
            } catch(InterruptedException e) {}}    
    }).start();}

3.java.lang.OutOfMemoryError: PermGen space
起因:Permanent Generation 空间有余,不能加载额定的类。
解决:调整 -XX:PermSize= -XX:MaxPermSize= 两个参数来增大 PermGen 内存。个别状况下,这两个参数不要手动设置,只有设置 -Xmx 足够大即可,JVM 会自行抉择适合的 PermGen 大小。

PermGen space 即永恒代,是非堆内存的一个区域。次要寄存的数据是类构造及调用了 intern() 的字符串。List<Class<?>> classes = new ArrayList<Class<?>>();
while(true){MyClassLoader cl = new MyClassLoader();
    try{classes.add(cl.loadClass("Dummy"));
    }catch (ClassNotFoundException e) {e.printStackTrace();
    }
}
类加载的日志能够通过 btrace 跟踪类的加载状况:import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;

@BTrace
public class ClassLoaderDefine {@SuppressWarnings("rawtypes")
    @OnMethod(clazz = "+java.lang.ClassLoader", method = "defineClass", location = @Location(Kind.RETURN))
    public static void onClassLoaderDefine(@Return Class cl) {println("=== java.lang.ClassLoader#defineClass ===");
        println(Strings.strcat("Loaded class:", Reflective.name(cl)));
        jstack(10);
    }
}
除了 btrace 也能够关上日志加载的参数来查看加载了哪些类,能够把参数 -XX:+TraceClassLoading 关上,或应用参数 -verbose:class(-XX:+TraceClassLoading, -XX:+TraceClassUnloading),在日志输入中即可看到哪些类被加载到 Java 虚拟机中。该参数也能够通过 jflag 的命令 java -jar jflagall.jar -flag +ClassVerbose 动静关上 -verbose:class。上面是一个应用了 String.intern() 的例子:
List<String> list = new ArrayList<String>(); int i=0; while(true) list.add(("Consume more memory!"+(i++)).intern());
 你能够通过以下 btrace 脚本查找该类调用:
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class StringInternTrace {@OnMethod(clazz = "/.*/", method = "/.*/", location = @Location(value = Kind.CALL, clazz = "java.lang.String", method = "intern"))
 public static void m(@ProbeClassName String pcm, @ProbeMethodName String probeMethod, @TargetInstance Object instance) {println(strcat(pcm, strcat("#", probeMethod)));
 println(strcat(">>>>", str(instance)));
 }
}

4.java.lang.OutOfMemoryError: Requested array size exceeds VM limit
起因:这个谬误比拟少见(试着 new 一个长度 1 亿的数组看看),同样是因为 Heap 空间有余。如果须要 new 一个如此之大的数组,程序逻辑多半是不合理的。
解决:批改程序逻辑吧。或者也能够通过 -Xmx 来增大堆内存。

 详细信息示意利用申请的数组大小曾经超过堆大小。如应用程序申请 512M 大小的数组,但堆大小只有 256M,这里会抛出 OutOfMemoryError,因为此时无奈冲破虚拟机限度调配新的数组。在大多少状况下是堆内存调配的过小,或是利用尝试调配一个超大的数组,如利用应用的算法计算了谬误的大小。

5. 在 GC 破费了大量工夫,却仅回收了大量内存时,也会报出 OutOfMemoryError,我只遇到过一两次。当应用 -XX:+UseParallelGC 或 -XX:+UseConcMarkSweepGC 收集器时,在上述情况下会报错,在 HotSpot GC Turning 文档上有阐明:
The parallel(concurrent) collector will throw an OutOfMemoryError if too much time is being spent in garbage collection: if more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, an OutOfMemoryError will be thrown.
对这个问题,一是须要进行 GC turning,二是须要优化程序逻辑。

6.java.lang.StackOverflowError
起因:这也内存溢出谬误的一种,即线程栈的溢出,要么是办法调用档次过多(比方存在有限递归调用),要么是线程栈太小。
解决:优化程序设计,缩小办法调用档次;调整 -Xss 参数减少线程栈大小。

7.java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

本地内存调配失败。一个利用的 Java Native Interface(JNI) 代码、本地库及 Java 虚拟机都从本地堆分配内存调配空间。当从本地堆分配内存失败时抛出 OutOfMemoryError 异样。例如:当物理内存及替换分区都用完后,再次尝试从本地分配内存时也会抛出 OufOfMemoryError 异样。

8. java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)

如果异样的详细信息是 <reason> <stack trace> (Native method) 且一个线程堆栈被打印,同时最顶端的桢是本地办法,该异样表明本地办法遇到了一个内存调配问题。与后面一种异样相比,他们的差别是内存调配失败是 JNI 或本地办法发现或是 Java 虚拟机发现。

9.java.lang.OutOfMemoryError: Direct buffer memory

即从 Direct Memory 分配内存失败,Direct Buffer 对象不是调配在堆上,是在 Direct Memory 调配,且不被 GC 间接治理的空间(但 Direct Buffer 的 Java 对象是归 GC 治理的,只有 GC 回收了它的 Java 对象,操作系统才会开释 Direct Buffer 所申请的空间)。通过 -XX:MaxDirectMemorySize= 能够设置 Direct 内存的大小。

List<ByteBuffer> list = new ArrayList<ByteBuffer>(); while(true) list.add(ByteBuffer.allocateDirect(10000000));

10. java.lang.OutOfMemoryError: GC overhead limit exceeded

JDK6 新增谬误类型。当 GC 为开释很小空间占用大量工夫时抛出。个别是因为堆太小。导致异样的起因:没有足够的内存。能够通过参数 -XX:-UseGCOverheadLimit 敞开这个个性。11. java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

本地内存调配失败。一个利用的 Java Native Interface(JNI) 代码、本地库及 Java 虚拟机都从本地堆分配内存调配空间。当从本地堆分配内存失败时抛出 OutOfMemoryError 异样。例如:当物理内存及替换分区都用完后,再次尝试从本地分配内存时也会抛出 OufOfMemoryError 异样。

  1. java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)

如果异样的详细信息是 <reason> <stack trace> (Native method) 且一个线程堆栈被打印,同时最顶端的桢是本地办法,该异样表明本地办法遇到了一个内存调配问题。与后面一种异样相比,他们的差别是内存调配失败是 JNI 或本地办法发现或是 Java 虚拟机发现。

关注公众号:java 宝典

正文完
 0