Java 内存溢出分析

原文地址:Java 内存溢出分析html

博客地址:http://www.moonxy.comjava

1、前言ide

Java 的 JVM 的内存通常可分为 3 个区:堆(heap)、栈(stack)和方法区(method)。wordpress

1.1 堆区工具

1)存储的所有是对象,每一个对象都包含一个与之对应的 Class 的信息,Class 的目的是获得操做指令;spa

2)JVM 只有一个堆区(heap)被全部线程共享,堆中不存放基本类型和对象引用,只存放对象自己。操作系统

1.2 栈区线程

1)每一个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中;3d

2)每一个栈中的数据(原始类型和对象引用)都是私有的,其余栈不能访问;code

3)栈分为3个部分:基本类型变量区、执行环境上下文、操做指令区(存放操做指令)。

1.3 方法区

1)又叫静态区或永久代,跟堆同样,被全部的线程共享。方法区包含全部的 Class 和 static 变量;

2)方法区中包含的都是在整个程序中永远惟一的元素,如 Class,static 变量;

3)运行时常量池都分配在 Java 虚拟机的方法区之中,可是从 JDK 1.7 的 HotSpot 虚拟机开始中,已经把本来放在永久代的字符串常量池移出了。

2、内存溢出分析

如下代码验证,建议使用 JDK 1.6,由于从 JDK 1.7 开始,HotSpot 虚拟机改进了不少,特别是方法区(永久代)。

2.1 Java 堆溢出

Java堆是用于存储对象实例的,所以,只要咱们不断建立对象,而且保证对象不被垃圾回收机制清除,那么当堆中对象的大小超过了最大堆的容量限制,就会出现堆内存溢出。

堆溢出常见的异常是:java.lang.OutOfMemoryError: Java heap space

下面这段代码,将Java堆的大小设置为 20MB,而且不可扩展(即将堆的最小值 -Xms 参数和最大值 -Xmx 参数设置为相等的 20MB,来避免堆自动扩展);经过参数 -XX:+HeapDumpOnOutOfMemoryError 可让虚拟机在出现内存溢出异常时生成当前内存堆的快照,以便过后进行分析;-verbose:gc 的做用是在虚拟机发生内存回收时在输出设备显示信息。

package com.java.error; import java.util.ArrayList; import java.util.List; /** * VM Args:-verbose:gc -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * Error:java.lang.OutOfMemoryError: Java heap space * @author moonxy * */
public class HeapOOM { public static void main(String[] args) { List<HeapOOM> list = new ArrayList<HeapOOM>(); while(true) { list.add(new HeapOOM()); } } }

运行后显示以下:

[GC (Allocation Failure)  5380K->3745K(19968K), 0.0042158 secs]
[GC (Allocation Failure)  9287K->9710K(19968K), 0.0058399 secs]
[Full GC (Ergonomics)  9710K->7589K(19968K), 0.1200134 secs]
[Full GC (Ergonomics)  16387K->12869K(19968K), 0.1112792 secs]
[Full GC (Ergonomics)  16428K->16382K(19968K), 0.1711686 secs]
[Full GC (Allocation Failure)  16382K->16370K(19968K), 0.1371103 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1204.hprof ...
Heap dump file created [28024534 bytes in 0.077 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at com.java.error.OutOfMemoryJavaHeapSpaceTest.main(OutOfMemoryJavaHeapSpaceTest.java:19)

控制台打印了OutOfMemoryError,而且提示是Java heap发生的内存溢出,同时生成了dump文件。对于Java堆溢出,通常经过内存映像分析工具(如Eclipse Memory Analyzer)对dump文件进行堆快照分析,确认内存中的对象是否是必要的:

若是对象不是必要的,那就属于内存泄漏,须要分析为何对象没有被回收;

若是对象确实有必要继续存在,那就属于内存溢出,须要经过将堆的大小调高(-Xms-Xmx)来避免内存溢出。

此处比较一下内存泄漏和内存溢出这两个概念,二者一般一块儿说,可是两个倒是不一样的概念,彼此之间又有联系,以下:

内存泄漏 memory leak:是指程序在申请内存后,没法释放已经申请的内存空间,一次内存泄露彷佛不会有大的影响,但内存泄漏堆积后的后果就是内存溢出;

内存溢出 out of memory:是指程序申请内存后,没有足够的内存供申请者使用或将 long 类型的数据存储到 int 类型的存储空间时,就会出现 OOM 错误。

2.2 虚拟机栈和本地方法栈溢出

因为HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以,对于 HotSpot 来讲,-Xoss参数(设置本地方法栈大小)是无效的,栈容量只由 -Xss 设置。

在Java虚拟机规范中,这个区域有两种异常状况:

若是线程运行时的栈帧的总大小超过虚拟机限制的大小,会抛出 StackOverflowError,这一点一般发生在递归运行时,栈溢出容易出现:java.lang.StackOverflowError

若是虚拟机栈设置为能够动态扩展,而且在扩展时没法申请到足够内存,则会抛出 OutOfMemoryError,此时容易出现:java.lang.OutOfMemoryError: unable to create native thread

StackOverflowError

/** * VM Args:-Xss128k * Error:java.lang.StackOverflowError * @author moonxy */
public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }

运行后显示以下:

994
Exception in thread "main" java.lang.StackOverflowError
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:15)
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
     ……

