JVM-如何理解进程内存模型(JVM内存模型-进程)

 

在软件开发的世界中,JVM(Java虚拟机)是Java语言的核心组成部分,它为Java程序提供了一个跨平台的运行环境。然而,要深入理解JVM,我们首先需要了解操作系统中的进程模型,毕竟JVM是虚拟机,而操作系统是真实机。JVM是操作系统上的一个特殊的进程,操作系统上有无数个进程。

操作系统中的进程内存模型

进程内存模型是指操作系统如何为运行中的程序分配和管理工作内存的方式。在Linux中,每个进程的虚拟内存空间被划分为多个段,每个段都有特定的用途和权限。



Linux进程内存模型的组成

  1. +------------------+ <-- 栈顶(高地址)
  2. | |
  3. | Stack |
  4. | |
  5. +------------------+ <-- 栈底(低地址)
  6. | |
  7. | Heap |
  8. | |
  9. +------------------+ <-- 堆基(低地址)
  10. | |
  11. | BSS |
  12. +------------------+
  13. | Data |
  14. +------------------+
  15. | Text |
  16. +------------------+ <-- 程序基(低地址)
  • 代码段(Text Segment):存储程序的机器代码,通常是只读的,以防止程序在运行时被修改,这个区域是共享的,当多个进程执行同一个程序时,代码段在内存中只有一份拷贝。
  • 数据段(Data Segment):存储程序中已初始化的全局变量和静态变量,数据段在每个进程中都有一份拷贝,因为每个进程可能有不同的数据值。
  • BSS段(Block Started by Symbol):存储程序中未初始化的全局变量和静态变量,它们在程序启动时被自动初始化为零,与数据段类似,BSS段也是私有的,每个进程有其独立的拷贝。
  • 堆(Heap):用于动态内存分配,如通过 malloc或 new申请的内存,堆的大小在程序运行时可以动态增长和缩小,由程序员控制。
  • 栈(Stack):用于存储局部变量、函数参数和返回地址,每个线程都有自己的栈,栈的大小通常有限制,并且是向下增长的。

为什么操作系统中的进程内存模型是这样的?

讨论操作系统的进程内存模型时,我们不得不提到目标文件格式,如ELF(Executable and Linkable Format)。ELF文件格式是Linux系统中常用的一种文件格式,它定义了程序的组织方式,包括程序的代码、数据和资源。ELF文件格式的设计直接影响了操作系统如何加载和执行程序,进而影响了进程模型的设计。

  • 左边是ELF的链接视图,可以理解为是目标代码文件的内容布局。
  • 右边是ELF的执行视图,可以理解为可执行文件的内容布局。

注意目标代码文件的内容是由section组成的,而可执行文件的内容是由segment组成的。

我们写汇编程序时,用 .text.bss.data这些指示,都指的是section,比如 .text,告诉汇编器后面的代码放入.text section中。

目标代码文件中的section和section header table中的条目是一一对应的。section的信息用于链接器对代码重定位。而文件载入内存执行时,是以segment组织的,每个segment对应ELF文件中program header table中的一个条目,用来建立可执行文件的进程映像。

比如我们通常说的,代码段、数据段是segment,目标代码中的section会被链接器组织到可执行文件的各个segment中。 .text section的内容会组装到代码段中, .data, .bss等节的内容会包含在数据段中。

链接文件的所有section头部信息

  1. There are 12 section headers, starting at offset 0x270:
  2. Section Headers:
  3. [Nr] Name Type Address Offset
  4. Size EntSize Flags Link Info Align
  5. [ 0] NULL 0000000000000000 00000000
  6. 0000000000000000 0000000000000000 0 0 0
  7. [ 1] .text PROGBITS 0000000000000000 00000040
  8. 0000000000000015 0000000000000000 AX 0 0 1
  9. [ 2] .rela.text RELA 0000000000000000 000001e0
  10. 0000000000000018 0000000000000018 I 9 1 8
  11. [ 3] .data PROGBITS 0000000000000000 00000055
  12. 0000000000000000 0000000000000000 WA 0 0 1
  13. [ 4] .bss NOBITS 0000000000000000 00000055
  14. 0000000000000000 0000000000000000 WA 0 0 1
  15. ... ... ... ...
  16. [11] .shstrtab STRTAB 0000000000000000 00000210
  17. 0000000000000059 0000000000000000 0 0 1
  18. Key to Flags:
  19. W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  20. L (link order), O (extra OS processing required), G (group), T (TLS),
  21. C (compressed), x (unknown), o (OS specific), E (exclude),
  22. l (large), p (processor specific)

JVM中的内存结构

JVM内存模型是JVM规范中定义的一组内存区域,包括堆(Heap Area)、方法区(Method Area)、程序计数器(Program Counter)、虚拟机栈(VM Stack Area )和本地方法栈(Native Method Stack Area)。这些内存区域共同构成了JVM运行Java程序的基础。

  1. +------------------+ <-- 栈顶(高地址)
  2. | |
  3. | VM Stack |
  4. | |
  5. +------------------+ <-- 栈底(低地址)
  6. | |
  7. | Native |
  8. | Method Stack |
  9. | |
  10. +------------------+
  11. | |
  12. | Heap |
  13. | |
  14. +------------------+ <-- 堆基(低地址)
  15. | |
  16. | Method |
  17. | Area |
  18. +------------------+
  19. | |
  20. | Program |
  21. | Counter |
  22. +------------------+ <-- 程序基(低地址)

  • 堆(Heap):存储对象实例和数组,new 对象时。
  • 方法区(Method Area):存储类信息、常量、静态变量等,类加载时。
  • 程序计数器(Program Counter):当前线程所执行的字节码的行号指示器。
  • 虚拟机栈(VM Stack):每个方法调用时,都会创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息,调用Java代码时。
  • 本地方法栈(Native Method Stack):用于支持本地方法的执行,调用C/C++代码时。

JVM 内存模型和进程内存模型对比

相同点

名称JVMOS-Process总结
Heap存储大的内存数据存储大的内存数据两者都用于存储大的内存数据
Stack存储局部变量表、操作栈、动态链接、方法出口等存储局部变量、函数参数和返回地址两者都用于存储局部变量、函数参数和返回地址
Code Area方法区(Method Area)代码段(Text Segment)两者都用于存储代码,如JVM的方法区和OS的代码段
Program CounterPC AreaOS-PC两者都用于指示当前执行的代码位置

不同点

  • Heap:JVM有垃圾回收机制,而操作系统进程需要手动管理内存释放。
  • Stack:JVM有两个栈,一个用于Java方法,一个用于本地方法;操作系统进程只有一个栈。
  • Code Area:JVM的方法区存储类信息、常量、静态变量等,而操作系统的代码段存储程序的机器代码。
  • Program Counter:JVM和操作系统进程都有程序计数器,但JVM的程序计数器是专门为JVM设计的。

总结

本文概述的介绍了操作系统进程内存模型和JVM内存模型,希望抽象出来一个进程内存模型。每个进程都有自己的内存空间,这个空间通常由 代码数据组成。未来如果存在 BVM,CVM,DVM ......,就可以快速的抓住本质,只需要了解它是如何设计的即可,无需再为新瓶装旧酒创造出来的概念感到惊慌抵触和恐惧。