JVM内存模型图
这里介绍的是JDK1.8的内存模型,与之前的版本相比其最大的不同就是使用元空间取代了永久代。元空间与永久代类似,它们的主要区别在于元空间并不在JVM中,而是使用了本地内存。
各区域介绍
模型图中三个蓝色区域为每个线程独有的,而橙色区域则是所有线程共享的,即每个线程都有属于自己的程序计数器、虚方法栈和本地方法栈,而堆区和方法区被所有线程共享。
1. 程序计数器
学过汇编语言的同学肯定不陌生,它保存着当前正在执行的指令的地址。
Question:为什么程序计数器是私有的?
要解答这个问题需要从程序计数器的作用来回答:
(1)字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制。
(2)在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程运行到哪了。
注意,如果当前线程执行的是native方法,则其值为null。
所以,程序计数器私有主要是为了线程切换后能够恢复到正确的执行位置。
2. 本地方法栈
Java底层很多都是由C++实现的,例如我们打开查看Thread类的源码最后就会发现start()方法中调用的start0()就是本地方法。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); // 此处调用了本地方法start0()
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// 本地方法start0
private native void start0();
因此,本地方法栈为虚拟机使用到的native方法服务。
3. 虚拟机栈
虚拟机栈也就是平时所说的栈内存,每个Java方法在被调用的时候都会创建一个栈帧(一个方法就对应着一个栈帧),并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就结束了。
栈帧由四部分组成,分别是局部变量表、操作数栈、动态链接与方法出口。
局部变量表 :存放着方法中的局部变量
操作数栈:用来操作方法中的数的一个临时栈
动态链接:把符号引用存在直接应用存在内存空间中
方法出口:记录该方法调用完毕应该回到的地方
4. 堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。在jdk1.8后,字符串从常量池从永久代(方法区)中分离出来,存放于堆中。
堆的结构如上图所示,这里不做过多讲解。
5. 方法区
方法区在jdk1.7时采用永久代的方式,到了1.8时改成了元数据区,使用本地内存,并且字符串常量池也移入到堆区进行管理。方法区存放着虚拟机加载的类信息、常量、静态变量以及即时编译器编译的方法代码等。
一个简单的Math运行过程
这里通过一个简单的例子来演示虚拟机栈的使用过程
public class JVM {
public static void main(String[] args) {
int a=1;
int b=2;
int c=a+b;
System.out.println(c);
}
}
上面的代码是一个非常简单的两数相加操作,那么这个过程在JVM内存中是如何进行的呢?
我们可以在target的classes目录下通过javap -c JVM.class这条命令获取字节码的反编译指令,得到的指令如下所示:
public class JVM {
public JVM();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_3
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: return
}
这里介绍几个常用的JVM指令集:
iconst_1:int型常量值1进栈
istore_1: 将栈顶int型数值存入第二个局部变量,从0开始计数
iload_1: 第二个int型局部变量进栈,从0开始计数
iadd: 栈顶两int型数值相加,并且结果进栈
注意这里的局部变量是从0开始计数的,因此istore_1和iload_1是第二个局部变量。 第一个局部变量为this,指向该对象。
执行步骤:
1. main线程开始运行,此时局部变量表和操作数栈均为空
2. 常量值1进入操作数栈:iconst_1(这里1指数字1)
3. 栈顶元素存入第二个局部变量:istore_1(这里1指第二个局部变量)
4. 重复上面两步后(iconst_2,istore_2)得到下面结果
5. 将第2、3个局部变量放入操作数栈(iload_1, iload_2)
注意局部变量是从0开始计数的,这里指第二第三个
6. 将操作数栈中最上面两个数取出相加后再存放入操作数栈中(iadd)
7. 最后将操作数栈中的结果存放到第四个局部变量中(istore_3)
不难发现,JVM指令其实已经很类似于汇编语言了,可以看出越接近底层代码量就会越多。