1、对象创建时内存分配
1.1、对象所属类的加载检测
虚拟机遇到字节码new指令时,会检测该类是否已经被加载、解析和初始化。如果没有则进行相关的加载操作。
1.2、内存分配
该阶段相当于在堆内存中划分出一块大小确定的内存。内存的分配方式和垃圾回收器的实现方式有关,取决于垃圾回收器是否具备空间压缩整理能力。分配方式有:
1.2.1、指针碰撞:堆内存如果是绝对规整的,即被使用在一边,空闲的在另一边,两者之间通过一个指针来区分,那么划分内存就是通过移动该指针(移动大小为该对象的大小)来实现。
1.2.2、空闲列表:如果堆内存中空闲内存和已使用内存互相交错的话,那么需要通过维护一张空闲内存列表来记录空闲的内存。当需要为对象分配内存的时候,找出一块足够大的内存空间,并更新空闲列表。
使用如Serial、ParNew等带有压缩整理过程的垃圾收集器时使用指针碰撞的方式;使用CMS这种基于清除算法的收集器,理论上只能使用空闲列表来分配内存,但实际上CMS设计了一个分配缓冲区,该区域内使用的是指针碰撞。
1.2.3、内存分配线程安全问题解决方案:
1、将内存分配动作同步,虚拟机采用CAS(CAS简单说就是修改前比较该数据是否被更改过,如果没有被更改则修改值)加失败重试的方法保证内存分配的原子性。
2、本地线程分配缓冲:每一个线程都有一个缓冲内存,需要分配内存的时候,在自己的线程分配缓冲中分配。只有当线程分配缓冲被用完时才需要同步锁定,分配新的缓冲。
1.3、对象设置
对对象进行最基本的设置,其中包括:
1、对象头
2、对象所属的类
3、如何找到对象的元数据信息
4、对象的HashCode(实际上会延后到对象调用Object::hashCode()时)
5、对象的GC分代信息
此时对象中所有字段都是默认的值,完成这些操作后才是对象的初始化——Class中的<init>()方法执行,这时才真正的构建你所需的对象。
2.对象内存布局
2.1、运行时数据,该数据在64位下占用64bit:
2.1.1、HashCode
2.1.2、GC分代年龄
2.1.3、锁状态标志
2.1.4、线程池有的锁
2.1.5、偏向线程id
2.1.6、偏向时间戳
2.2、类型指针
类型指针指向通过该指针确定对象属于那个类,但是该部分不一定都有,确定对象属于那个类不一定要通过对象本身。如果对象是个数组,那么还必须有一块用于记录数组长度的数据,只有这样虚拟机才能确定 Java对象的大小,如果数组长度位置,则数组大小未知,则对象大小未知,这是不行的。
2.3、有效数据
该部分是对象存储的真正有效的数据,是我们在类中定义的数据,如父类继承下来的数据、本身的字段,这些字段存储的顺序受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和在代码中定义的顺序的影响。默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers 普通对象指针),相同宽度的数据被分配到一起存放,父类变量会出现在子类之前(如果虚拟机参数-XX:CompactFields值为true,则子类数据中较窄的数据也可以插入父类变量的空隙中以节省空间,该参数默认值也是true)。
2.3、对齐部分
非必要部分,仅因为HotSpot虚拟机自动内存管理系统要求对象起始地址要为8字节的整倍数,也就是对象大小必须是8字节整倍数,如果对象不满足8字节整倍数则需要填充。
3、对象访问定位
1、句柄
句柄这个概念我迷惑了超久,从字面上根本看不出来是什么意思,百度到的也是Windows相关的内容,或者仅仅是说这个东西相当于一个指针,并没有说具体是什么。句柄在Java中其实就是一个保存了对象实例的指针和对象类型数据的指针的数据。
2、定位方式
2.1、通过句柄定位
前文说到句柄是一个保存了对象实例的指针和对象类型数据的指针的数据,在某些Java虚拟机中,Java堆中会开辟一个空间,该空间存放着这些句柄,被称为句柄池。Java每创建一个对象都会在栈中建立该对象的reference(对象的引用),这个reference指向的就是句柄池中标识该实际对象的句柄。通过句柄就可以访问到实际对象,但是这种方法需要定位两次才能找到需要的数据,增加了开销。好处是无论对象经过怎样的蹂躏(垃圾回收),改变了位置,句柄的位置始终不变,变的只是里面的指针数据,这样reference就不会频繁改变了。
2.2、通过直接指针定位
该方法是指reference指向的直接就是对象的内存地址,这样获取需要的数据只需要一次定位就可以了,但是使用直接指针的方式定位,那么就要考虑在对象中存放对象类型的相关信息。目前HotSpot主要使用直接指针的方式定位对象。