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
局部变量,如下图所示
假设 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
对这块有很多优化的地方,比如补齐机制、指针压缩机制