跳至主要內容

JVM 运行时数据区

JavaJVM

程序计数器

Program Counter Register,是当前线程所执行的字节码的行号指示器,字节码解释器通过改变该寄存器的值,来定位下一条将要执行的字节码指令。

特点:

  • 线程私有,各线程之间的计数器互不影响,独立存储
  • 不会产生OutOfMemoryError

虚拟机栈

Java Virtual Machine Stacks,虚拟机栈描述的是线程中方法的内存模型,每执行一个方法,虚拟机栈中会同步创建一个栈帧,其中包括局部变量表操作数栈动态连接方法返回地址和一些额外的附加信息

一个方法的执行到结束,对应着一个栈帧在虚拟机中从入栈到出栈的过程,而栈顶即活动栈帧,对应着当前正在执行的方法。

特点:

  • 线程私有,随线程生灭
  • 栈帧过多可能导致StackOverflowError,栈空间不足可能导致OutOfMemoryError
  • -Xss size 指定线程的最大栈空间

局部变量表

存储方法里的基本数据类型以及对象的引用。

  • 容量以变量槽Slot为最小单位
    • 除了long double需要2个Slot外,其余数据类型都需要1个Slot
    • Slot根据变量作用范围可复用
  • JVM通过索引定位的方法使用局部变量表,范围从0开始至局部变量表最大的变量槽数量
  • JVM实现通过引用应完成两件事:
    • 根据引用直接或间接找到对象在Java堆种数据存放的起始地址或索引
    • 根据引用直接或间接找到对象所属数据类型在方法区中存储的类型信息
  • 类字段有两次赋初始值的过程,一次是准备阶段赋系统初始值,另一次是初始化阶段赋程序定义初始值。但局部变量没有初始化就不能使用

动态链接

指向运行时常量池的方法引用,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,以便支持调用过程中的动态连接。

方法返回地址

方法正常退出或异常退出的地址,有两种退出方法执行的方式:

  • 正常调用完成,正常向主调函数提供返回值
  • 异常调用完成,不会提供任何返回值

操作数栈

  • 32位数据栈容量为1,64位栈容量为2
  • 优化处理:两个不同的栈帧会出现一部分重叠,节约空间,且可以共享一部分数据

附加信息

  • JVM规范没有描述的信息,如调试、性能收集相关信息
  • 一般把动态连接、方法返回地址以及其它附加信息全部归为栈帧信息

本地方法栈

类似虚拟机栈,不过是为虚拟机使用到的 Native 方法服务 (如C, Cpp),以支持 JNI 调用。同样的,栈帧过多可能导致StackOverflowError,栈空间不足导致可能导致OutOfMemoryError。在 Hotspot JVM 中,直接将本地方法栈和虚拟机栈合二为一。

JVM 中最大的一块内存区,是垃圾回收器管理的主要区域,主要存放:

  • 对象实例:通过 new 关键字创建的对象
  • 字符串常量池:存储 String 对象的直接引用
  • static 静态变量
  • 线程分配缓冲区 TLAB(Thread Local Allocation Buffer):为了提升内存分配效率,堆中线程私有的一块区域

特点:

  • 线程共享,需要考虑线程安全问题
  • 分配对象过大或过多可能产生OutOfMemoryError
  • 由垃圾收集器 GC 管理
  • -Xms size指定堆初始内存,-Xmx size指定堆最大内存

栈是运行时的单位,解决程序的运行问题,即程序如何执行,如何处理数据。 堆是存储的单位,解决数据怎么放,放在哪的问题。

方法区

按照 JVM 的规范,Method Area 方法区需要存储已被虚拟机加载的类信息、常量、静态变量、代码缓存等数据。而在实现上,JDK8 以前用永久代实现,但是为了能够加载更多的类同时改善 GC,现在改用位于本地内存的元空间作为方法区的实现,并且将静态变量和字符串常量池放入了堆中。

方法区的永久代实现

方法区的元空间实现

