Java学习篇(二)—— C++和Java的区别之程序内存分布
上一篇介绍了C++和Java编译的区别和Java独有的网络编程,线程管理。这一篇主要介绍一下两者在程序运行时的内存空间。
内存分布
| 项目 | C++ 程序 | Java 程序(使用 JVM) |
|---|---|---|
| 编译结果 | 直接生成机器码(如 .exe) |
编译成 .class 字节码 |
| 执行方式 | 操作系统直接加载执行 | 需要 JVM 加载 .class 文件 |
| 启动时发生的事 | 操作系统创建进程并加载程序 | 操作系统创建 JVM 进程,JVM 加载类 |
| 所属进程 | 你的 C++ 程序本身就是进程 | Java 程序是运行在 JVM 进程中的一段代码 |
| 内存分配方式 | 程序直接使用虚拟内存 | JVM 申请虚拟内存再分出堆、栈等区域 |
C++的程序启动过程,是操作系统创建一个用户进程,运行C++程序。一个程序就是一个新的用户进程,有自己独立的内存空间(虚拟)。
Java的程序启动过程,与C++相比较,只是多了JVM的启动过程。Java程序也是一个进程,但是JVM的“客人”,必须在 JVM 虚拟机中运行。
由此可以判断出,为什么Java的线程管理这么方便,因为它的线程管理不依赖于操作系统,自由度也就更高。
C++程序内存分布

+-------------------+ 高地址
| 栈区(Stack) | 局部变量、函数调用帧
+-------------------+
| 堆区(Heap) | new / malloc 分配的内存
+-------------------+
| 全局区(静态区)| static 和全局变量
+-------------------+
| 代码段(Text) | 程序指令
+-------------------+ 低地址
程序能够使用的最大内存为4GB,这个大小是PC指针能够寻址的最大范围,也是你的总线的位数。高位地址是内核空间,用来执行系统调用。在C/C++中内存分为5个区,分别为栈区、堆区、全局/静态存储区、常量存储区、代码区。
- 静态内存分配:编译时分配。包括:全局、静态全局、静态局部三种变量。
- 动态内存分配:运行时分配。包括:栈(stack): 局部变量。堆(heap): c语言中用到的变量被动态的分配在内存中。(malloc或calloc、realloc、free函数)
在 C++ 中,你创建的对象是放在「堆区」还是「栈区」,取决于你如何创建它。普通变量或自动对象创建在栈区,而new和malloc的对象则创建在堆区。
Java程序内存分布
Java 的内存分布由JVM管理,而C++的内存分布由操作系统直接管理。
Java的JVM是在操作系统虚拟内存之上的一个“虚拟机”,它向操作系统申请一块内存,然后自己划分成方法区、堆、栈等区域进行管理。Java虚拟机先运行,Java程序被“加载进去”。
+-------------------------+ 高地址
| 元空间 (Metaspace) | 存类信息、静态变量、常量池(JDK8之后)
| 方法区(永久代,已废弃)| 存储类结构、方法数据等(JDK7及以前)
+-------------------------+
| Java 堆(Heap) | 对象实例分配的内存,大部分对象都在这里
+-------------------------+
| 虚拟机栈(每线程一个) | 方法调用、局部变量表、操作数栈等
+-------------------------+
| 程序计数器 | 当前执行字节码的行号指示器
+-------------------------+
| 本地方法栈 | 用于执行 native 方法
+-------------------------+ 低地址

上图可知,Java的内存区包括两部分:线程私有部分———虚拟机栈、本地方法栈、程序计数器,公共部分——堆、本地内存。
Java方法和Native方法
Java 方法是使用 Java 语言编写的方法,最终被编译成字节码(.class 文件中的Code属性),由 JVM 直接解释或编译执行。Native 方法是使用非 Java 语言(通常是 C/C++)编写的方法,并通过 JNI(Java Native Interface) 在 JVM 中调用。
私有部分
-
程序计数器:这个和CPU的PC指针是一样的,CPU的PC指针指向当前运行指令的物理内存地址,后续取指令,解析指令,运行指令。线程的程序计数器记录的是正在执行的字节码指令地址,这是一个增量,基于当前正在执行的方法的字节码数组上增加。
-
虚拟机栈:每一个线程在执行代码时,都需要栈区来记录局部变量、返回地址等信息,Java方法调用都是通过虚拟机栈来实现的,栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈(栈顶存放有当前方法调用之后的返回值)、动态链接(调用当前方法所需的引用)、方法返回地址(方法返回后继续执行的位置,也就是调用方法时当前PC的值)。所以对于线程而言,若要调用一个方法,然后返回至原来的位置,流程是:
-
PC指针保存在当前栈帧的方法返回地址,创建新的栈帧,将参数保存在局部变量表,动态链接区对符号引用进行解析,将新的栈帧压栈。
-
PC指针指向新的方法,开始执行字节码,将产生的变量和操作数保存在局部变量表和操作数栈。
-
执行完方法后,将返回值保存在操作数栈栈顶。将栈帧弹出,PC指针恢复为新的栈顶的方法返回地址,从弹出栈帧的操作数栈栈顶取执行结果。
-
-
本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
每个线程所执行的“代码”(即类的字节码)在方法区或堆中共享,自己的“变量”在JVM 栈(局部变量表)中,当前线程正在执行的字节码位置保存在程序计数器中。当线程需要修改共享区域时,使用锁。
共有部分
| 比较项 | 堆(Heap) | 方法区(Method Area) |
|---|---|---|
| 存放内容 | 实例对象 | 类的元数据、方法信息、静态变量、常量池等 |
| 作用 | 运行时对象分配 | 加载类结构信息、JIT 编译缓存、静态数据 |
| 生命周期 | 随 JVM 存在 | 随 JVM 存在 |
| 垃圾回收 | 对象回收 | 类卸载时才回收(较少发生) |
上表用来比较堆和方法区的区别,也就是说方法区存在的只是方法,真正的实例化对象在堆,一个方法可以有很多个实例化对象。
- 堆:此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。GC堆通过分代垃圾收集算法,控制对象年龄,更好地回收内存,或者更快地分配内存。

-
字符串常量池:是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。JDK 1.7以后从永久代移动到了堆中,来实现高效的内存回收。
-
方法区:方法区属于是当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
-
运行时常量池:字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量。常见的符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。总的来说:编译器能确定的字面量和常量,符号引用。
符号引用(Symbolic Reference)是 Java 字节码中用于表示类、字段、方法等的一种 “符号形式” 的间接引用,在类未加载或链接前,它还不是一个内存地址或直接指针,而是“名字”+“描述符”等信息的组合。
- 直接内存:通过 JNI 的方式在本地内存上分配的。

浙公网安备 33010602011771号