对象的创建
当虚拟机遇到一条字节码new指令时,首先检查指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载过程。
在类加载检查完毕后,就要对这个对象分配内存了,对象所需的内存大小在类加载完成后就确定了。
内存分配
如果内存规整——所有使用过的内存在一边,所有未使用的内存在另一边,中间有一个指针指向分界点,则分配内存的时候只需要将指针向空闲方向移动指针即可。这种内存分配方式叫做“指针碰撞”。
如果内存不规整,使用过的和空闲的内存混杂在一起,则虚拟机需要维护一个列表,记录哪些内存是可用的,分配的时候在这个列表上找一块内存区域分配给对象,然后更新列表上的记录。
Java堆是否规整取决于垃圾收集器是否带有空间压缩能力。
并发情况
如果正在给对象A分配内存,指针还没来得及修改,此时要给对象B分配内存,就会错误的使用了旧的指针。为了解决这个问题,会有两种方案
- 使用CAS和失败重试的方式保证原子性
- 每个线程在堆上预先分配一块缓冲区,每个线程优先使用自己的缓冲区分配内存。只有当本地缓冲区用完,分配新的缓冲区的时候才需要同步锁定。
内存分配完成后,虚拟机将分配到的内存空间都初始化为0。如果使用本地缓冲区分配内存的话,这一步可以提前完成。
然后初始化对象头中的信息,比如当前实例属于哪个类,哈希值等。
初始化
从虚拟机的角度,对象已经创建了,从Java程序的角度对象的创建刚刚开始,这时要执行对象的构造函数。
对象的内存布局
在HotSpot虚拟机中,对象在堆中的存储可以划分为三部分:对象头、实例数据、对其填充。
对象头
包含两类信息
- 存储对象自身的运行时数据,如哈希码、GC分代年龄、线程持有的锁等。这部分的数据长度为32位或64位(取决于虚拟机的位数)。官方称之为“Mark Word”。
- 类型指针,对象指向它类型的指针。Java虚拟机通过这个指针得知这个对象是哪个类的实例。
- 如果对象是一个数组,则头中还必须有一块用于记录数据的长度的数据。
实例数据
HotSpot虚拟机默认的分配顺序是longs/doubles、ints、shorts/chars、bytes/boolean、oops(Ordinary Object Pointers),在这个前提下,父类定义的变量出现在子类之前。
对齐填充
由于HotSpot虚拟机内存管理系统要求对象起始地址必须是8字节的整数倍,所以需要这部分将对象大小补齐为8字节的整数倍。
对象的访问
访问对象有两种方式
- 通过句柄访问
- 通过直接指针访问
如果使用句柄访问的话,则Java堆中需要分出一部分句柄池。Java栈中的reference存放的是句柄地址,在句柄中存放了到类型数据的指针和到对象实例的指针。
如果通过直接指针访问的话,直接指针指向堆中对象的地址,而且Java堆中对象需要放置访问类型信息的指针。
这两种方法各有优劣,使用句柄访问,当对象移动的时候不需要改变reference的值,只需要改变句柄中的对象实例指针,而使用直接访问要比使用句柄访问更快,只需要一次指针定位。
HotSpot主要使用直接指针。