在类编译期间,会把类元信息放入方法区(元空间),包括类的方法、参数、接口,以及常量池表 Constant Pool Table。其中常量池表存储了编译期间生成的字面量(基本数据类型、字符串类型常量、声明为 final 的常量值等)、符号引用(类、字段、方法、接口等的符号引用),JVM 会为每个已加载的类维护一个常量池。

方法区中还有一个区域叫运行时常量池,在类加载-加载阶段,JVM 会把类的常量池数据放入运行时常量池;在类加载-解析阶段,会将池中的符号引用替换为直接引用。

除了在编译期生成的常量,运行时常量池还可以动态添加数据,例如 String 类的 intern() 方法可以主动将串池中的字符串对象引用放入运行时常量池。(字符串拼接见:Java基础#字符串

  • 字符串变量拼接的原理是 StringBuilder
  • 字符串常量拼接的原理是编译期优化

类常量池和运行时常量池都在方法区中,而字符串常量池在堆中,且存储的是字符串对象的引用。

特点:

  • 线程共享
  • 类加载过多或常量过多可能产生OutOfMemoryError
  • JDK8 后用-XX:MetaspaceSize-XX:MaxMetaspaceSize=sz设置元空间大小

直接内存

Direct Memory,不属于 JVM 运行时数据区,也不受 GC 管理,其分配回收成本较高,但读写性能很高,受物理内存的约束,超出物理内存将产生OutOfMemoryError

直接内存由 Native 方法分配,例如 NIO 使用直接内存作为数据缓冲区,底层使用了 Unsafe 对象完成直接内存的分配与回收(内部使用 Cleaner 配合虚引用,自动调用 freeMemory 方法回收),大大提高了 IO 性能。

虚拟机对象

对象创建

步骤:

  1. 遇到new指令时,首先检查该指令的参数是否能在常量池中定位到一个类的符号引用,并检查该符号引用代表的类是否已被加载、加载、解析和初始化,如果没有必须先执行相应类的加载过程
  2. 加载检查通过后,为新生对象分配内存
  3. 内存分配完成后,JVM将该内存空间初始化为0
  4. JVM对对象进行必要设置,例如元类型信息、HashCode、GC分代年龄等(存储在对象头中)
  5. JVM对象已产生,接着开始执行对象的构造方法 <init>()
  6. 这样一个真正可用的对象被完全构造出来

分配内存方法:

  • 碰撞指针: 使用过的内存放一边,空闲的放另一边,中间用指针分隔。分配内存**就是移动指针。内存分配规整
  • 空闲列表:维护可用内存块的记录表,分配内存时修改记录。内存分配不规整

解决并发:

  • CAS 同步:Compare And Swap 保证更新的原子性
  • TLAB 本地线程分配缓冲:线程私有的分配缓冲区

对象内存布局

  • 对象头
    • Mark Word: 存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态、偏向信息等
    • kClass Pointer: 类型指针,对象指向它的类型元数据的指针
  • 实例数据:对象真正存储的有效信息
    • 默认分配顺序:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)
    • 相同宽度的字段会被分配在一起,除了oops,其他的长度由长到短
    • 满足上述条件下,父类定义变量在子类变量之前
    • --XX:FieldsAllocationStyle 控制变量分配策略
    • --XX:CompactFields 控制是否允许较窄变量插入父类变量的间隙
  • 对齐填充:8Byte整数倍

对象访问定位

  • Java通过栈上的reference来操作堆上的具体对象,实现方式主要以下两种

  • 句柄访问

    • Java堆中划分一块内存作为句柄池,reference存储对象的句柄地址,句柄中包含对象实例数据和类型数据的具体地址
    • 好处:reference存储的是稳定句柄,移动对象时不需要改变reference 对象的访问定位_句柄访问
  • 直接指针

    • reference直接存储对象地址,但需要考虑如何存放类型数据的相关信息
    • 好处:速度快,减少一次指针定位的时间开销 对象的访问定位_直接指针访问