在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.*;@BTracepublic 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.*;@BTracepublic 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异样。
- java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)
如果异样的详细信息是 <reason> <stack trace> (Native method) 且一个线程堆栈被打印,同时最顶端的桢是本地办法,该异样表明本地办法遇到了一个内存调配问题。与后面一种异样相比,他们的差别是内存调配失败是JNI或本地办法发现或是Java虚拟机发现。
关注公众号:java宝典