JVM学习02——从虚拟机的角度看java对象的创建

JVM学习02——从虚拟机的角度看java对象的创建

对象的创建流程

从jvm的角度,对象的创建具有以下的流程

JVM学习02——从虚拟机的角度看java对象的创建

  1. 当jvm遇见了new指令
  2. 检查常量池是否能定位到一个类的符号引用,如果没有的话就执行类加载流程,如果有的话就进行下一步
  3. 给对象分配内容
  4. 把内存空间初始化为零值
  5. 设置对象头

总的来说是这五步

如何分配内存

分配内存通过有两种算法:指针碰撞和空闲列表

1. 指针碰撞

假设java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。这种策略适用于规整的内存分布。

2. 空闲列表

如果java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞,虚拟机必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新例表上的记录。

选择哪种分配方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

资源竞争

在多线程的情况下,对内存的分配可能是,刚刚给一个线程分配了内存,还没有实际分配出来,例如指针没移动、空闲列表没有写入,另一个线程也在同样的位置要进行写入,就会发生问题。解决这个问题的方案,《深入理解java虚拟机》提到了两种,而调研发现似乎有三种,后两种的思路比较相似。

1. 采用同步处理

采用CAS进行同步处理

2. 栈上分配

在我们的应用程序中,其实有很多的对象的作用域都不会逃逸出方法外,也就是说该对象的生命周期会随着方法的调用开始而开始,方法的调用结束而结束,对于这种对象,是不是该考虑将对象不在分配在堆空间中呢?

因为一旦分配在堆空间中,当方法调用结束,没有了引用指向该对象,该对象就需要被gc回收,而如果存在大量的这种情况,对gc来说无疑是一种负担。

因此,JVM提供了一种叫做栈上分配的概念,针对那些作用域不会逃逸出方法的对象,在分配内存时不在将对象分配在堆内存中,而是将对象属性打散后分配在栈(线程私有的,属于栈内存)上,这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给gc增加额外的无用负担,从而提升应用程序整体的性能

3. TLAB(thread local allocation buffer)

我们知道,对象分配在堆上,而堆是一个全局共享的区域,当多个线程同一时刻操作堆内存分配对象空间时,就需要进行同步,而同步带来的效果就是对象分配效率变差(尽管JVM采用了CAS的形式处理分配失败的情况),但是对于存在竞争激烈的分配场合仍然会导致效率变差。

那么能不能构造一种线程私有的堆空间,哪怕这块堆空间特别小,但是只要有,就可以每个线程在分配对象到堆空间时,先分配到自己所属的那一块堆空间中,避免同步带来的效率问题,从而提高分配效率。

JVM默认开启了TLAB功能,也可以使用-XX: +UseTLAB 显示开启。

JVM提供了-XX:+PrintTLAB 参数打开跟踪TLAB的使用情况。

-XX:TLABSize 通过该参数指定分配给每一个线程的TLAB空间的大小。

需要TLAB的原因就是提高对象在堆上的分配效率而采用的一种手段,就是给每个线程分配一小块私有的堆空间,即TLAB是一块线程私有的堆空间(实际上是Eden区中划出的)。

如下流程所示:

JVM学习02——从虚拟机的角度看java对象的创建

对象布局

进行完内存分配和对应的空间置零后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC粉黛年龄等信息。这些信息存放在对象的对象头中。
Java对象由三部分构成:对象头、实例数据、对齐补充。

对象头

  • 第一部分是与对象在运行时状态相关的信息,长度通过与操作系统的位数保持一致。包括对象的哈希值、GC分代年龄、锁状态以及偏向线程的ID等。由于对象头信息是与对象所定义的信息无关的数据,所以使用了非固定的数据结构,以便存储更多的信息,实现空间复用。因此对象在不同的状态下对象头的存储信息有所差别

JVM学习02——从虚拟机的角度看java对象的创建

  • 另一部分是类型指针,即指向该对象所属类元数据的指针,虚拟机通常通过这个指针来确定该对象所属的类型(但并不是唯一方式)。
  • 另外,如果对象是一个数组,在对象头中还应该有一块记录数组长度的数据,因为JVM可以通过对象的元数据确定对象的大小,但不能通过元数据确定数组的长度。

实例信息

实例数据存储的是真正的有效数据,即各个字段的值。无论是子类中定义的,还是从父类继承下来的都需要记录。这部分数据的存储顺序受到虚拟机的分配策略以及字段在类中的定义顺序的影响。

对齐补充

这部分数据不是必然存在的,因为对象的大小总是8字节的整数倍,该数据仅用于补齐实例数据部分不足整数倍的部分,充当占位符的作用。

对象的访问定位

Java中的对象在堆内存中分配内存空间,引用保存在栈内存中,通过引用定位对象的具体为止通常有两种方式:句柄访问和直接指针访问。

句柄访问

此方式在堆空间维护一个句柄池,对象引用中保存的是对象的句柄位置。句柄中包含各对象的实例数据和类型数据的地址信息:

JVM学习02——从虚拟机的角度看java对象的创建

此方式的好处是对象引用中保存的是稳定的对象句柄的地址,因为对象的移动在GC过程中是非常普遍的行为,对象的移动会导致实例数据的地址发生变化。带来的缺点就是访问效率受影响。

直接指针访问

即对象引用中保存的直接的对象地址

JVM学习02——从虚拟机的角度看java对象的创建

该方式的优点是节省了一次指针定位的开销,访问速度快。缺点是当对象地址发生变化是引用中保存的数据也需要变化。