JVM内存模型详解
一、JVM内存结构概述
JVM在运行时将内存划分为多个区域,每个区域有不同的作用和生命周期。
二、程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
特点:
- 线程私有
- 唯一不会出现OOM的区域
- 记录字节码指令地址
三、虚拟机栈
虚拟机栈描述Java方法执行的内存模型,每个方法执行时会创建一个栈帧。
栈帧结构:
- 局部变量表:存储方法参数和局部变量
- 操作数栈:作为方法执行的工作区
- 动态链接:指向运行时常量池的方法引用
- 方法返回地址:方法正常或异常退出的地址
特点:
- 线程私有
- 生命周期与线程相同
- StackOverflowError:栈深度超出限制
- OutOfMemoryError:栈扩展时无法申请到内存
四、本地方法栈
本地方法栈为Native方法服务,与虚拟机栈类似。
特点:
- 线程私有
- 服务于本地方法
- 可能抛出StackOverflowError和OutOfMemoryError
五、堆
堆是JVM中最大的一块内存区域,所有线程共享,存放对象实例。
特点:
- 线程共享
- 垃圾收集的主要区域
- 可以处于物理上不连续的内存空间
堆的划分:
- 新生代:Eden区、Survivor区
- 老年代:长期存活的对象
内存分配策略:
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活对象进入老年代
六、方法区
方法区存储已被加载的类信息、常量、静态变量、即时编译后的代码等数据。
特点:
- 线程共享
- 逻辑上属于堆的一部分
- JDK8之后使用元空间实现,使用本地内存
运行时常量池:
- 方法区的一部分
- 存放编译期生成的字面量和符号引用
- 具有动态性,运行期间可加入新常量
七、直接内存
直接内存不是虚拟机运行时数据区的一部分,但也会被频繁使用。
特点:
- 使用Native函数库直接分配堆外内存
- 避免Java堆和Native堆之间复制数据
- 大小受限于物理内存
八、对象内存布局
对象在内存中的布局分为三部分:
对象头:
- Mark Word:存储对象运行时数据
- 类型指针:指向类元数据的指针
实例数据:
- 对象真正存储的有效信息
对齐填充:
- 保证对象大小是8字节的整数倍
九、对象的访问定位
主流的对象访问方式:
句柄访问:
- 堆中划分句柄池
- reference存储句柄地址
- 句柄包含对象实例数据和类型数据的地址
直接指针访问:
- reference存储对象地址
- 访问速度快
- 节省内存
十、内存溢出问题排查
常见OOM类型:
Java堆溢出:
- 原因:对象过多且无法回收
- 排查:使用jmap分析堆转储文件
虚拟机栈溢出:
- 原因:方法调用层次过深
- 排查:检查递归调用
方法区溢出:
- 原因:加载类过多
- 排查:检查动态代理和框架使用
本机直接内存溢出:
- 原因:DirectByteBuffer分配过多
- 排查:检查NIO使用情况
十一、总结
JVM内存模型划分:
- 程序计数器:记录执行位置
- 虚拟机栈:方法执行内存模型
- 本地方法栈:Native方法服务
- 堆:对象存储区域
- 方法区:类信息和常量存储
- 直接内存:堆外内存
理解内存模型是排查内存问题和优化性能的基础。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 夏天的风吹向哪里!
