JVM内存调用机制小结
一. JVM内存分配
从整体上来看,Java运行时内存空间可以分为两种,线程私有和线程共享
线程私有:
- 虚拟机栈
- 本地方法栈
- 程序计数器
线程共享:
- 堆
- 方法区(1.8之前叫永久代,1.8之后叫元空间)
- 直接内存(在Java运行时内存区域之外)
详细解释
1. 程序计数器: 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
⚠️ 注意 :程序计数器是唯一一个不会出现OutOfMemoryError
的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2.虚拟机栈: Java虚拟机栈是每个线程私有的,它的声明周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。线程每调用一次方法就会创建一个栈帧,并且把栈帧入栈,一旦方法调用完成,则对应这个方法的栈帧就会出栈。每个栈帧中都保存着方法的局部变量表、操作数栈、动态连接、方法返回地址。
局部变量表: 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指>针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
操作数栈: 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接: 主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
- 批注:
栈空间不是无限的,正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
除了 StackOverFlowError 错误之外,栈还可能会出现OutOfMemoryError错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
3. 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现StackOverFlowError 和 OutOfMemoryError两种错误。
4.堆: Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap) 。
5. 方法区: 方法区是《Java虚拟机规范》中一个规范,规定了方法区的概念和他的作用,具体的实现就是虚拟机要考虑的事情了,在Hotspot虚拟机中,具体的实现有永久代和元空间,JDK1.8之前的实现叫做永久代,1.8开始就改名为元空间了。不管叫什么名字,这块儿区域是线程共享的一块内存区域。方法区的作用是保存一个类的信息,当我们需要使用一个类的时候,需要先加载并解析这个类的Class文件,获取类的信息,其中类的信息包括字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
6. 运行时常量池: Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量和符号引用的 常量池表(Constant Pool Table) 。字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量,符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。常量池表会在类加载后存放到方法区的运行时常量池中。对于字符串JVM会专门开辟一块区域叫做字符串常量池,
二. 具体例子
类和对象的内存分配机制
- 栈:存放基本数据类型(局部变量)
- 堆:存放对象, 数组等
- 方法区: 常量池(常量, 比如字符串), 类加载信息
分析以下代码的内存变化
Person p = new Person();
p.name = "jack";
p.age = 10;
分析:
- 先加载
Person
类属性和方法信息(只会加载一次) - 在堆中分配空间, 进行初始化
- 将堆中的地址赋予给
p
, 此时p
指向这个对象 - 根据语句找到内存地址, 进行赋值
注意 栈中的p
永远只是对象的引用,这说明两个事情
- 真正存放对象的地方在堆中,不在栈中
2.如果在方法中使用p2 = p
那么p2
实际上也是这个对象的引用,改变p
的属性,p2
也会变
方法调用机制
- 程序运行到方法时, 会开辟一个独立的栈空间去执行方法内逻辑
- 执行完毕之后会回到调用方法的地方
- 方法执行完毕后栈空间会释放
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)