在JVM中内存一共有3种:Heap(堆内存),Non-Heap(非堆内存) [3]和Native(本地内存)。 [1]html
堆内存是运行时分配全部类实例和数组的一块内存区域。非堆内存包含方法区和JVM内部处理或优化所需的内存,存放有类结构(如运行时常量池、字段及方法结构,以及方法和构造函数代码)。本地内存是由操做系统管理的虚拟内存。当一个应用内存不足时就会抛出java.lang.OutOfMemoryError 异常。 [1]java
问题 | 表象 | 诊断工具 |
内存不足 | 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][4] | |
Finalizers | 对象等待结束 | jconsole [6] 或 jmap -dump + jhat [8][4] |
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规范对Java运行时的内存划定了几块区域(详见这里),有:JVM栈(Java Virtual Machine Stacks)、堆(Heap)、方法区(Method Area)、常量池(Runtime Constant Pool)、本地方法栈(Native Method Stacks),但对各块区域的内存布局和地址空间却没有明确规定,而留给各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栈和本地方法栈是一回事。oracle
因为一个JVM进程首先是一个操做系统进程,所以会遵循操做系统进程地址空间的规定。32位系统的地址空间为4G,即最多表示4GB的虚拟内存。在Linux系统中,高地址的1G空间(即0xC00000000xFFFFFFFF)被系统内核占用,低地址的3G空间(即0×000000000xBFFFFFFF)为用户程序所使用(显然JVM进程运行在这3G的地址空间中)。这3G的地址空间从低到高又分为多个段;Text段用于存放程序二进制代码;Data段用于存放编译时已初始化的静态变量;BSS段用于存放未初始化的静态变量;Heap即堆,用于动态内存分配的数据结构,C语言的malloc函数申请的内存便是今后处分配的,Java的new实例化的对象也是自此分配。不一样于前面三个段,Heap空间是可变的,其上界由低地址向高地址增加。内存映射区,加载的动态连接库位于这个区中;Stack即栈空间,线程的执行便是占用栈内存,栈空间也是可变的,但它是经过下界从高地址向低地址移动而增加的。详情参见这里。图示以下:
app
JVM自己是由native code所编写的,因此JVM进程一样具备Text/Data/BSS/Heap/MemoryMapping/Stack等内存段。而Java语言的Heap应当是创建在操做系统进程的Heap之上的,Java语言的Stack应该也是创建操做系统进程Stack之上的。 综合HotSpot的内存区域和操做系统进程的地址空间,能够大体获得下列图示:
less
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的排查)。
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参数增长线程栈大小。
本地内存分配失败。一个应用的Java Native Interface(JNI)代码、本地库及Java虚拟机都从本地堆分配内存分配空间。当从本地堆分配内存失败时抛出OutOfMemoryError异常。例如:当物理内存及交换分区都用完后,再次尝试从本地分配内存时也会抛出OufOfMemoryError异常。
若是异常的详细信息是
(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));
JDK6新增错误类型。当GC为释放很小空间占用大量时间时抛出。通常是由于堆过小。致使异常的缘由:没有足够的内存。能够经过参数-XX:-UseGCOverheadLimit关闭这个特性。 11. java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?
本地内存分配失败。一个应用的Java Native Interface(JNI)代码、本地库及Java虚拟机都从本地堆分配内存分配空间。当从本地堆分配内存失败时抛出OutOfMemoryError异常。例如:当物理内存及交换分区都用完后,再次尝试从本地分配内存时也会抛出OufOfMemoryError异常。
若是异常的详细信息是
关注公众号:java宝典