JVM

Java内存区域

Java内存区域

目录

  • 运行时数据区
    • 程序计数器
    • Java虚拟机栈
    • 本地方法栈
    • Java堆
    • 方法区
    • 运行时常量池
  • HotSpot虚拟机对象探秘
    • 对象的创建
    • 对象的内存布局
    • 对象的访问定位

运行时数据区

程序计数器

  • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
  • 字节码解释器通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖程序计数器来完成。
  • 如果线程正在执行的是一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的方法是一个Native方法的,程序计数器的值为空(Undefine)。
  • 此内存是JVM唯一没有规定任何OOM的区域。
  • 程序计数器是线程私有的。

Java虚拟机栈

  • Java虚拟机栈是线程私有的。
  • Java虚拟机栈指的是Java方法执行的内存模型:即每个方法在执行时都会创建一个栈帧(是方法运行时的一种基础数据结构),用于储存局部变量表,操作数栈,动态链接,方法出口等信息,每个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈的入栈到出栈的过程。
  • 局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用和returnAddress类型。
  • 局部变量表所需的内存空间在编译期完成分配,在方法运行期间不会改变局部变量表的大小。
  • 该区域会出现两种异常情况
    • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
    • 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError。

本地方法栈

  • 发挥的作用与Java虚拟机栈相似。
  • 与Java虚拟机栈之间的区别:
    • 虚拟机栈执行Java方法服务。
    • 本地方法栈执行本地方法服务。

Java堆

  • Java虚拟机所管理的内存区域最大的一块。
  • Java堆是在虚拟机启动时被创建,是被所有线程所共享。
  • 一般情况下:几乎所有对象实例和数组都是在堆上分配。
  • 如果在堆上没有内存完成实例分配,并且堆也无法扩展时,将抛出OOM。

方法区

  • 与堆一样,是各个线程共享的内存区域。
  • 它用于储存已被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据。
  • 当方法区无法满足内存分配的需求时,将会抛出OOM。

运行时常量池

  • 运行时常量池是方法区的一部分。
  • 当运行时常量池无法满足内存分配的需求时,将会抛出OOM。

HotSpot虚拟机对象探秘

对象的创建

  • 当虚拟机遇到new指令时,首先检查这个指令的参数是否在常量池能定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析,初始化过。如果没有的话,那必须先进行类的加载
  • 类加载检查通过后,虚拟机将为新生对象分配内存。
  • 对象所需的内存在类加载完成后便完全可确定,为对象分配空间相当于在堆上划分一块内存出来。
  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作在TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码可以不赋值就直接使用。程序能访问到这些字段的零值。
  • 接下来,虚拟机需要为对象进行必要的设置。例如:类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。

对象的内存布局

在HotSpot虚拟机中,对象在内存中储存的布局可以分为3块区域:对象头,实例数据和对齐填充。

对象头
  • 对象头包含两部分信息。
  • 第一部分用于储存对象自身的运行时数据,如哈希码,GC分代年龄,锁标记,线程持有的锁,偏向线程ID,偏向时间戳等。考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,它会根据对象的状态来复用自己的内存空间。例如,在32位的虚拟机中,如果对象处于未被锁定的情况下,那么 Mark Word的32bit空间中的25bit用于储存哈希码,4bit用于储存对象分代年龄,2bit储存锁标志,1bit固定为0。
  • 对象头的另一部分为类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
  • 实例数据为对象真正储存的有效信息,也是代码中所定义的各种类型的字段内容。
对齐填充
  • 对齐填充不是必然存在的,它仅仅起到占位符的作用
  • 由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。
  • 对象头部分正好是8字节的倍数。
  • 当实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

  • Java程序需要通过栈上的reference数据来操作堆上的具体对象。
  • 目前主流的访问方法有使用句柄和直接指针两种。
  • 如果使用句柄的话,那么Java堆将划分出一块内存来作为句柄池,reference中储存的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

  • 如果使用指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中储存的直接就是对象地址。

  • 两种访问方式的优缺:

    • 使用句柄的最大好处就是reference中储存的是稳定的句柄地址,在对象移动时只会改变句柄中的实例数据指针,而reference不需要修改。
    • 直接使用指针的最大好处就是速度更快,它节省了一次指针定位的时间开销。

参考资料:《深入理解Java虚拟机》

分享到