JVM(二):HotSpot虚拟机对象探秘

时间:2022-01-05 10:20:40

HotSpot虚拟机对象探秘

对于Java程序员来讲,在实际开发过程中,new指令应该是使用的最多的指令之一了,当我们需要使用某一个类的实例时,就可以使用new指令创建出这个类的实例,但是对于虚拟机来说,当我们使用new指令的时候,它又做了什么呢?

对象的创建

当虚拟机遇到一个new指令的时候,首先它会去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并且要检查这个类是否已经被加载,解析和初始化,如果没有,就需要先去执行类加载。

当这些都检查通过之后,虚拟机就会创建一个新生对象并且分配内存,也就是从堆内存中划分一块儿地方给这个新生对象。

分配内存的方式有两种:

  1. 指针碰撞
  2. 空闲列表

因为创建对象是非常频繁的一件事情,虚拟机必须要考虑到在并发的情况下,如何保证线程安全问题,对于虚拟机来说,保证线程安全有两种方式:

  1. CAS+失败重试
  2. TLAB

我在另一篇博客中有详细分析了指针碰撞以及空闲列表的实现方式以及线程安全的问题,需要了解的可以跳转去看,在此不做太多说明。

http://blog.csdn.net/sunny_aaadolly/article/details/78924799 [深入分析虚拟机创建对象的两种方式以及如何在并发情况下实现线程安全]

内存分配完成后,虚拟机会对分配到的内存空间初始化为零值,这个操作不包括对象头。

当然,如果虚拟机采用TLAB来实现线程安全,也可以在TLAB中去做初始化的操作。

那为什么在初始化的时候不包括对象头呢?因为对象头中存的基本上都是一些这个对象的信息,这些东西没办法也不能初始化为零值,比如这个对象是哪个类的实例,如何才能找到类的元数据,对象的哈希码,对象的GC分代年龄等等,就比如你生产一步手机,手机的说明书,配件信息,保修卡之类的,你总不能给用户一个空白的,等着用户去编写吧?呵呵。

好了,我们再来说下零值,零值就是某一种数据类型的默认初始值,比如int类型,如果你不赋初值的话默认就是0,如果是引用类型,你不赋初值的话默认就是null,这个就叫零值,那为什么要赋零值呢?就是为了在开发过程中,即使你不去给这些数据字段赋值,你也可以拿过来使用(当然,这么做风险很大,比如很容易出现空指针)。

举个例子,看以下代码:

public class Demo {
int age;
Integer score;
public void setAge (int age) {
this.age = age
}

public void setScore (Integer score) {
this.score = score
}

public int getAge () {
return age;
}

public Integer getScore () {
return score;
}
}

=================================================

public class Test {
public static void main (String[] args) {
Demo demo = new Demo();
System.out.println(demo.getScore());
}
}

如果我们运行这个main方法的话,你会发现,虽然我们并没有调用Demo类中的set方法,但是我们调用get方法还是会返回一个结果null,那么这个null就是在创建对象的时候,虚拟机初始化这个对象时去赋的零值。

OK,到这个时候,从虚拟机角度上来讲,一个对象就创建成功了。

当然,对于Java程序来说,我们还少了一步,就是这个对象的init方法还没有执行,当一个类被编译成字节码的时候,会自动生成两个方法,init和clinit,首先虚拟机会调用clinit方法,然后再调用init方法,把对象按照程序员的意愿进行初始化,然后一个真正可以使用的对象就产生了。

对象的内存布局

在HotSpot虚拟机中,一个对象所占的内存布局分为三部分:

  1. 对象头
  2. 实例数据
  3. 对齐填充

    • 对象头

对象头包含了对象自身所有的运行时数据以及类型指针。

运行时数据就是哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等等

另一部分就是类型指针,这个指针指向了方法区中的对象类型数据,用于告诉虚拟机这个对象是哪一个类的实例。

但是也并不是所有的虚拟机都把这个类型指针方法对象头里,也就说,并不一定要通过对象才能找到对象的类型,这点我们在最后会做解释,

  • 实例数据

这部分是对象中真正有用的东西,其实简单理解,就是我们所定义的类的成员变量以及父类定义的变量。

  • 对齐填充

基本没什么用,就是为了补足对象占用的空间,毕竟一个对象的占用内存大小都是8的倍数,这个东西就是为了补齐8的倍数,没毛用。

对象的访问定位

了解了基本的堆栈知识,我们应该就能知道,对象是通过引用去访问的,那么在虚拟机中,访问对象也有两种方式:

  1. 句柄访问
  2. 指针访问

那么这两种方式有什么不同呢?

首先我们分析一下句柄访问,当一个对象创建完成后,会涉及到三部分内存,堆,栈,方法区。

随着方法的运行,在栈里面会创建一个栈帧,这个栈帧中有一个局部变量表,表里有一个引用类型变量,引用类型变量里存放了一个地址,这个地址指向的是堆内存中的一个句柄池里面的一个句柄,那这个句柄又包含两个指针,一个指针指向了堆内存的另一个部分,实例池,实例池中就是这个对象的实例数据,而另一个指针指向了方法区里面的对象类型数据,这个是句柄访问。

另一种就是指针访问,不同点在于,引用类型变量里面存的地址,直接指向了Java堆中的实例数据,那么这个到对象类型数据的指针就包含在了实例数据里面,这个到对象类型数据的指针又指向了方法区里面的对象类型数据。

反过头来我们看之前说的“但是也并不是所有的虚拟机都把这个类型指针方法对象头里,也就说,并不一定要通过对象才能找到对象的类型”这句话,如果通过句柄访问的话,我们就是通过句柄池里的指针找到的这个对象的类型数据,也就是说类型指针在对象头里,我们并没有通过对象本身的实例数据去找这个对象的类型,但是如果通过指针访问呢,类型指针是被包含在了实例数据中的,通过实例数据找到的类型数据,那么类型指针就是在实例数据里而不是在对象头里了。