引:总有些人会思考对象是如果创建、如何布局、以及如何访问的?对于这些问题,我们必须把讨论范围限定在具体的虚拟机和集中在某一个内存区域才有意义。基于实用原则,我们以常用的虚拟机HotSpot和常用的内存区域Java堆为例。
对象的创建
Java程序创建对象不过是一个new关键字而已,而在虚拟机中,创建了一个对象却经历了一系列过程。
- 虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,如果没有就会抛出ClassNotFoundException,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
类加载检查通过后,虚拟机为新生对象分配内存,对象所需内存的大小在类加载完成后便可以完全确定。
这里有两种内存分配方式:(由采用的垃圾收集器是否带有压缩整理的功能决定)
指针碰撞
假设Java堆的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表
假如Java堆中的内存并不是完整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录那些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给实例,并更新列表上的记录。
解决在并发情况下不安全的方案:
对分配内存空间的动作进行同步处理——虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
把内存分配的动作按线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,只有需要重新分配的时候才同步锁定
虚拟机将分配到的内存空间都初始化为零值(默认初始化),保证了对象实例在Java代码中可以不赋初始值就可以使用。
- 设置对象头
- 利用构造函数进行初始化
对象的内存布局
对象头
- 存储对象自身的运行时数据(哈希码,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等),官方称为“Mark Word”,它被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,他会根据对象的状态复用自己的存储空间,具体见《深入理解Java虚拟机》。
- 类型指针(可选)
即对象指向它的元数据(方法区)的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 - 数组长度(可选)
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象元数据信息确定Java对象的大小,但是从数组数据无法确定数据大小
实例数据
存储所有成员变量,无论是父类继承下来的,还是在子类定义的。
存储顺序会受到虚拟机分配策略参数和字段在源码定义的顺序的影响。
HotSpot默认的分配策略为相同宽度的字段总是被分配到一起(如long和double),在这个前提下,父类先于子类,若CompactFields参数为true,那么子类之中较窄的变量也可能插入到父类变量的空隙中(是因为一个slot太大)
对齐填充(可选)
起到占位符的作用,确保对象的长度为8字节的整数倍
HotSpot VM的自动内存管理系统要求对象起始位置必须是8字节的整数倍,由于对象头一定是8字节的整数倍,所以利用占位符可以达到数据部分也是8字节的整数倍。从而达到对象的长度是8字节的整数倍。(有点绕口啊,哈哈)
对象的访问定位
我们通常都会使用Java对象,我们基本上都是通过虚拟机栈上的reference数据来操作堆上的具体对象,而栈上只是一个指向对象的引用,对象的具体访问方式取决于虚拟机,目前有以下两种访问方式:
通过句柄访问对象
可以看下面的图:
使用句柄访问,Java堆中会划分出一块内存来作为句柄池,reference存储的就是对象的句柄地址,而句柄包含了对象实例数据与类型数据各自的具体地址信息。
通过直接指针访问对象
可以看下面的图:
使用直接指针访问,Java堆对象的布局中就要考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
两者对比
- 通过句柄访问对象可以当对象被移动之后只会改变句柄中的实例数据指针,而reference本身不需要改变。
- 使用直接指针访问可以加快Java对象的访问,HotSpot就是使用直接指针访问对象的方式。
总结
这里讲的对象重点还是在虚拟机执行部分,关于Class文件的讲解没有涉及到,但它却是十分重要的,日后会提及。