在Java中,和内存相关的问题主要有两种,内存溢出和内存泄漏。
内存溢出(OutOfMemory):就是申请内存时,JVM没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。内存泄露(MemoryLeak):就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。1、内存溢出
在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能。
1.1、Java堆溢出
Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
我们来看一个代码的例子:
/***VM参数:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError*/publicclassHeapOOM{staticclassOOMObject{}publicstaticvoidmain(String[]args){ListOOMObjectlist=newArrayListOOMObject();while(true){list.add(newOOMObject());}}}接下来,我们来设置一下程序启动时的JVM参数。限制内存大小为20M,不允许扩展,并通过参数-XX:+HeapDumpOnOutOf-MemoryError让虚拟机Dump出内存堆转储快照。
在Idea中设置JVM启动参数如下图:
运行一下:
Java堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“Javaheapspace”。Java堆文件快照文件dump到了java_pid.hprof文件。
要解决这个内存区域的异常,常规的处理方法是首先通过内存映像分析工具(如JProfiler、EclipseMemoryAnalyzer等)对Dump出来的堆转储快照进行分析。
看到内存占用信息如下:
然后可以查看代码问题如下:
常见堆JVM相关参数:-XX:PrintFlagsInitial:查看所有参数的默认初始值-XX:PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)-Xms:初始堆空间内存(默认为物理内存的1/64)-Xmx:最大堆空间内存(默认为物理内存的1/4)-Xmn:设置新生代大小(初始值及最大值)-XX:NewRatio:配置新生代与老年代在堆结构的占比-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄(默认15)-XX:+PrintGCDetails:输出详细的GC处理日志打印GC简要信息:①-XX:+PrintGC②-verbose:gc-XX:HandlePromotionFailure:是否设置空间分配担保
1.2、虚拟机栈和本地方法栈溢出
HotSpot虚拟机中将虚拟机栈和本地方法栈合二为一,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设定。关于虚拟机栈和本地方法栈,有两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。1.2.1、StackOverflowError
HotSpot虚拟机不支持栈的动态扩展,在HotSpot虚拟机中,以下两种情况都会导致StackOverflowError。
栈容量过小如下,使用Xss参数减少栈内存容量/***vm参数:-Xssk*/publicclassJavaVMStackSOF{privateintstackLength=1;publicvoidstackLeak(){stackLength++;stackLeak();}publicstaticvoidmain(String[]args)throwsThrowable{JavaVMStackSOFoom=newJavaVMStackSOF();try{oom.stackLeak();}catch(Throwablee){System.out.println(stacklength:+oom.stackLength);throwe;}}}运行结果:
栈帧太大如下,通过一长串变量,来占用局部变量表空间。
运行结果:
无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。
1.2.2、OutOfMemoryError
虽然不支持动态扩展栈,但是通过不断建立线程的方式,也可以在HotSpot上产生内存溢出异常。
需要注意,这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。因为操作系统给每个进程的内存时有限的,线程数一多,自然会超过进程的容量。
创建线程导致内存溢出异常:
/***vm参数:-Xss2M*/publicclassJavaVMStackOOM{privatevoiddontStop(){while(true){}}publicvoidstackLeakByThread(){while(true){Threadthread=newThread(newRunnable(){publicvoidrun(){dontStop();}});thread.start();}}publicstaticvoidmain(String[]args)throwsThrowable{JavaVMStackOOMoom=newJavaVMStackOOM();oom.stackLeakByThread();}}以上是一段比较有风险的代码,可能会导致系统假死,运行结果如下:
1.3、方法区和运行时常量池溢出
这里再提一下方法区和运行时常量池的变迁,JDK1.7以后字符串常量池移动到了堆中,JDK1.8在直接内存中划出一块区域元空间来实现方区域。
String:intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,永久代本身内存不限制可能会出现错误:
java.lang.OutOfMemoryError:PermGenspace1.4、本机直接内存溢出
直接内存(DirectMemory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。
直接通过反射获取Unsafe实例,通过反射向操作系统申请分配内存:
/***vm参数:-Xmx20M-XX:MaxDirectMemorySize=10M*/publicclassDirectMemoryOOM{privatestaticfinalint_1MB=*;publicstaticvoidmain(String[]args)throwsException{FieldunsafeField=Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessible(true);Unsafeunsafe=(Unsafe)unsafeField.get(null);while(true){unsafe.allocateMemory(_1MB);}}}运行结果:
由直接内存导致的内存溢出,一个明显的特征是在HeapDump文件中不会看见有什么明显的异常情况。
2、内存泄漏
内存回收,简单说就是应该被垃圾回收的对象没有被垃圾回收。
在上图中:对象X引用对象Y,X的生命周期比Y的生命周期长,Y生命周期结束的时候,垃圾回收器不会回收对象Y。
我们来看几个内存泄漏的例子:
静态集合类引起内存泄漏静态集合的生命周期和JVM一致,所以静态集合引用的对象不能被释放。publicclassOOM{staticListlist=newArrayList();publicvoidoomTests(){Objectobj=newObject();list.add(obj);}}单例模式:和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在JVM的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被GC回收,导致内存泄漏。数据连接、IO、Socket等连接创建的连接不再使用时,需要调用close方法关闭连接,只有连接被关闭后,GC才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被GC回收。try{Connectionconn=null;Class.forName(