java虚拟机内存区域分析
Java JVM.1.内存区域与内存溢出异常
Java运行时数据区域

方法区 和 堆 是随虚拟机启动而启动的;
虚拟机栈、本地方法栈和程序计数器是线程私有的;
-
·程序计数器 (Program Counter Register)
是 当前线程 执行字节码的行号指示器,每一条线程都有一个独立的程序计数器。
- 程序计数器是程序控制流的指示器,线程的分支,循环,跳转等功能由它实现
- 执行Java方法,计数器记录的时虚拟机字节码指令的地址
- 若执行Native方法,则为 Undefined(空)
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
-
Java虚拟机栈 (Java Virtual Machine Stack)
虚拟机栈描述的是Java方法执行的线程内存模型,是线程私有的:
- 每个方法被执行时会同步创建一个帧栈 (Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法接口等信息。方法的调用和退出对应着帧栈在虚拟机栈中的入栈和出栈。
局部变量表:存放了编译期可知的基本数据类型、对象引用(reference类型)和ReturnAddress类型。
- 存储空间以局部变量槽 (slot) 为基本单位。
- 局部变量表所需要的 slot 的数量即内存空间是在编译期间确定并完成分配的。
虚拟机栈的两种异常状况:
- StackOverFlowError:线程请求的栈深度大于虚拟机所允许的深度。
- OutOfMemoryError:如果虚拟机栈容量可以动态扩展,但是栈扩展无法获取足够的内存。
-
本地方法栈 (Native Method Stacks)
和Java虚拟机栈类似,不过是为本地方法服务,也会存在StackOverFlowError和OutOfMemoryError;
(部分虚拟机会将 Java虚拟机栈 和 本地方法栈 合并)
-
Java堆 (Java heap)
是虚拟机所管理内存中最大的一块,在虚拟机启动时创建,为所有线程共享。
- 几乎所有的对象实例和数组都应当在堆上分配。
- 由于即时编译技术的进步和逃逸分析技术的强大,栈上分配和标量替换优化并不在堆上申请内存。
- 所以java对象实例也并不绝对在堆中申请内存
-
Java堆是垃圾收集器管理的内存区域,因此堆也被称为 Garbage Collected Heap。
-
线程共享的Java堆可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer, TLAB)提升对象分配时的效率
-
Java堆逻辑上是连续的,物理上可以存储在不连续的空间中。但对于大对象如对象数组,可能需要连续的内存空间
-
Java堆可以选择扩展或不扩展,如果堆空间被用尽会抛出 OutOfMemoryError。
-
方法区 (Method area)
方法区也为多个线程共共享,是堆的一个逻辑区域,被称为非堆 (Non-Heap)。
所有线程共享的内存区域
- 方法区中存储了已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK7之后,字符串常量池、静态变量等被移到了Java堆中。
JDK8中,抛弃了永久代的观念,永久代中的内容被本地内存中实现的元空间替代。
-
方法区可以选择大小是否可扩展,还可以选择是否实现垃圾回收机制。(因为方法很少改变,某只意义上是永久的)
-
方法区无法申请新内存时,会抛出 OutOfMemoryError。
-
运行时常量池 (Runtime Constant Pool)
是方法区的一部分。
Class文件包含类的版本、字段、方法、接口等信息外,还包含常量池表 (Constant Pool Table)
- 其用于存放编译器生成的各种字面量和符号引用,在类加载后被方法方法区的运行时常量区中。
运行时常量池是动态的,Class文件常量池是静态的,可以在运行期间将新的常量放入运行时常量池中。
new Scanner(System.in).next().intern()
比如string类的intern方法
常量池无法申请内存时会抛出 OutOfMemoryError。
-
直接内存 (Direct Memory)
不是运行时数据区的一部分,但是这部分区域被频繁使用,且会抛出OutOfMemoryError。
方法区 和 堆 是随虚拟机启动而启动的;
虚拟机栈、本地方法栈和程序计数器是线程私有的;
虚拟机对象创建的内存分配过程

1. 类的加载检查
- 在语言方面,创建对象的开始是 new 关键词
- 在虚拟机中,遇到new指令首先区检查指令参数能否找到常量池中的符号引用,并检查该类是否被加载
2. 为对象实例分配内存
- 为对象分配空间实际上等同于把一块确定大小的内存从java堆中划分出来。
1. 第一种方式是指针碰撞,该方法通过一个指针讲java内存分为使用中和空闲两边,要分配内存空间,实际上相当于把该指针向空闲区域移动相应需要的空间。该方法的前提是java内存要绝对规整,这需要该虚拟机采用的垃圾收集器带有空间压缩整理。
2. 第二种方式是空闲列表,该方法要求虚拟机维护一个表单,上面记录着哪块内存是空闲,哪块是使用中。分配内存时从表单上找到足够的内存并刷新表单
- 解决线程并发创建对象时的内存安全
- 一种是对内存空间进行同步处理:采用CAS配上失败重试方法保证更新操作的原子性
- 另一种是把分配内存的操作按线程划分在不同空间处理,即每个线程都预先分配一块堆的内存,称为本地线程分配缓冲区
- 将分配到的内存初始化为零值
- 保证了实例对象的字段不需要赋初始值就可以直接使用,是程序能访问到这些数据相应类型的零值
- 完成以上工作后,java对象已经产生,但是还没有执行构造函数。
对象的内存布局
- 对象在堆中的存储布局可以分为三个部分,对象头,实例数据,对齐填充
- 对象头分为两部分,对象自身的运行时数据和类型指针
1. 自身运行时数据本身于对象自身定义的数据无关,也被称为“Mark Word”,包括哈希码,GC,锁状态标志,线程持有的锁等
锁状态 | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁 | 对象的HashCode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程ID | Epoch | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
2. 类型指针既是对象指针指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个了类的实例
如果是数组,对象头中还有一块用于记录长度的数据。
- 实例数据
1. 实例数据存储部分存储对象真正的有效信息
2. 各类类型的字段存储顺序会受到虚拟机分配策略的影响
- 对齐填充
为了使任何对象为8的整数倍而存在,仅仅起到占位符的作用,任何实例的数据不是8字节的整数倍,就要通过对齐填充来补全
对象的访问定位
主流的访问方式有句柄和直接指针两种。
- 句柄访问:在Java堆中划分出一块内存作为句柄池,引用存储了句柄的地址,而句柄又存包含了实例数据 (Java堆)和类型数据 (方法区,元空间) 的地址信息。
- 优势:移动对象时,只需要修改句柄的实例数据指针,不需要修改对象本身。
- 直接指针访问:引用中存储的是对象地址,可以直接访问到对象本身。而对象中又有一个指向对象类型数据的指针。
- 优势:速度快,HotSpot主要采用直接制造访问。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)