本文会对JVM运行时数据区(java内存区域)进行详细的介绍,并对介绍中提到的OOM异常进行实战演示。
运行编译器: Intellij IDEA
虚拟机类型: HotSpot
操作系统: win11
JDK: 1.8
理论介绍
1. 程序计数器
- 作用: 是当前线程所执行的字节码的行号指示器,就是标志着当前线程执行到哪了。
- 是否线程私有: 是。
- 是否会发生OOM: 不会,因为其需要的空间很小,是JVM运行时数据区中唯一一块JVM规范中没有规定OOM情况的区域。
2. Java 虚拟机栈
- 作用: 描述了java方法执行的内存模型,每执行一个java方法都会在java虚拟机栈创建一个栈帧,其中存有局部变量表、操作数栈、动态链接、方法出口等信息。随着方法的执行对应的进行出栈入栈操作。
- 是否线程私有: 是。
- 是否会发生OOM: 是。
3. 本地方法栈
- 作用: 与java虚拟机栈及其类似,过执行的并非java字节码,而是其他语言。
- 是否线程私有: 是。
- 是否会发生OOM: 是。
4. Java 堆
- 作用: java对象进行分配的地方,GC垃圾回收算法主要工作的地方。
- 是否线程私有: 否。
- 是否会发生OOM: 是。
5. 方法区(元数据区MetaSpace)
- 作用: 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。也会有GC的工作,主要进行针对常量池的回收和对类型的卸载。注意,JDK1.8之后,用MetaSpace取代了方法区的概念,它不在属于JVM的运行时数据区,受限于物理内存。
- 是否线程私有: 否
- 是否会发生OOM: 是(受限于物理内存)
6. 字符串常量池
- 作用: 它在JDK1.8之前是方法区的一部分,最大的特征就是程序运行期间,可以将新的常量放入其中。例如,String类的intern()方法。但JDK1.8之后就存储在JAVA堆上。
- 是否线程私有: 否
- 是否会发生OOM: 是(受JAVA堆的大小限制)
7. 直接内存
- 作用: 不属于JVM 运行时数据区域的一部分,java程序会频繁进行访问。
- 是否线程私有: 否
- 是否会发生OOM: 是(受限于本机内存)
实战演示
1. Java 堆 溢出
JVM参数设置(注意参数之间有空格):
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
设置位置:
测试代码:
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
// 建立静态内部类
static class OOMObject{}
public static void main(String[]args) {
List<OOMObject> list=new ArrayList<OOMObject>();
// 不断的创建对象保存
while(true){
list.add(new OOMObject());
}
}
}
结果展示
结果分析
我们在JVM参数设置上指定了-XX:+HeapDumpOnOutOfMemoryError 参数,此参数的作用在于JVM会储存生成发生异常时的java内存堆的快照。我们可以看到结果中打印了输出文件的log。
并且在我们的项目目录下也发现了这个文件:
之所以要保存下内存堆的快照是因为在工程中,我们的OOM异常并不会像今天我们写的这样明显,我们需要根据快照发现内存泄漏的位置或是OOM的具体原因。
2. 虚拟机栈和本地方法栈溢出
JVM参数设置(注意参数之间有空格):
-Xss128k
设置位置:同上
测试代码:
public class JavaVMStackSOF{
private int stackLength=1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[]args) throws Throwable{
JavaVMStackSOF oom=new JavaVMStackSOF();
try{
oom.stackLeak();
} catch(Throwable e) {
System.out.println("stack length:"+oom.stackLength);
throw e;
}
}
}
结果展示:
结果分析:
我们发现程序出现的异常并非是OOM,而是StackOverflowError异常。StackOverflowError代表的是我们栈的调用过深,栈的深度和所占内存也是正相关的。但是在单线程情况下,不管是栈过深还是JVM栈空间不够,都会报StackOverflowError异常。
而如果真的想测试出JAVA虚拟机栈的OOM,就要用多线程的方式进行,随着不断的创建线程内存就会不断被切分出去,为每个线程的运行的虚拟机栈分配空间,最终随着每个线程的无休止递归调用,就会发生JVM栈的OOM异常。测试代码如下(谨慎运行可能导致系统假死):
public class JavaVMStackOOM{
private void dontStop(){
while(true){}
}
public void stackLeakByThread(){
while(true){
Thread thread=new Thread(() ->{dontStop();});
thread.start();
}
}
public static void main(String[]args)throws Throwable{
JavaVMStackOOM oom=new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
3. 字符串常量池溢出
JVM参数设置(注意参数之间有空格):
-Xms5m -Xmx5m
设置位置:同上
测试代码:
import java.util.ArrayList;
import java.util.List;
public class RuntimeConstantPoolOOM{
public static void main(String[]args) {
//使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list=new ArrayList<String>();
int i=0;
while(true) {
System.out.println(i);
list.add(String.valueOf(i++).intern());
}
}
}
结果展示:
结果分析:
上面我们讲了,字符串常量池存储在堆中,因此我们这里报了一个堆的OOM异常。
4. 本机直接内存溢出
JVM参数设置(注意参数之间有空格):
-Xmx20M -XX:MaxDirectMemorySize=10M
设置位置:同上
测试代码:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class DirectMemoryOOM {
private static final int _1MB = 1024*1024;
public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe=(Unsafe)unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1MB);
}
}
}
结果展示:
结果分析:
我们用Unsafe类进行了内存的申请分配。
总结:
我们已经和这些异常混了个脸熟,我们当然希望在工作中快速的分析出导致这些异常发生的根源。希望随着文章的更新和大家一起达到这个目的,下一章讲讲JVM为了不出现这些问题都做了哪些努力。大家加油啊!
参考
《深入理解Java虚拟机》,并针对JDK1.8之后版本做出了改进。
预告
接下来讲JVM的垃圾回收机制哦~
Java虚拟机(JVM)的垃圾收集机制(GC)详解(上)
Java虚拟机(JVM)垃圾回收机制(GC)详解(下)——垃圾收集器