之前笔者在参加面试的时候,发现在八股拷打中最常见的一个问题就是:JVM的内存模型,而内存模型中涉及到的概念:虚拟机栈、本地方法栈、堆、方法区(元空间、永久代)、程序计数器等,以及垃圾回收器涉及到的新生代、老年代,这些错综复杂的八股,我看着都头大,于是便自己整理了JVM的内存模型,希望可以帮到大家。
JVM的内存模型是Java在程序运行时管理内存的规范,解决多线程环境下的可见性、有序性和原子性问题,所以我们在理解JVM时可以重点去关注:每部分内存是否线程公有、线程如何访问共享变量等线程执行角度相关的问题。
1.线程私有区:虚拟机栈、本地方法栈、程序计数器
虚拟机栈(也叫Java栈)和本地方法栈的作用和原理是一样的,只不过服务对象不同。虚拟机栈为虚拟机执行Java方法服务,本地方栈为虚拟机使用到的本地方法服务,一个线程的生命周期中,可能都在执行Java方法,操作虚拟机栈;也有可能在执行Java方法的过程中又调用了本地方法,所以在虚拟机栈和本地方法栈之间切换。
所谓本地方法,即Native方法,其实就是一种交流机制,当我们的Java应用需要与其他环境交互时,比如与底层操作系统或者硬件系统交换信息,本地方法这时就可以发挥作用,为我们提供一个接口。
虚拟机中对本地方法栈中的方法所使用的编程语言、数据结构等并没有规范,不同的虚拟机有不同的实现方法,例如Sun HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一了。
虚拟机栈是线程私有的,和线程同时创建,用于存储栈帧,每个方法被调用时都会创建一个新的栈帧压入栈,栈帧包括局部变量表、操作数栈、动态链接、返回地址等。方法执行完毕后,栈帧会被弹出。
- 局部变量表存放方法执行过程中使用的局部变量,包括方法参数和方法内部的局部变量。存储类型包括基本数据类型(int、long、double等)和对象的引用(引用类型变量,指向对象的内存地址,而不是对象本身)。
- 操作数栈用于存储方法执行过程中的操作数以及中间计算结果,例如,在进行
a + b操作时,a和b首先被推入操作数栈,执行完毕后结果也会被压入栈中。 - 动态链接负责在运行时解析方法和字段的引用,当JVM调用一个方法时,它会根据方法区中的方法和字段的实际位置来解析符号引用,在查找到相应方法的内存地址后,将其转换为直接引用。
- 返回地址用于保存方法调用完成后,程序需要返回的位置。每个方法调用时,栈帧会保存一个返回地址,它是调用方法后返回到的代码位置,通常是一个指向字节码指令的指针。
那么当一个Java线程调用Java方法和本地方法时从虚拟机栈到本地方法栈发生了什么样的切换?,如下图所示,当前线程依次调用了两个Java方法,第二个Java方法调用了本地方法,从而产生了本地方法栈。
本地方法栈也是线程私有的,和虚拟机栈有着相同的权限,可以通过本地方法接口来访问虚拟机内部的运行时数据区,也可以调用任何Java方法,所以在图中本地方法调用了新的Java方法,在虚拟机栈中产生了两个新的栈帧。
程序计数器也是线程私有的。在任何一个确定的时刻,一个处理器只会执行一条线程中的指令,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,并且生命周期和线程的生命周期保持一致,程序计数器用来存储指向下一条指令的地址,当线程切换时,由执行引擎读取当前线程的下一条指令,就知道从哪接着执行。
程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等功能都要依赖程序计数器完成。
程序计数器是整块内存中很小的一块空间,几乎可以忽略不计,也是运行速度最快的存储区域。
2.线程共享区:堆和方法区
堆和方法区都是线程共享区,多个线程可以同时访问堆中的对象或方法区中的类元数据,并且堆和方法区在JVM内存管理中都占据着相对较大的内存区域。
堆(Heap)是JVM中最大的一块内存区域,所有由我们创建的对象实例和数组(包括基本类型数组,如int[]、char[]和对象类型数组,如String[]、Object[])都存放在堆区。
对象的回收由垃圾回收器负责。JVM的堆分为新生代和老年代,而堆的垃圾回收机制通过分代回收(Minor GC、Major GC等)来优化性能。
(1)新生代默认占整个堆的1/3,包含Eden区和两个Survivor区(S0、S1),对象创建时,优先分配在Eden区。当Eden区满时,会触发垃圾回收(minor GC),将存活对象移动到Survivor区。
(2)老年代占2/3堆空间,存放生命周期较长的对象。当对象在Survivor区经历多次GC仍然存活(默认15次GC),会晋升到老年代。老年代Eden区满时,会触发垃圾回收(Major GC)进行老年代的内存清理,否则报OOM异常。
方法区是JVM运行时数据区的一部分,方法区的作用:(1)用来存放类的结构信息(类名、访问修饰符、字段、方法、接口等)(2)存放类的静态变量和JIT即时编译后的代码。(3)运行时常量池,存储编译期生成的常量和运行时动态生成的常量。
方法区只是JVM规范中定义的一个概念,用来存放(1)类信息(类名、访问修饰符、字段、方法、接口等)。(2)存放类的静态变量和JIT即时编译后的代码。(3)运行时常量池和类常量池,存储编译期生成的常量和运行时动态生成的常量。
在JDK1.6及之前的版本,字符串常量池存放在方法区中,在JDK1.7版本之后,字符串常量池被移到堆中。
那么方法区和永久代、元空间有什么关系呢?
永久代和元空间是方法区的两种实现,在JDK 1.7及之前的版本,HotSpot JVM实现了方法区,将其放在了永久代中,永久代大小固定,容易导致OutOfMemoryError: PermGen space。在JDK1.8及之后的版本,HotSpot JVM引入了元空间来替代永久代,元空间使用本地内存(而非JVM堆内存),提高了可扩展性。
3.从线程执行看JVM内存模型
首先,我们总结上文内容,JVM内存模型可以分为线程私有区和线程共享区两部分,如下图所示。
以下面的多线程程序为例,我们可以总结JVM内存模型是如何工作的。
-
虚拟机栈是线程私有的,存储方法调用时的栈帧,线程每调用一个方法,就在栈中创建一个新的栈帧。比如程序中的
main方法、MyObject类的构造方法、modifyValue方法等都对应着各自的栈帧,每个栈帧都有它自己的局部变量表和操作数栈。例如当thread1执行MyObject obj1 = new MyObject(localVar1);时,JVM为thread1创建一个新的栈帧。栈帧的局部变量表会包含:localVar1(值为10)obj1(指向MyObject对象的引用)
此时,
MyObject对象会在堆上分配内存,并且obj1变量存储的是指向这个对象的引用。 -
这段程序没有调用任何本地方法,所以也就没有显示地使用本地方法栈。
-
程序计数器也是线程私有的,在
thread1和thread2的执行过程中,程序计数器会随每个线程的字节码指令的执行而变化。每当线程从栈中获取方法时,程序计数器的值指向当前执行的方法的位置。 -
堆用来存储动态分配的对象,当
thread1和thread2分别创建MyObject实例时,这些对象会存储在堆内存中。 -
方法区主要存储类的结构信息、静态变量、常量池等。程序中,
JvmMemoryModelExample和MyObject这两个类的结构信息、静态变量(如staticVar)都存储在方法区(或元空间)中。
我们一定要注意在这个程序中
staticVar和``value`变量的区别,前者是静态变量,与类相关联,而不是与某个具体的对象实例相关,所以保存在负责存储类信息的方法区中。而后者是对象的成员变量,与对象实例相关,所以保存在堆中。
public class JvmMemoryModelExample {
private static int staticVar = 0; // 静态变量存储在方法区
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
// 创建线程1,执行一些局部变量和堆内存操作
int localVar1 = 10; // 局部变量存储在栈中
MyObject obj1 = new MyObject(localVar1); // 对象存储在堆中
obj1.modifyValue();
System.out.println("Thread1: " + obj1.getValue());
});
Thread thread2 = new Thread(() -> {
// 创建线程2,执行类似的操作
int localVar2 = 20;
MyObject obj2 = new MyObject(localVar2);
obj2.modifyValue();
System.out.println("Thread2: " + obj2.getValue());
});
thread1.start();
thread2.start();
}
}
class MyObject {
private int value; // 对象的成员变量存储在堆中
public MyObject(int value) {
this.value = value;
}
public void modifyValue() {
this.value += 5;
}
public int getValue() {
return value;
}
}
JVM的内存模型是Java开发者必须掌握的基础知识,尤其在面试中经常被问到。理解虚拟机栈、本地方法栈、方法区、程序计数器,以及堆的分代回收机制,不仅有助于优化Java应用的性能,也能在面试中脱颖而出。
如果您觉得这篇文章有帮助,欢迎点赞、分享、在看,更多高频八股解析,敬请关注!