03_JVM 中的那些内存区域
约 2487 字大约 8 分钟
2024-08-10
JVM 中的内存区域划分
JVM 在运行我们写好的代码时,他是需要使用多块内存空间的,不同的内存空间用来存放不同的数据,配合我们写好的代码流程,才能让我们的系统运行起来,让我们看看下面这张图

首先,一定有一块内存区域用来存放加载的类信息
其次,代码运行起来时,执行的方法中可能有很多变量之类的东西,也是需要存在某个内存区域里的
接着,如果我们写的代码里创建了一些对象,这些对象也是需要内存空间来存放的
为了我们写好的代码在运行过程中根据需要来使用,所以 JVM 中需要划分出来不同的内存区域
存放类的方法区
方法区是在 JDK1.8 以前的版本中,代表 JVM 中的一块区域,主要存放从 .class 文件中加载进来的类,还有一些类似常量池的东西
JDK1.8 之后,这块区域的名字改成了 Metaspace,可以认为是 元数据空间 的意思,主要还是存放我们写的各种类相关的信息,如下图所示

执行代码指令用的程序计数器
我们写的代码经过编译之后变成 .class 后缀的字节码文件,字节码是计算机可以理解的一种语言,看起来像下面这样
public OrderServiceImpl();
Code:
0: aload_0
1: invokespecial #1
4: aload_0
5: invokestatic #2
8: putfield #3
11: return像 0: aload_0 这样的就是字节码指令,对应着一条一条的机器指令,计算机读到这种机器码指令,才知道具体要做什么
当 JVM 加载类信息到内存之后,就会使用自己的字节码执行引擎去执行编译出来的字节码指令
在执行代码指令的时候,JVM 里就需要一个特殊的内存区域,那就是程序计数器
程序计数器就是用来记录当前执行的字节码指令的位置,也就是记录目前执行到了哪一条字节码指令
JVM 是支持多个线程的,因此每个线程都会有自己的程序计数器,用来记录当前这个线程执行到了哪一条代码指令了,如下图

Java 虚拟机栈
Java 代码在执行的时候,是线程来执行某个方法中的代码的
main 线程执行 main() 方法代码指令的时候,会通过 main 线程对应的程序计数器记录自己执行的指令位置
public class Order {
public static void main(String[] args) {
MqManager mqManager = new MqManager();
mqManager.loadMessageFromDisk();
}
}在方法中,我们经常会定义一些方法内的局部变量,比如在上面的 main() 方法中,就有一个 mqManager 局部变量,他是引用了一个 MqManager 实例对象
因此,JVM 必须有一块内存区域是用来保存每个方法内局部变量等数据的,这个区域就是 Java虚拟机栈
每个线程都有自己的 Java虚拟机栈,用来存放自己执行的方法的局部变量
如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧,栈帧里面就有这个方法的局部变量表、操作数栈、动态链接、方法出口等东西,在这里我们先不关注这些,我们先关注局部变量
比如 main 线程执行了 main() 方法,那么就会给这个 main() 方法创建一个栈帧,压入 main 线程的 Java虚拟机栈,同时在 main() 方法的栈帧里,会存放对应的 mqManager 局部变量,如下图所示
.png)
假设 main 线程继续执行 MqManager 对象里的方法,像下面这样,loadMessageFromDisk 方法里定义了一个局部变量 hasFinishedLoad
public class MqManager {
public void loadMessageFromDisk() {
Boolean hasFinishedLoad = false;
}
}那么 main 线程在执行上面的 loadMessageFromDisk 方法时,就会为 loadMessageFromDisk 方法创建一个栈帧压入线程自己的 Java虚拟机栈 里去,并且在这个栈帧的局部变量表里有 hasFinishedLoad 这个局部变量,如下图所示

接着如果 loadMessageFromDisk 方法调用了另外一个 isLocalDataCorrupt 方法,这个方法中也有自己的局部变量
public class MqManager {
public void loadMessageFromDisk() {
Boolean hasFinishedLoad = false;
isLocalDataCorrupt();
}
public Boolean isLocalDataCorrupt() {
Boolean isCorrupt = false;
return isCorrupt;
}
}这个时候会给 isLocalDataCorrupt 方法又创建一个栈帧,压入线程的 Java虚拟机栈 里,并且在这个栈帧的局部变量表里有 isCorrupt 这个局部变量,如下图所示

