JVM(2) 运行时数据区
2024-10-17 15:28:44 # Language # Java

1. 运行时数据区

image.png

1.1 程序计数器PC

存储下一条需要执行的字节码指令地址,PC是唯一一块不会出现内存溢出的内存区域

1.2 栈

HotSpot中只使用了一个栈,同时作为虚拟机栈与本地方法栈

栈帧

局部变量表

存放了编译期可知的各种Java虚拟机基本数据类型、对象引用和 returnAddress 类型

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的 long 和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个

实例方法中的局部变量表会在0号槽存放this,运行时会在内存中存放实例对象地址,接下来会依次存放参数、方法体中的数据

槽可复用

操作数栈

存放临时数据,在编译器即可确定其最大深度

帧数据

包含动态链接、方法出口、异常表的引用等

  • 当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。
  • 方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
  • 异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。

内存溢出

StackOverFlowError

设置栈内存大小

  • -Xss{栈大小}
  • 单位:字节(默认,必须是1024的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
  • 也可以通过 -XX:ThreadStackSize={栈大小} 来配置堆栈大小

如果不指定栈的大小,JVM创建的栈的默认大小取决于操作系统和体系结构

HotSpot对栈大小有最大和最小的要求

  • Win64 JDK8测试最小值为180k,最大值为1024m

局部变量过多、操作数栈深度过大也会影响栈内存的大小

一般可以设置为-Xss256k节省内存

1.3 堆

线程共享,会内存溢出(OutOfMemoryError)

堆空间有三个值,可通过arthas dashboard -i {刷新频率(ms): 5s}memory 来查看:

  • used: 当前已使用的堆内存
  • total: 虚拟机已分配的可用堆内存
  • max: 虚拟机可分配的最大堆内存

随着堆中数量增多,当total可使用内存即将不足时,虚拟机会继续分配内存给堆,total最多只能与max相等

并不是当 used=total=max 时堆内存溢出,详见垃圾回收篇

如果不设置虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64

  • -Xms{size} 设置初始的total
  • -Xmx{size} 设置max
    • 单位:字节(默认,必须是1024的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
    • 限制:Xmx必须大于 2 MB,Xms必须大于 1 MB

服务端开发时建议将 -Xms 和 -Xmx 设置为相同的值,减少分配和收缩内存的时间开销

为什么arthas中显示的堆大小与设置的值不同?例如 -Xmx1g 只有 981M

  • arthas中的堆内存使用了JMX技术中内存获取方式,这种方式与垃圾回收器有关,计算的是可以分配对象的内存,而不是整个内存。

1.4 方法区

线程共享,每种虚拟机对方法区的实现都不同,包含类的元信息、运行时常量池、字符串常量池

虚拟机 方法区实现
HotSpot JDK7及之前版本 存放于堆中的永久代空间,堆的大小由虚拟机参数 -XX:MaxPermSize={值} 来控制
HotSpot JDK8及之后版本 本地内存中的元空间,由OS维护,独立于虚拟机内存,可使用 -XX:MaxMetaspaceSize={值} 来限制元空间最大大小

类的元信息

  • 存储每个类的基本信息,即InstanceKlass对象,在类的加载阶段完成
    • 虚方法表:实现多态的基础

运行时常量池

  • 存储字节码文件中的常量池内容,可以通过内存地址快速定位到常量

字符串常量池

  • JDK7之前:运行时常量池逻辑包含字符串常量池
  • JDK7:字符串常量池单独存放在堆中,运行时常量池依旧存放在永久代
  • JDK8之后:永久代改为元空间,字符串常量池还在堆中

由于JDK7及之后版本的字符串常量池存放在堆中,intern()方法会把第一次遇到的字符串的引用放入字符串常量池

静态变量的存放

  • JDK6及之前版本,静态变量存放在方法区,即永久代
  • JDK7及之后版本,静态变量存放在堆区的Class对象中,脱离了永久代,参考putstatic源码

1.5 直接内存

不存在于《Java虚拟机规范》,不属于Java运行时的数据区域,也存在内存溢出(OutOfMemoryError)

要创建直接内存上的数据,可以使用ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);

手动调整直接内存的大小:-XX:MaxDirectMemorySize={大小}