[JVM]Java内存区域和HotSpot虚拟机对象探秘

一、运行时数据区域

[JVM]Java内存区域和HotSpot虚拟机对象探秘

1.程序计数器(Program Counter Register)

当前线程所执行的字节码的行号指示器。

    为了线程切换后可以恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
    如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个本地(Native)方法,这个计数器值则为空(Undefined)。

2.Java虚拟机栈

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

    与程序计数器一样,虚拟机栈也是线程私有的,它的生名周期与线程相同。

3.本地方法栈

与虚拟机栈的作用相似,区别在于虚拟机栈为虚拟机执行Java方法(也就是字节码)提供服务,本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

HotSpot虚拟机将虚拟机栈和本地方法栈合二为一了。

4.Java堆

Java堆是虚拟机所管理的内存中最大的一块。是被所有线程共享的一块内存区域,虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例

5.方法区

与Java堆一样,方法区也是线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

     在方法区中可以选择不实现垃圾收集。相对而言,垃圾手机行为在这个区域的确是比较少见的,但是并非数据进了方法区就永久存在,这个区域的内存回收目标主要是针对常量池的回收类型的卸载

二、HotSpot虚拟机对象探秘

1、对象的创建

当虚拟机遇到一条字节码new指令时,会发生对象的创建。
对象的创建过程分三步:

  • 首先检查这个指令的参数是否能在常量池中定位到一个符号的引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那么必须先执行相应的类加载过程。
  • 类加载检查通过后,虚拟机将为对象分配内存。
  • 分配内存后,虚拟机对对象进行一些必要的设置: 比如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。

2.对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对象填充(Padding)

(1)对象头
    HotsPot虚拟机的对象头部分包括两类信息。
    第一类是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
    另一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

(2)实例数据
    这部分存放的是我们在程序代码中定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

(3)对齐补充
    这不是必然存在的,也没有什么特别的含义,它仅仅起到占位符的作用。
    由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。而对象头已经被设计好了,因此如果实例数据部分没有对齐的话,就需要通过对齐填充来补全。

3.对象的访问定位

    引用(reference)如何访问、定位到对象的位置。

    主流的访问方式有两种:

  • 句柄

    • 使用句柄时,Java堆中会分出一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄池中包含了对象实例数据与类型数据各自具体的地址信息。
  • 直接指针

    • 使用直接指针时,Java堆中对象的内存布局就需要考虑如何放置访问类型数据相关信息。

      reference中存储的就是对象的地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。

HotSpot虚拟机主要采用的是直接指针的方式。

这两种方式各有优劣:

句柄方式的最大好处就是reference中存储的是稳定句柄地址,当对象在堆中被移动时只会改变句柄中的示例数据指针,而reference本身不需要修改。

直接指针的最大好处速度更快,节省了一次指针定位的时间开销。