这里需要了解到下面这四种比较常见的内存问题。
1.堆内存溢出
堆内存中主要存放对象、数组等,只要不断地创建这些对象,并且保证GC Roots到对象之间有可达路径来避免垃圾收集回收机制清除这些对象,当这些对象所占空间超过最大堆容量时,就会产生OutOfMemoryError的异常。堆内存异常示例如下:
/**
* 运行时,不断在堆中创建OOMObject类的实例对象,且while执行结束之前,GC Roots(代码中的oomObjectList)到对象(每一个OOMObject对象)之间有可达路径,垃圾收集器就无法回收它们,最终导致内存溢出。
*/
List<OOMObject> oomObjectList = new ArrayList<>();
while (true) {
oomObjectList.add(new OOMObject());
}
运行后就会报异常,在堆栈信息中可以看到 java.lang.OutOfMemoryError: Java heap space 的信息,说明在堆内存空间产生内存溢出的异常。
异常原因过程:新产生的对象最初分配在新生代,新生代满后会进行一次Minor GC,如果Minor GC后空间不足会把该对象和新生代满足条件的对象放入老年代,老年代空间不足时会进行Full GC,之后如果空间还不足以存放新对象则抛出OutOfMemoryError异常。
造成此异常的常见原因有:
1、内存中加载的数据过多如一次从数据库中取出过多数据;
2、集合对对象引用过多且使用完后没有清空;
3、代码中存在死循环或循环产生过多重复对象;
4、堆内存分配不合理;
5、网络连接问题、数据库问题等。
解决方式有:
1. 适当调整-Xms和-Xmx两个jvm参数;
2. 尽量避免大的对象的申请,像文件上传,大批量从数据库中获取,这是需要避免的,尽量分块或者分批处理。
2 虚拟机栈/本地方法栈溢出
StackOverflowError:当线程请求的栈的深度大于虚拟机所允许的最大深度,则抛出StackOverflowError,
简单理解就是虚拟机栈中的栈帧数量过多(一个线程嵌套调用的方法数量过多)时,就会抛出StackOverflowError异常。
最常见的场景就是方法无限递归调用,如下:
/**
* 运行时,不断调用doSomething()方法,main线程不断创建栈帧并入栈,导致栈的深度越来越大,最终导致栈溢出。
*/
public class StackSOF {
public void doSomething(){
doSomething();
}
public static void main(String[] args) {
StackSOF stackSOF=new StackSOF();
stackSOF.doSomething();
}
上述代码执行后抛出:Exception in thread “Thread-0” java.lang.StackOverflowError的异常。
解决方式:
1、如果程序中确实有递归调用,出现栈溢出时,可以调高-Xss大小,就可以解决栈内存溢出的问题了。
2、递归调用防止形成死循环,否则就会出现栈内存溢出。
OutOfMemoryError:如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。我们可以这样理解:
虚拟机中可以供栈占用的空间≈可用物理内存 - 最大堆内存 - 最大方法区内存,
比如一台机器内存为4G,系统和其他应用占用2G,虚拟机可用的物理内存为2G,最大堆内存为1G,最大方法区内存为512M,那可供栈占有的内存大约就是512M,假如我们设置每个线程栈的大小为1M,那虚拟机中最多可以创建512个线程,超过512个线程再创建就没有空间可以给栈了,就报OutOfMemoryError异常了。
栈上能够产生OutOfMemoryError的示例如下:
/**
* 设置每个线程的栈大小:-Xss2m
* 运行时,不断创建新的线程(且每个线程持续执行),每个线程对一个栈,最终没有多余的空间来为新的线程分配,导致OutOfMemoryError
*/
public class StackOOM {
public void doSomething() {
Thread.sleep(100000000);
}
public static void main(String[] args) {
final StackOOM stackOOM = new StackOOM();
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
stackOOM.doSomething();
}
});
thread.start();
上述代码运行后会报异常,在堆栈信息中可以看到 java.lang.OutOfMemoryError: unable to create new native thread的信息,无法创建新的线程,说明是在扩展栈的时候产生的内存溢出异常。
总结:
在线程较少的时候,某个线程请求深度过大(一个栈内调用的方法太多),会报StackOverflow异常,解决这种问题可以适当加大栈的深度(增加栈空间大小),也就是把-Xss的值设置大一些,但一般情况下是代码问题的可能性较大;
在虚拟机产生线程时,无法为该线程申请栈空间了(线程太多,栈不够),会报OutOfMemoryError异常,解决这种问题可以适当减小栈的深度,也就是把-Xss的值设置小一些,每个线程占用的空间小了,总空间一定就能容纳更多的线程,但是操作系统对一个进程的线程数有限制,经验值在3000~5000左右。在jdk1.5之前-Xss默认是256k,jdk1.5之后默认是1M,这个选项对系统硬性还是蛮大的,设置时要根据实际情况,谨慎操作。
3 本机直接内存溢出
本机直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但Java中用到NIO相关操作时(比如ByteBuffer的allocteDirect方法申请的是本机直接内存),也可能会出现内存溢出的异常。
磁盘--》内核缓存(本机缓存)--》用户缓存(jvm内存)
4 内存泄漏
内存泄露:指一个不再被程序使用的对象或变量一直被占据在内存中。
java中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
开发中容易造成内存泄露的操作如下:
1、创建大量无用对象
2、静态集合类的使用
3、各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭
4、监听器的使用:释放对象时,没有删除相应的监听器。