OutOfMemoryError

不建议执行以下验证,容易发生系统假死,由于在 Windows 平台的虚拟机中,Java 线程是映射到操做系统的内核线程中的。

/** * VM Args:-verbose:gc -Xss2m * Error:java.lang.OutOfMemoryError: unable to create native thread * @author moonxy * */
public class JavaVMStackOOM { private void dontStop() { while(true) { } } public void stackLeakByThread() { while(true) { Thread thread = new Thread(new Runnable(){ @Override public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }

运行后显示以下:

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

线程须要消耗栈容量,而操做系统分配给每一个进程的内存是有限制的。考虑以下场景:系统总内存 6G,JVM分配了 2G 内存,其中堆内存分配了 1G,方法区(永久代)分配 512M,忽略其余较小的内存分配,则剩余分配给虚拟机栈和本地方法栈的内存就只有512M,颇有可能没有足够的可用内存建立不少的线程。就有可能出现 java.lang.OutOfMemoryError: unable to create new native thread。若是是这种状况,考虑减少堆内存大小或适当减小每一个线程的栈分配大小。

线程会占用内存,若是每一个线程都占用更多内存,总体上将消耗更多的内存。每一个线程默认占用内存大小取决于 JVM 实现。能够利用 -Xss 参数限制线程内存大小,下降总内存消耗。例如,JVM 默认每一个线程占用 1M 内存,应用有 500个 线程,那么将消耗 500M 内存空间。若是实际上 256K 内存足够线程正常运行,配置 -Xss256k,那么 500 个线程将只须要消耗 125M 内存。(注意,若是 -Xss 设置的太低,又将会产生 java.lang.StackOverflowError 错误)

2.3 方法区溢出

方法区存储的是虚拟机加载的类信息、常量、静态变量、JIT 编译器编译后的代码等数据,在 JDK1.7以前,HotSpot 都是使用 "永久代" 来管理这些数据,也就是说,方法区的数据,其实也是属于堆内存的,会占用堆内存。

所以:方法区的数据大小 < -XX:MaxPermSize < -Xmx

咱们只要限制一下永久代的大小(-XX:MaxPermSize),很容易就会发生方法区溢出,此时方法区容易出现:java.lang.OutOfMemoryError: PermGen space。能够经过设置 -XX:PermSize-XX:MaxPermSize 来限制方法区大小。

/** * VM Args:-verbose:gc -XX:PermSize=10M -XX:MaxPermSize=10M * Error:java.lang.OutOfMemoryError: PermGen space * @author moonxy * */
public class OutOfMemoryPermGenSpaceTest { public static void main(String[] args) { List<String> oomPgsList = new ArrayList<String>(); int i = 0; while(true) { oomPgsList.add(String.valueOf(i++).intern()); } } }

运行后结果以下:

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

若是咱们在 JDK 1.7 的环境下执行上面一样的代码,会发现 while 循环会一直执行下去。缘由是在 JDK 1.7 中,HotSpot 已经再也不将常量放入永久代中进行管理,而是放到内存上限是本机内存的 Native Memory 中,这样作的好处就是减小了内存溢出的概率(没有了-XX:MaxPermSize 的限制),同时常量再也不占用堆的内存。这种 "去永久代" 的作法,从 JDK 1.7 已经开始进行。终于在 JDK 1.8 中,被完全的执行了,永久代被完全去掉了,同时 HotSpot 新增了一块叫作 Mataspace 的区域,并提供了 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 参数,来设置运行 Java 虚拟机使用的 Metaspace 的初始容量和最大容量。

2.4 本机直接内存溢出

直接内存(Direct Memory)并非虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。可是这部份内存也被频繁的使用,并且也可能致使 OutOfMemoryError 出现,即:java.lang.OutOfMemory

本机直接内存的容量能够经过 -XX:MaxDirectMemorySize 指定,若是不指定,默认与 Java 堆最大值(-Xmx指定)同样。

本机直接内存溢出的一个明显特征是,dump 文件很小,由于主要对象都在 direct memory了,而且异常信息也不会说明是在哪一个区域发生内存溢出,就像这样:java.lang.OutOfMemoryError

/** * * VM Args:-verbose:gc -XX:MaxDirectMemorySize * Error:java.lang.OutOfMemory * @author moonxy * */
public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } }

运行结果以下:

Exception in thread "main" java.lang.OutOfMemoryError

综上,当 Java 应用比较大时,能够设置以下的 JVM 参数:

-Xms256m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=512m

注意:

方法区存储的对象类型在 JDK 1.六、1.七、1.8 等不一样的 JDK 版本中都有变化,在具体的开发中,须要特别注意 JDK 版本的不一样,而致使的一些差别。

JDK 1.6 以后提供了 jvisualvm 工具,用于监控 JVM 内存的使用状况,一般在 jdk 的 bin 目录下,直接运行 jvisualvm.exe,能够监控内存泄露、跟踪垃圾回收、运行时内存、cpu 分析、线程分析等。

相关文章
相关标签/搜索