一、对象的创建:
创建对象在java上面是很简单的,使用new关键字就可以了,但是其实在虚拟机中,java对象的创建是一个复杂的过程。
当java虚拟机遇到一个new的指令的时候,对象创建的程序正式启动:
1、检查这个指定的参数是否能在常量池当中定位到一个类的符号引用,并且去检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有就需要先执行相应的类加载的过程;
2、类加载完成之后需要进行为新生的对象分配内存,对象所需要的内存大小在类加载完成之后就已经完全确定:
分配内存的其中两种方式:
(1)、指针碰撞:
前提:Java堆中的内存时绝对规整的,所有用过的内存放在一边,空闲的放在另外一边,中间有一个指针作为分界点的指示器;
分配方式:把指针往空闲内存那边移动一段与对象所需内存大小相等的距离。
(2)、空闲列表:
前提:Java堆中的内存并不是规整的,已使用和未使用的内存相互交错(这个是正常的,因为GC会进行垃圾回收,中间出现空闲的内存空间原因是这部分的对象被回收了)
分配方式:虚拟机必须维护一个列表,记录那些内存块是可用的,在分配对象内存的时候从列表中找到一块足够大的空间划分给对象实例。
* 使用哪一种方式分配内存,由java的垃圾收集器是否带有压缩整理功能决定,如果有该功能则垃圾收集完成之后java内存会是规整的,可以使用指针碰转法。
注意点:这里是需要考虑到并发的,即使是移动指针这样子的小动作都是有可能产生并发。解决方式有两种:
(1)、堆是线程共享的,每一个线程都可以来申请堆内存空间,对内存空间的分配采用同步处理的方式;
(2)、为每一个线程都分配一小块内存,称为本地线程分配缓冲(TLAB),各个线程需要分配内存的时候就在各自的本地线程分配缓冲(TLAB)上面分配,只有当本地线程分配缓冲(TLAB)的内存空间不足进行重新分配的时候才进行同步锁定。
3、初始化:当内存空间分配完成后需要将内存空间都初始化为零(不含对象头),如果使用的是本地线程分配缓冲(TALB),这一步也可以提前至TLAB分配内存空间的时候进行。这一步操作的目的是保证对象的实例字段在java的代码中可以不赋初始值就直接使用,所以在java程序中,类当中的字段都是会有默认值的。
4、设置对象头:包括对象的类信息元数据、哈希码、GC分代年龄等信息。
5、将对象引用入栈。
至此,在java虚拟机中,一个对象的创建已经完成了。这就是new在java虚拟机当中需要执行的动作。
--------------------------------------我是对象创建的分割线--------------------------------------
二、对象的内存布局:
如图所示,一个对象在内存中的数据包括三个部分:对象头、实例数据、对齐填充。
1、对象头:
对象头的数据包括两个部分:
(1)、对象运行时的数据:如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位虚拟机中的大小分别为32bit和64bit(这部分数据是与对象自身定义的数据无关的额外存储,所以这部分数据被设计成一个无固定的数据结构,即不固定安排大小,因此可以存储更多的数据)。
(2)、对象的类型指针,指向该对象的类元数据。
虚拟机通过类型指针来确定那个对象是类的实例,但是并不是所有的虚拟机实现都是需要在对象数据上保留类型指针的,因为查找对象的类元数据信息不一定要经过对象本身。这个涉及到访问对象的方式(句柄方位方式和直接指针方式,直接指针方式才需要把对象的类型数据指针放在对象头当中)
(3)、如果对象是一个数组,那么在对象头当中还需要有一块用来记录数组的长度,因为虚拟机可以通过普通的java对象的元数据知道java对象的大小,但是无法从数组的元数据当中确定数组的大小。
2、实例数据:
对象的实例数据是对象真正存储的有效信息,也是我们在代码中真正定义的字段内容。对象的实例数据不论是从父类那边继承下来的还是自己创建的,都需要记录。所以我们在编写代码的时候,使用继承让我们少些了很多代码,但是事实上我们编写的子类的对象,字段和数据虽然是可以继承自父类的,但是却是独立的。
3、对齐填充:
这部分的数据是没有意义的,并且也不是必需的,作用是占位。
原因:如果JVM的内存自动管理系统是要求对象的其实地址必需是8字节的整数倍,也就是说对象的大小必需是8字节的整数倍,对象头部分正好是8字节的倍数,当对象实例数据没有对齐的时候,需要对齐填充来补齐。因此这部分的数据是没有意义的,只是用来填充。
注释:需要对齐填充的原因有蛮多的,包括:CPU的读取效率,因为地址总线的原因,在寻址过程当中不对齐的情况读取一个8比特的数据需要对读取两次,而写的时候效率就更低了,需要写两次,然后组合起来的数据才是我们要写入的数据,这样子写入的效率是很低的。我们定义数据库字段的长度的时候最好也是定义成8的倍数。
三、对象的访问定位:
访问一个对象我们主要需要知道两个信息,一个是这个对象是什么(对象的类型数据),另一个是这个对象有什么(对象的实例数据)。
以前我们都知道,对象是放在堆中的,而栈中存放着指向堆相应位置的内存地址。
应该说栈上存放的是reference数据,用来操作堆上的具体对象。
java虚拟机规范中规定了reference数据是一个指向对象的引用,并没有定义这个引用是怎么去定位堆中的对象的位置和访问堆中的对象的。所以访问堆中的对象的方式也是取决于虚拟机的实现方式:
1、使用句柄:
(1)、使用句柄访问的方式的话堆中会有一个区域用来作为句柄池,reference数据存放的就是对象的句柄地址。
(2)、句柄中包含了对象实例数据与类型数据各自的具体地址信息。
原理图如下:
句柄访问的方式就是虚拟机栈中存放着reference数据,reference数据当中存放着指向对象的实例数据指针和指向对象的类型数据的指针,这些指针指向相应的对象实例数据和对象类型数据。
2、使用直接指针:
使用直接指针的访问方式,java堆中就需要考虑怎么去放置访问类型数据的相关信息,reference数据当中存储的直接就是对象地址。
前文有提到的,对象的内存分配包括三部分内容:对象头、实例数据,对齐填充。对象头当中就存放着指向对象类型数据的指针。
优缺点:使用句柄访问方式的最大好处是未定,因为里面存放的是稳定的句柄地址,对象被移动时只会改变句柄当中的实例数据指针,reference数据本身不需要改变,
使用直接指针访问好处是速度快,因为少掉了一次指针定位的时间开销(句柄访问方式两次指针定位:对象的实例数据指针到对象的实例数据,对象的类型数据指针到对象的类型数据,而直接指针方式的reference数据直接存放着对象实例数据的地址,只要从对象的对象头当中读取对象的类型数据地址定位到对象的类型数据就可以了)。