我们在谈及JVM内存的堆、虚拟机栈和本地方法栈、程序计数器和方法区等名词的时候,有没有想过一个问题。JVM是一个进程,那么天真的以为就该和操作系统进程内存模型结构保持一致,比如C/C++程序就是和操作系统的进程的内存模型保持一致。但是JVM内存管理中内存的划分明显和操作系统的进程内存模型有很大出入,那么他们之间的关系究竟是怎样的呢? 这一篇专题来解读这个问题。
一 操作系统进程的内存模型
1.1 进程的地址空间
进程的地址空间,也叫作虚拟地址空间或者逻辑地址空间,指的是这个进程中所有虚拟地址的集合,此时还没有加载到物理内存中。地址空间大小取决于操作系统的位数,即CPU处理数据的能力,如果是32位操作系统,那么地址空间的寻址范围就是0 ~ 2^32-1。
物理内存资源是有限的,不可能将虚拟地址空间全部放到物理内存中。如果需要将虚拟地址空间的虚拟地址加载到物理内存,需要使用页表,然后按需调页。虚拟地址空间和物理内存将内存划分为大小相等的块,一般而言,一个内存块就叫做一个页,默认4096字节大小。 所谓的页表其实就是进程维护的一个记录虚拟页和物页映射的一张表。程序在运行过程中,首先根据页表判断这个地址所在的虚拟页是否有物理页,如果有直接读取对应的地址上的数据;如果没有则需要找一块页来存储虚拟页,然后构建映射关系。
1.2 进程的内存模型
1.2.1 用户地址空间和内核地址空间
对于计算机的内存,内存除了要存储进程需要的数据,还需要存储操作系统内核的数据,所以内存分为两个部分,用户空间和内核空间。对于32位操作系统4G内存来讲,用户空间占用0x000000000 ~ 0xC00000000 3GB内存,内核空间占用0xC00000000 ~ 0xFFFFFFFF 1GB内存。其中用户空间处于低位,内核空间处于高位。
1.2.2 内存模型
用户空间内存主要分为代码段(.text)、数据段(.data)、bss段(.bss)、内存映射区、堆、栈,如图所示:
1.2.2.1 代码段
代码段: 这块区域主要存放的是当前进程的源代码编译后的机器指令或者一些常量,这部分区域在程序执行之前就已经确定,并且这块区域是只读的,防止被修改。
1.2.2.2 数据段
数据段:这块区域主要是用来存放已经初始化的全局变量和静态变量,其中静态变量不区分是在函数内还是外,只要初始化了,都会放在数据段
1.2.2.3 BSS段
BSS段:这块区域主要是用来存放未初始化的全局变量和静态变量,其中静态变量不区分是在函数内还是外,只要没有初始化,都会放在BSS段
1.2.2.4 内存映射区域
当进行内存映射的时候,会在虚拟地址空间寻找一段连续的空间和文件进行映射,这一段连续的空间就是内存映射区域
1.2.2.5 栈
栈: 这块区域存放的是函数调用过程中的参数和局部变量和函数的返回值,由操作系统管理,进行自动负分配和回收。每一个函数执行的啥时候会在栈上占据一定的空间,这部分空间叫做栈帧,这个栈帧中存储的就是形参、本地变量和返回值等。注意,不管本地变量是不是初始化,都是需要放在栈上的。
1.2.2.6 堆
堆: 这块区域主要是用来存放通过调用malloc或者new动态分配的内存,存储的都是一些生命周期不限于某个函数的变量等。这块区域一般由程序员分配和释放。如果没有释放,就属于内存泄露,那么就只有程序退出的时候才会释放,比如内存不够用操作系统可能会kill掉一些吃内存比较多的进程。
1.3 C语言内存分配的例子
int a = 0; // 初始化的全局变量, 保存在数据段
char *t1; //未初始化的全局变量,保存在BSS段
long c; //未初始化的全局变量,保存在BSS段
static int d = 10; // 初始化的静态变量, 保存在数据段
static int m; // 未初始化的静态变量, 保存在BSS段
int main()
{
int b; //未初始化的局部变量, 保存在栈上
char elements[] = "123456"; // "123456"为字符串常量保存在代码段,数组保存在栈上,并将代码区的常量"123456"复制到该数组中。
char *t2; //t2保存在栈上
char *t3 = "aaa";//t3保存在栈上,"aaa"是常量保存在代码区
static int c = 0;//初始化的静态局部变量,保存在数据段
t1 = (char *)malloc(sizeof(char) * 16);// 在堆上分配16字节
t2 = (char *)malloc(sizeof(char) * 20);// 在堆上分配20字节
return 0;
}
二 JVM是什么? JVM是由什么组成的?
2.1 什么是JVM?
Java虚拟机是一个通过模拟计算机的功能,从而实现用于解释和运行Java程序的C++程序。所以本质上JVM就是一个程序。
我们知道Java是一款跨平台的编程语言,那么它是如何实现跨平台的呢?就是通过JVM来实现的。一般的高级语言在不同的平台下运行,需要编译成不同的目标代码,但是Java语言通过编译,生成可以被JVM解释执行的字节码文件,就可以在多个平台上直接运行。实现了一次编译,到处运行。
2.2 JVM主要由哪几部分组成
JVM主要包括执行引擎、类加载器、以及运行时数据区三大部分。
2.2.1 类加载器(ClassLoader)
类加载器主要负责将编译后的字节码文件加载到JVM中的内存里,然后会被执行引擎解释执行。类加载器主要包括三种类型:
第一:Bootstrap ClassLoader
主要负责加载核心类库,%JAVA_HOME%\lib\jre下面除了ext包之外的所有的库
第二:Extension ClassLoader
它主要负责加载核心库的扩展包,%JAVA_HOME%\lib\jre\ext下的类库
第三:Application ClassLoader
主要加载用户类路径(classpath)下指定的类库
2.2.2 执行引擎(Execution Engine)
执行引擎主要包括解释字节码、即时编译器(JIT compiler)。
2.2.2.1 Java解释器
类加载器会将字节码文件(class文件)加载到JVM内存中,但是class文件此时还是不能执行的,因为机器不认识字节码,只认识机器码,所以Java解释器就是将字节码文件一行一行的翻译,将字节码文件中指令转换成机器指令,这样机器才可以执行。高级语言一般都会有解释器,比如python也有。
2.2.2.2 JIT编译器
Java解释器一行一行的解释字节码,速度不快,JIT编译器会把多次调用的方法和多次执行的循环体的字节码编译成本地节机器码并运行。
2.2.3 运行时数据区
JVM运行时数据区域主要包括线程共享区和线程独占区。线程共享主要包括方法区、堆;线程独占去主要包括虚拟机栈、本地方法栈和程序计数器。
三JVM和操作系统中进程的内存模型有啥关系? JVM是如何划分内存的?
3.1 JVM和操作系统中进程的内存模型有啥关系
我们知道,JVM对于操作系统来说,就是一个程序,JVM本身也是基于C++来写的,所以JVM本身的内存结构就是C/C++的内存模型,如图所示:
但是Java程序中要使用的堆栈却和这个不同,因为Java程序栈和堆都是在JVM堆上分配的。
第一:为什么不Java的堆栈不在JVM的栈上分配呢?
因为JVM的栈是受操作系统自动管理的,随时可能被回收,那么有些对象生命周期还没有结束就被回收了,肯定不行的。
第二:为什么不直接使用JVM堆呢?非要在JVM堆的基础上重新实现堆呢?
因为C++的堆内存是程序员分配和释放的,不是操作系统自动管理,如果要实现JVM的GC的功能,那么直接使用JVM原生堆是不可能实现GC的
3.2 JVM是如何划分内存的
我们知道JVM是虚拟的计算机,那么它在内存布局是模仿了操作系统的一些功能。
第一:操作系统进程的内存模型是有堆和栈的,那么JVM中也是有的
第二:操作系统执行进程需要从磁盘加载目标文件到内存,那么JVM也需要一个地方存放加载的字节码信息等,所有JVM中有了方法区
第三:操作系统进程控制块有程序计数器,用于保存下一条需要执行的指令的地址,那么JVM也是有的,用于保存下一条需要执行的JVM指令的地址
如图所示:
3.2.1 程序计数器
程序计数器,和进程控制块的程序计数器功能一样,都是用于指示下一条需要执行的指令的地址,每一个线程对应一个程序计数器,属于线程私有。如果当前线程正在执行一个Java方法,则程序计数器记录正在执行的字节码地址;如果当前线程正在执行本地方法,则寄存器为空。因为这不属于JVM的范畴。
3.2.2 虚拟机栈
虚拟机栈类似于进程的内存模型的栈,用于存放方法调用过程中的参数、局部变量和返回值,每一个方法对应一个栈帧,方法结束栈帧销毁
3.2.3 本地方法栈
在Java程序中,有时候可能需要调用底层非Java的本地代码,所以需要一个执行本地方法的栈来存放调用过程中的参数、局部变量和返回值等信息
3.2.4 堆
堆也是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,主要存放对象实例。JDK1.7字符串常量池从方法区移到了堆中,JDK1.8将静态变量和静态方法移到了堆中。
3.2.5 方法区
方法区主要存储加载的字节码元数据信息,JDK1.8之后使用元数据空间代替了方法区,不再使用JVM内存,而是使用直接内存。方法区中字符串常量池移到了堆中;类变量(静态变量)也移到了堆中;符号引用移到了直接内存当中。