我们的对象一般存储在我们的堆内存中,我们把实例对象可以划分为对象头,实例数据,对齐填充

我们可以在Hotspot官方文档中找到它的描述(下图)。从中可以发现,它是Java对象和虚拟机内部对象都有的共同格式,由两个字(计算机术语)组成。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

它里面提到了对象头由两个字组成,这两个字是什么呢?我们还是在上面的那个Hotspot官方文档中往上看,可以发现还有另外两个名词的定义解释,分别是 mark word 和 klass pointer。
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit,因为对象头中要存储的数据已经超过了64bit的限制,考虑到了了虚拟机的空间效率,所以Mark Word被设计成动态定义的数据结构,以便在极小的内存空间存储尽量多的数据,根据对象的状态来复用我们的存储空间
在32位JVM中是这么存储的

在64位JVM中是这么存的

即类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。JVM使用的是直接指针
对象的访问定位
建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于在Java虚拟机规范里面只规定了 reference类型 是一个指向对象的引用,并没有定义这个引用应该通过什么种方式去定位、访问到堆中的对象的具体位置,对象访问方式也是取决于虚拟机实现而定的。主流的访问方式有使用句柄和直接指针两种。
如果使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。如图1所示。

图1 通过句柄访问对象
如果使用直接指针访问的话,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如图2所示。

图2 通过直接指针访问对象
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可 观的执行成本。从上一部分讲解的对象内存布局可以看出,就虚拟机HotSpot而言,它是使用第二种方式进行对象访问,但在整个软件开发的范围来看,各种 语言、框架中使用句柄来访问的情况也十分常见。
如果对象有属性字段,则这里会有数据信息。如果对象无属性字段,则这里就不会有数据。根据字段类型的不同占不同的字节,例如boolean类型占1个字节,int类型占4个字节,oops(引用类型)等等;
对象可以有对齐数据也可以没有。默认情况下,Java虚拟机堆中对象的起始地址需要对齐至8的倍数。如果一个对象用不到8N个字节则需要对其填充,以此来补齐对象头和实例数据占用内存之后剩余的空间大小。如果对象头和实例数据已经占满了JVM所分配的内存空间,那么就不用再进行对齐填充了。
所有的对象分配的字节总SIZE需要是8的倍数,如果前面的对象头和实例数据占用的总SIZE不满足要求,则通过对齐数据来填满。
为什么要对齐数据?字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。
这里我们只考虑new关键字(复制,反序列化,反射不考虑)和普通对象(不包括class对象和数组对象)
1)检查是否有对象对应的类信息
当我们的Java虚拟机碰到了一条字节码new指令时
2)分配内存
对象所需的内存的大小在类加载完成后便可完全确定,对应堆内存的是否是绝对规整的,采用不同的方式分配内存
关于内存分配的安全性
我们知道创建对象在JVM中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发的情况下也并不是线程安全的,比如出现在给A分配内存,指针还没有来得及修改,对象又同时使用了原来的指针进行分配内存
3 )对对象进行必要的设置
4 )构造函数的执行
简单类对象的实例化过程
1、在方法区加载类;
2、在栈内存申请空间,声明变量P;
3、在堆内存中开辟空间,分配对象地址;
4、在对象空间中,对对象的属性进行默认初始化,类成员变量显示初始化;
5、构造方法进栈,进行初始化;
6、初始化完成后,将堆内存中的地址赋给引用变量,构造方法出栈;
子类对象的实例化过程
1、在方法区先加载父类,再加载子类;
2、在栈中申请空间,声明变量P;
3、在堆内存中开辟空间,分配对象地址;
4、在对象空间中,对对象的属性(包括父类的属性)进行默认初始化;
5、子类构造方法进栈;
6、显示初始化父类的属性;
7、父类构造方法进栈,执行完毕出栈;
8、显示初始化子类的属性;
9、初始化完毕后,将堆内存中的地址值赋给引用变量P,子类构造方法出栈;