目录
一、前言和定义
在先前的博客中已经分析了JAVA对象内存分配,对象布局和对象访问定位,如果没有看过该博客的客官请移步Java中的对象_熟透的蜗牛的博客-CSDN博客。那么JAVA在运行过程中可能时时刻刻都在创建新的对象,那么在创建对象时如果没有分配到内存的时候会发生什么?首先可能会频繁的进行垃圾回收,如果回收的速度赶不上创建的速度,那么这时候当内存达到一定量时就会发生内存溢出。
内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现OOM。比如我们的JVM内存还剩10MB,但是创建了一个20MB的对象,那么此时JVM已经没有足够的空间可以盛放这个对象,此时就会发生内存溢出的问题。
内存泄漏(memory leak):是指程序在申请内存后,无法释放已申请的内存空间。比如一个对象占用了10M的空间,但是它使用完了,一直不释放。如果一次内存泄漏可以容忍,但是有很多的内存泄漏,不管有多大的内存迟早会被用光,而导致的后果就是内存溢出。
二、内存溢出和内存泄漏
在《JAVA虚拟机规范》中指出,程序计数器是唯一一个不会产生OOM的区域。那么言外之意就是其他几个区域都有可能产生内存溢出。下面一个一个开始分析。
JAVA堆溢出
为了更好的演示内存溢出,修改JVM的参数,我的jdk版本为17
-Xmx20M -Xms20M -Xmn10M -XX:+HeapDumpOnOutOfMemoryError
-Xmx20M 堆最大内存
-Xms20M 堆初始内存
-Xmn10M 堆内新生代的大小
-XX:+HeapDumpOnOutOfMemoryError 当发生内存溢出时dump出当前内存堆的快照
代码演示
public class TestHeapOOM {
static class User {
private long id;
private String name;
User() {
this.id = 100L;
this.name = "熟透的蜗牛";
}
}
public static void main(String[] args) {
ArrayList<User> userList = new ArrayList<>();
while (true) {
User user = new User();
userList.add(user);
}
}
}
运行结果如下,发生了OOM。
JVM虚拟机栈和本地方法栈
这两个区域在《JAVA虚拟机规范》有两种异常
- 如果线程请求的栈深度大于虚拟机所允许的最大深度将抛出栈溢出异常(StackOverflowError)异常
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。由于hotspot虚拟机不支持动态扩展,所以除非在创建线程申请内存时就因为无法获取足够的内存空间出现OOM异常,否在线程运行期间不会出现OOM异常。
为了更好的演示修改JVM参数
-Xmx20M -Xms20M -Xmn10M -Xss180k -XX:+HeapDumpOnOutOfMemoryError
-Xss180k设置每个线程可使用的内存大小,即栈的大小。在jdk17最小值为180k,其他jdk版本和不同的操作系统这个值会存在差异。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,可能会出现内存溢出的错误。
代码演示
public class TestStackOOM {
private int length = 1;
//递归
public void stackLeak() {
length++;
stackLeak();
}
public static void main(String[] args) {
TestStackOOM testStackOOM = new TestStackOOM();
try {
testStackOOM.stackLeak();
} catch (Throwable e) {
System.out.println("栈深度为>>>>>>>>>>>>>>:" + testStackOOM.length);
throw e;
}
}
}
运行结果
对于多线程情况下产生的内存溢出,感兴趣的可以自己试试。
方法区
由于JDK8之后HotSpot放弃了永久代,改为元空间实现方法区,而元空间则不再占用堆内存的大小,而是使用本地内存实现,所以除非在极端情况下,电脑没有一点内存分配给方法区,否则理论上不会出现OOM异常了。
三、如何尽可能的避免内存溢出和内存泄漏
那么如何尽量避免内存溢出和内存泄漏呢?本人总结了几点。
内存溢出
- 内存中加载的数据量过于庞大,比如一次从数据库取出过多数据,这个时候我们就需要采取分页。
- 某些对象使用完后未清空,使得JVM不能回收,这个时候我们可以在代码中对不再使用的对象手动设置为null。
- 尽量避免在死循环中创建大量的对象。
- 在内存允许的情况下对堆的最大内存做适当的调整。
- 设置合适的垃圾回收器。
内存泄漏
- 使用完的资源记得关闭清理,比如io,数据库连接、ThreadLocal等。