接着如果 isLocalDataCorrupt 方法执行完毕了,就会把 isLocalDataCorrupt 方法对应的栈帧从 Java虚拟机栈 里给出栈
然后如果 loadMessageFromDisk 方法也执行完毕了,就会把 loadMessageFromDisk 方法也从 Java虚拟机栈 里出栈
上述就是 JVM 中 Java虚拟机栈 这个组件的作用了:调用执行任何方法时,都会给方法创建栈帧然后入栈
在栈帧里存放这个方法对应的局部变量之类的数据,包括这个方法执行的其他相关信息,方法执行完毕之后就出栈
每个线程在执行代码时,除了程序计数器外,还搭配了一个 Java虚拟机栈 内存区域来存放每个方法中的局部变量表

Java 堆内存
我们在代码中创建的各种对象就是存放在 Java堆内存 中的,是 JVM 中一个非常关键的区域
public class Order {
public static void main(String[] args) {
MqManager mqManager = new MqManager();
}
}public class MqManager {
private String MqName;
}上面的 new MqManager() 这个代码是创建了一个 MqManager 类的对象实例,这个对象实例包含了一些数据,类似 MqManager 这样的对象实例,就是存放在 Java堆内存 中的
相应的,main() 方法对应的栈帧的局部变量表里,会让一个引用类型的 mqManager 局部变量来存放 MqManager 对象的地址,可以理解为局部变量表里的 mqManager 指向了 Java堆内存 里的 MqManager 对象

核心内存区域总结
为了有个更清晰的认识,我们来梳理一下整体的流程,整体代码和图如下
public class Order {
public static void main(String[] args) {
MqManager mqManager = new MqManager();
mqManager.loadMessageFromDisk();
}
}public class MqManager {
private String MqName;
public void loadMessageFromDisk() {
Boolean hasFinishedLoad = false;
isLocalDataCorrupt();
}
public Boolean isLocalDataCorrupt() {
Boolean isCorrupt = false;
return isCorrupt;
}
}
1、
JVM进程启动,加载Order类到内存中,然后有一个mian线程,执行Order中的main()方法2、
main线程关联一个程序计数器,记录代码指令执行的位置。并且main线程关联的Java虚拟机栈中会压入一个main()方法的栈帧3、接着执行到创建
MqManager类的实例对象,此时会加载MqManager类到内存中4、创建一个
MqManager类的实例对象分配在Java堆内存中,并且在main()方法的栈帧里的局部变量表中引入一个mqManager变量,让他引用MqManager对象在Java堆内存中的地址5、
main线程开始执行MqManager对象中的方法,依次把自己执行到的方法对应的栈帧压入自己的Java虚拟机栈6、执行完方法之后再把对应的栈帧从
Java虚拟机栈中出栈
其他内存区域
在 JDK 很多底层 API 里,比如 IO、NIO、网络Socket 相关的,他内部的源码很多地方不是 Java 代码,而是 native 方法去调用操作系统的一些方法,可能是调用的 C 语言写的方法或者一些底层类库
在调用这种 native 方法的时候,都会有线程对应的本地方法栈,与 Java虚拟机栈 类似,也是存放各种 native 方法局部变量表之类的信息
还有一个不属于 JVM 区域的,通过 NIO 中的 allocateDirect 这种 API,可以在 Java 堆外分配内存空间,然后通过 Java虚拟机 里的 DirectByteBuffer 来引用和操作堆外存空间。很多技术都会使用这种方式,一些场景下,堆外内存分配可以提升性能
思考环节
1、在 Java 堆内存中分配的那些对象,会占用多少内存呢?一般如何计算和估算系统创建的对象内存占用压力呢?
一个对象对内存空间的占用,大致分为两块:
1、对象自己本身的信息
2、对象的实力变量作为数据占用的空间
比如对象头,如果在 64 位的 linux 操作系统上,会占用 16 字节,如果实例对象内部有 int 类型的实例变量,会占用 4 个字节,如果是 long 类型的实例变量,会占用 8 个字节。如果是数组、Map 之类的,就会占用更多的内存了
JVM 对这块有很多优化的地方,比如补齐机制、指针压缩机制