JVM内存模型
废话不多说,给你上个图,想必你一定觉得眼熟吧
- 类装载子系统:将.class字节码文件加载到内存中
- 字节码执行引擎:JVM的核心组成之一,用于执行字节码里面的那些指令
- 程序计数器:主要存储的是当前线程所执行的字节码的行号,主要用作程序控制流的指示器、分支、循环、跳转、异常处理、线程恢复等基础功能
- 栈:用于执行Java方法
- 本地方法栈:用于本地方法
- 堆:用于存放对象实例
- 方法区:用于存放已被虚拟机加载的类型信息、常量、静态变量等数据
- 直接内存:这一块不属于JVM内存模型中的一块,是本机直接内存,受到本机总内存大小以及处理器寻址空间限制
其中,程序计数器、栈、本地方法栈都是线程私有的,堆和方法区是各线程共享的
栈
从上面我们知道,程序计数器、栈、本地方法栈是线程私有的,也就是说,每开一个线程,都会对应为这个线程开辟这几块内存空间。如图所示:
上面这张图主要为你展示程序计数器、栈、本地方法栈的线程私有特性,以及栈内部的接口
- 栈帧:在栈的内部,每个方法被执行时,都会创建一个栈帧,并压入栈,直接结束后则弹出栈,每个方法被调用到执行完毕的过程,就对应一个栈帧从入栈到出栈的过程
- 局部变量表:主要存储编译器可予置的各种Java基本数据类型和对象引用,存储的是对象的指针
- 操作数栈:栈结构实现,主要存储字节码执行过程中的操作数
- 动态链接:指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接
- 方法出口:用以恢复它的上层方法执行状态
下面我们通过一段代码,帮助你理解一下栈
package org.laugen.jvm;
public class Math {
public static int intData = 666;
public static final int constant = 520;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
当程序运行的时候,会经历以下一系列过程:
- 通过类装载子系统将.class文件加载的内存中
- 创建主线程的程序计数器、栈、本地方法栈
- 执行到
main()方法,创建main栈帧并压入栈 - 执行到
math.compute(),创建compute-栈帧并压入栈 - 执行完
compute()方法后,compute-栈帧弹出栈 - 执行完
main()方法,main-栈帧出栈 - 该线程结束,销毁线程并回收开辟的内存空间
下面,我们通过compute()方法,来看看一个方法在执行过程中,栈帧里面各个内存区域是如何变化的。首先,我们通过javap -c Math.class命令对这段代码反编译一下,然后阅读一下compute()方法反编译后的字节码指令
Compiled from "Math.java"
public class org.laugen.jvm.Math {
public static int intData;
public static final int constant;
public static org.laugen.jvm.User user;
public org.laugen.jvm.Math();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class org/laugen/jvm/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
static {};
Code:
0: sipush 666
3: putstatic #5 // Field intData:I
6: new #6 // class org/laugen/jvm/User
9: dup
10: invokespecial #7 // Method org/laugen/jvm/User."<init>":()V
13: putstatic #8 // Field user:Lorg/laugen/jvm/User;
16: return
}
这是反编译出来的结果,我们就看compute()方法这段代码
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
这里我们看到,一共会执行12条指令,我们现在一条一条阅读,可以自行上网下载一份JVM的指令手册
0: iconst_1:将int类型常量1压入操作数栈,此时操作数栈只有一个数值11: istore_1:将int类型值存入局部变量1,把数值1从操作数栈中弹出,此时完成int a = 1;这行代码,局部变量表中添加变量a并且它的值为12: iconst_2:将int类型常量2压入操作数栈,此时操作数栈只有一个数值23: istore_2:将int类型值存入局部变量1,把数值2从操作数栈中弹出,此时完成int b = 2;这行代码,局部变量表中添加变量b并且它的值为24: iload_1:从局部变量1中装载int类型值,这时变量a的值1会被压入操作数栈,此时操作数栈只有一个数值15: iload_2:从局部变量2中装载int类型值,这时变量b的值2会被压入操作数栈,此时操作数栈有两个数值,从栈底到栈顶分别是1,26: iadd:执行int类型的加法,将操作数栈里面的数值1和2弹出栈进行计算,得出结果为3,把3压入操作数栈,此时操作数栈只有一个数值37: bipush 10:将一个8位带符号整数压入操作数栈,此时操作数栈有两个数值,从栈底到栈顶分别是3,109: imul:执行int类型的乘法,将操作数栈里里面的数值3和10弹出栈进行计算,得出结果为30,把30压入操作数栈,此时操作数栈只有一个数值3010: istore_3:将int类型值存入局部变量3,把数值30从操作数栈中弹出,此时完成int c = (a + b) * 10;这行代码,局部变量表中添加变量c并且它的值为3011: iload_3:从局部变量3中装载int类型值,这时变量c的值30会被压入操作数栈,此时操作数栈只有一个数值3012: ireturn:从方法中返回int类型的数据,把数值30从操作数栈中弹出,作为方法的返回值返回
分析完上面的过程,你是否对局部变量表和操作数栈有了进一步了解呢?
补充两点:
- main-栈帧的局部变量表中会存math这个变量,它的值是math这个对象在堆上的指针
- user是一个静态变量,方法区中它的值是user这个对象在堆上的指针
堆
下面这个图是堆的内存模型,GC我会专门来讲,因此这里不过多涉及GC。
大概说一下上面这张图,JDK8中,JVM的堆主要分成两大块,分别是年轻代和老年代,他俩的占比一般情况下是年轻代:老年代=1:2,年轻代又分为Eden区和两个Survivor区,他们的占比大概是E:S1:S2=8:1:1。一般情况下,年轻代的对象通过15次GC后还存在,则会进入老年代。
那么,你知道为什么JVM的堆会有年轻代和老年代之分吗?