概述
本文主要讲述 JVM 内存模型, 解析各个运行时内存数据区域的作用和使用场景。
本文所提到的 JVM 模型都是基于 jdk-1.8 版本
JVM 内存模型
运行时数据区域
程序计数器(Program Conunter Regisiter)
程序计数器是一个比较小的内存空间,可以看作是当前线程执行的字节码行号指示器。本质就是记录字节码执行顺序。 在《Java 虚拟机规范》中没有任何 OutOfMemoryError 情况的区域。
虚拟机栈(JVM Stack)
虚拟机栈,存放的是线程运行时内部的局部变量,也可以理解为线程栈。
每个方法被执行的时候, 虚拟机会创建一个栈帧(Stack Frame)用于存放局部变量表(local variable),操作数栈(operand stack),动态连接,方法出口等信息。
栈帧(Stack Frame)随着方法的调用而创建,随着方法的结束而销毁(不论是正常结束还抛出异常)。
字节码指令分析(描述 JVM Stack 操作过程)
public int add() {
int a = 1;
int b = 2;
int c = b - a;
return c;
}
0 iconst_1 //将 a 压入局部变量表栈顶
1 istore_1 //对 a 进行赋值 1
2 iconst_2 //将 b 压入局部变量表栈顶
3 istore_2 //对 b 进行赋值 2
4 iload_2 //读取 b 到操作数栈
5 iload_1 //读取 a 到操作数栈
6 isub //执行 b - a
7 istore_3 //将 int 类型的值存入局部变量表 3
8 iload_3 //读取 c 到操作数栈
9 ireturn //返回
局部变量表(local variable)
局部变量表存放了各种编译期 Java 虚拟机基本数据库类型(boolean、byte、char、short、int、float、long、dubble)和对象引用(reference 类型,即对象的起始位置指针或者对象句柄), 对象的真实数据通常存放在堆空间。
局部变量表中的存储空间通过变量槽(slot) 来表示,其中 64 位长度的 long 和 double 占 2 个变量槽。
操作数栈(operand stack)
每个栈帧都包含一个操作数栈的先进先出(FIFO)栈,栈帧中操作数栈的深度由编译期决定,并且通过方法的 code 属性保存以及提供给栈帧使用。
动态链接
每个栈帧都包含一个指向当前方法所在类型的运行时常量池的引用。以便对当前方法的代码实现动态链接。
在 class 文件中,一个方法如果要调用其它方法, 或者访问局部成员变量,则需要将符号引用(synbolic reference)来表示,动态链接的作用就是将这些符号引用转换为对实际方法的直接引用。
方法出口
方法正常完成,当前栈帧恢复调用者的责任,包括恢复调用者的局部变量表,操作数栈,以及正确的程序计数器递增。跳过刚才执行的方法调用指令等,低哦啊用着的代码被调用的方法正返回值压入调用者操作数栈后,会继续正常执行。
方法一场完成,某些指令导致了 JVM 虚拟机抛出异常,或者用户显示的通过 thorw 关键字跑出一场,同时在改该方法中没有捕获异常。如果方法异常调用完成,那不一定有方法返回值返回给调用者。
本地方法栈(Native Method Stack)
为本地方法所分配的内存空间,就是为 native 关键字修饰的方法提供服务的。
本地方法主要是 Java 来调用 C/C++ 函数库的调用方法。
方法区(Method Area)
主要存放数据有:常量,静态变量,类信息。
方法区存放的是静态变量的内存地址, 方法区里面有一个元空间, 在JDK1.8 之前叫永久代。
堆(Heap)
JVM 管理的最大的一块内存空间。与堆相关的一个重要概念是垃圾收集器。几乎所有的垃圾收集器都是采用分代收集算法,所以对内存空间也是基于这一点进行相应的划分:新生代和老年代,新生代分为Eden 空间、From Survivor 空间、To Survivor 空间。
对象创建的过程中首先会存在 Eden 区,然后经过 minor gc 过后进入 survivor ,进过 15 次 survivor 转移过后,进入老年代。
如果内存都不够用了就触发 full gc, 再次触发 GC 过后无法分配申请内存,JVM 就会抛出 OOM。
分析对象是否存在引用,是否被回收采用的是 GC ROOT 可达性分析。
直接内存(Direact Memory)
直接内存,不是 JVM 来管理,是通过操作系统来管理的, 与 Java NIO 密切相关。 Java 通过DirectByteBuffer来操作直接内存。
JVM 内存参数设置
内存参数配置
Spring-Boot 程序的 JVM 内存参数设置格式(Tomcat 启动直接在 bin 目录下的 Catalina.sh 文件设置)
java -Xms2048m -Xmx2048m -Xmn1024 -Xss512k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -jar xxx-xxx.jar
关于元空间JVM 有两个:-XX:MetaspaceSize=N 和 -XX:MaxMetaspaceSize=N,对于 64 位 JVM 来说, 元空间默认是 21MB,默认的元空间的最大值是无限。
-XX:MaxMetaspaceSize: 设置元空间最大值,默认是 -1, 即不限制,或者说是受限制于本地内存大小。
-XX:MetaspaceSize:指定元空间的初始大小,以字节为单位,默认是 21M,达到该值过后就会触发 full gc 进行类型卸载,同时收集器会对该值进行调整;如果释放了大量的空间就适当降低该值;如果释放了很少的空间,那么就在不超过 -XX:MaxMetaspaceSize (如果设置)的情况下,适当提高该值。
由于调整元空间大小需要 full gc , 这是一个非常昂贵的操作,如果在启动过程中发生大量 full gc, 通常都是由于永久代或者元空间发生了大小调整,基于这种情况,一般建议在 JVM 参数将 MaxMetaspaceSize 和 MetaspaceSize 设置成一样的值,并设置得比初始值要大,对于 8G 的物理内存来说我们通常都会将这两个值设置为 256M。
堆空间内存溢出
import java.util.ArrayList;
import java.util.List;
public class HeapOverFlowTest {
byte[] a = new byte[1024 * 1024 * 2]; // 2mb
public static void main(String[] args) {
List<HeapOverFlowTest> list = new ArrayList<>();
while(true) {
list.add(new HeapOverFlowTest());
}
}
}
// 输出结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at cn.edu.cqvie.jvm.HeapOverFlowTest.<init>(HeapOverFlowTest.java:8)
at cn.edu.cqvie.jvm.HeapOverFlowTest.main(HeapOverFlowTest.java:13)
虚拟机栈内存溢出
public class StackOverFlowTest {
// JVM 设置
// -Xss128k, -Xss默认1M
static int count = 0;
static void redo() {
count++;
redo();
}
public static void main(String[] args) {
try {
redo();
System.out.println(count);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
// 输出结果: 栈溢出
java.lang.StackOverflowError
at cn.edu.cqvie.jvm.StackOverFlowTest.redo(StackOverFlowTest.java:11)
at cn.edu.cqvie.jvm.StackOverFlowTest.redo(StackOverFlowTest.java:11)
at cn.edu.cqvie.jvm.StackOverFlowTest.redo(StackOverFlowTest.java:11)
....
总结:
-Xss 设置越小 count 值越小,说明一个线程栈里能够分配的栈帧就越小,但是对于 JVM 整体来说能够开启的线程数就会更多。
方法区内存溢出
- 需要注意的是 1.8 内模型中,将运行时常量池数据放入堆中,所以我们限制方法区的大小对运行时常量池的限制毫无意义。最终也只会抛出
java.lang.OutOfMemoryError: Java heap space异常。 - 下面通过GCLib 模拟方法区溢出模拟的一个例子。
/**
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*/
public class MyTest4 {
public static void main(String[] args) {
for (; ; ) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyTest4.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) ->
proxy.invoke(obj, args1));
System.out.println("hello world");
enhancer.create();
}
}
}
//输出结果
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
......
JVM 监控工具
VisualVM
VisualVM 提供在 Java 虚拟机 (Java Virutal Machine, JVM) 上运行的 Java 应用程序的详细信息。在 VisualVM 的图形用户界面中,可以方便、快捷地查看多个 Java 应用程序的相关信息。
参考资料
- 《深入理解 Java 虚拟机》 第三版 周志明
- 《Java 虚拟机规范(Java SE 8 版)》 爱飞翔 周志明 等译
- visualvm 主页文档
- Oracle 官网 Java 虚拟机规范