一、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方法服务
  • 堆:对象存储区域
  • 方法区:类信息和常量存储
  • 直接内存:堆外内存

理解内存模型是排查内存问题和优化性能的基础。