当一个类加载到内存后,JVM运行时数据区域都需要存储什么东西呢?

1)类的格式信息、主次版本号、 常量池、字段、方法、属性信息需要存储,这部分数据需要每个线程共享;
当类加载器将静态的二进制字节码从类文件加载到内存后,对内容进行验证、解析转换为符合JVM存储的数据格式。

2)生成的Class类对象和代码运行时动态创建的对象和数组需要存储;
通过loading、linking、initializing后生成的Class对象,做为程序访问这个类的入口,
以及程序运行时产生的对象和数组,都有可能会被不同的线程引用和访问,因此这部分区域的数据也需要线程共享。
3)每个线程的线程数据需要存储和每个线程下方法的方法数据需要存储;
当线程创建的时候需要有一个线程栈来存放线程信息,以及PC计数器记录当前线程正在执行的代码行号。
还有JVM本地线程执行时需要的本地线程栈存放本地线程的执行情况。

《Java虚拟机规范》规定了如下内存区域来存储数据

  • 共享区域
  • 堆区
  • 方法区
    • 运行时常量池
    • 线程私有区域
  • JVM线程栈
  • 本地方法栈
  • PC计数器

Runtime DataArea

堆区

几乎所有的对象以及数组都存放在堆区。

方法区

用于存储已经被虚拟机加载的类型信息、常量、静态变量、及时编译器变异后的代码缓存等数据。

运行时常量池

运行时常量池是方法区的一部分,用于存放编译期或者运行期生成的各种字面量和符号引用。

Java虚拟机栈

每个线程会生成一个线程栈,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口信息。

局部变量表

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、long 、float、double)、引用类型、returnAddress类型。
这些数据类型在局部变量表中的存储空间以局部变量槽来表示(64位的long和double占用2个变量槽,其余数据类型占用1个变量槽)。局部变量表所需的内存空间在编译期完成分配,因此当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是确定的。在运行期间是不会改变局部变量表的大小(槽的多少)。

本地方法栈

与虚拟机栈作用类似,为虚拟机使用到的本地方法服务。

PC计数器

当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

对于JDK1.8以后HotSpot虚拟机改用与Jrokit、J9一样与本地内存中实现的元空间(Meta-space)来实现方法区。无论是原空间还是永久代,他们只是方法区的实现,字符串常量池和静态变量的存储逻辑上仍然属于方法区。元空间使用的是物理内存、永久代使用的是JVM内存。因此元空间的大小只受限于物理内存。

HotSpot 虚拟机实现

HotSpot 虚拟机对象的创建与引用

对象的创建

1) 在new一个对象的时候,先需要通过类加载检查,然后虚拟机为新生对象分配内存。

对象所需要的内存大小在类加载完成后便可以完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。

  • 指针碰撞
    假设内存规整,将使用过的在一边,空闲的在一边,中间放一个指针作为分界点的指示器,所分配内存就是把指针向空闲空间方向挪动一段与对象相等的距离。
    使用Serial、ParNew带压缩整理过程的GC收集器时,采用指针碰撞,简单高效;
  • 空闲列表
    假设内存不规整,是一块块不连续的,那么就需要维护一个列表记录哪块内存可用,哪块内存不可用,在分配的时候在列表中找一块足够大的空间划分给对象实例,并更新列表上的记录。
    使用CMS基于清除算法的收集器时,理论上只能采用复杂的空闲列表来分配内存。
Thread Local Allocation Buffer

在堆中创建对象是特别频繁的事情,并且涉及到多线程的访问。如何保证高效且安全的申请空间?

  • 同步或者CAS+失败重试的方式保证更新操作的原子性;
  • 为每个线程在堆中预先分配一小块内存,通过-XX:+/-UseTLAB 设定;

2)分配完内存后,为内存空间初始化零值以及必要的设置。
初始化零值可以保证对象实例字段在Java对象中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
之后设置这个对象的类型、关联类的元数据信息、GC分代年龄信息等。

3)执行 <init>() 方法,将对象进行初始化。

对象的内存布局

在Hotspot虚拟机中,对象在内存中的存储布局划分为三个部分

  • 对象头(Header)
    • 一部分是对象自身运行时数据,HashCode, GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向时间戳(数组长度)。
    • 一部分是类型指针,指向它的类型元数据的指针,用来确定是哪个类的实例。
  • 实例数据(Instance Data)
    字段的存储顺序受到虚拟机分配策略(-XX:FieldsAllocationStyle)和字段在源码中的顺序影响。
    默认相同宽度的字段被分配到一起存放,父类中定义的变量会出现在子类之前。
    使用+XX:CompactFields=true字类中较窄的变量也会插入到父类变量的空隙之中。
  • 填充(Padding)
    为了满足硬件的性能需求和提高垃圾回收时指针扫描的方便性。HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8个字节的整数倍(任何对象的大小必须是8个字节的整数倍)。

对象的访问定位

对象在堆中创建好了,Java程序会通过栈上局部变量表中的reference字段来引用堆上的对象。

  • 句柄访问
    Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息。
    好处:reference中存储的事稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针。
    句柄访问
  • 直接指针
    reference中存储的是对象的地址。好处是速度快,节约了指针定位的时间开销。
    直接指针