☆* o(≧▽≦)o *☆嗨~我是小奥
个人博客:小奥的博客
CSDN:个人CSDN
Github:传送门
面经分享(牛客主页):传送门
文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️
在面试的过程中,可能有的小伙伴会被问到:你有遇到过OOM吗?你是如何解决的?
当然,在日常的开发中,我们可能永远也遇不到OOM,哈哈哈,当然,万一碰到了呢,所以大家还是有必要来学习一下OOM以及如何排查。
通过阅读这篇文章,我将带领大家解决以下两个问题:
下面就让我们一起来学习吧~
在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生
OutOfMemoryError
(下文称OOM)异常的可能。
现在我们解决了第一个问题,除了程序计数器外,虚拟机内存的其他几个运行时区域都会产生OOM异常。那么我们就一起来学习当特定的区域出现了OOM的话应该如何排查和分析。
Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
比如下面这段测试的代码:
import java.util.ArrayList;
import java.util.List;
/**
* @Author zal
* @Date 2024/01/25 21:06
* @Description: VM option: -Xms20m -Xmx20m
* @Version: 1.0
*/
public class Main {
static class OOMObject { }
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
输出结果为:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3512)
at java.base/java.util.Arrays.copyOf(Arrays.java:3481)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:244)
at java.base/java.util.ArrayList.add(ArrayList.java:454)
at java.base/java.util.ArrayList.add(ArrayList.java:467)
at com.example.test.Main.main(Main.java:21)
Java堆内存的OutOfMemoryError
异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”
会跟随进一步提示“Java heap space”
。
如何排查和解决堆区OOM?
要解决堆区内存的异常,常规的处理方法首先是通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析。
关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:
StackOverflowError
异常。OutOfMemoryError
异常。我们知道这部分内存是线程私有的,每个线程都需要分配一块内存,所以当线程很多时就会发生内存溢出,下面来分析一下这句话背后的原理:
① 内存容量=堆内存+方法区内存+程序计数器内存(可忽略)+栈内存(虚拟机栈和本地方法栈);
② 因为栈容量在编译器就可知,且一旦分配在运行期就不会改变,在栈容量一定的情况下,每个虚拟机栈分配到的栈容量越大,可以创建的线程数就越少;
③ 当线程过多时,就会导致栈容量不足,从而发生内存溢出;
如何排查和解决栈中的OOM呢?
① 出现StackOverflowError
异常时,会有明确错误堆栈可供分析,相对而言比较容易定位到问题所在。
② 出现OutOfMemoryError
异常时:
由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。
方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。如果运行时产生大量的类去填满方法区,就能出现内存溢出异常。
一个类如果要被垃圾收集器回收,要达成的条件是比较苛刻的。在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。这里就涉及到如何动态产生大量类的方法,一般有如下两种:
除了以上两个直接产生大量类之外,还有如下的场景:
这里我们使用通过动态代理产生大量类来测试方法区溢出,测试代码如下:
/**
* @Author zal
* @Date 2024/01/25 21:06
* @Description: VM option: -XX:PermSize=10M -XX:MaxPermSize=10M
* @Version: 1.0
*/
public class Main {
static class OOMObject {
}
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
}
输出结果为:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
如何解决方法区内存溢出?
JDK8之后元空间替代了永久代,在默认设置下,正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常了。
不过还是可以通过一些参数的设置来预防OOM:
-XX:MaxMetaspaceSize
:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。-XX:MetaspaceSize
:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize
(如果设置了的话)的情况下,适当提高该值。-XX:MinMetaspaceFreeRatio
:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。-XX:Max-MetaspaceFreeRatio
,用于控制最大的元空间剩余容量的百分比。直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize
参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。
下面我们使用使用unsafe分配本机内存,来模拟OOM,测试代码如下:
/**
* @Author zal
* @Date 2024/01/25 21:06
* @Description: VM option: -Xmx20M -XX:MaxDirectMemorySize=10M
* @Version: 1.0
*/
public class Main {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
输出结果为:
Exception in thread "main" java.lang.OutOfMemoryError: Unable to allocate 1048576 bytes
at java.base/jdk.internal.misc.Unsafe.allocateMemory(Unsafe.java:632)
at jdk.unsupported/sun.misc.Unsafe.allocateMemory(Unsafe.java:462)
at com.example.test.Main.main(Main.java:24)
如何排查和解决直接内存溢出的OOM?
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump
文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory
(典型的间接使用就是NIO
),那就可以考虑重点检查一下直接内存方面的原因了。
直接内存溢出可通过调节参数-XX:MaxDirectMemorysize
调整内存区域,默认值和Java堆内存大小一样。