对象的创建
一 判断是否需要类加载
虚拟机遇到new 指令时,先去常量池中检查有没有这个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,必须先执行类加载过程
二 对象内存的分配
在确定了对象的空间大小之后,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
分配的方法有两种,取决于Java堆所采用的垃圾收集器是否带有压缩整理。
如果用Serial,ParNew等等带Compact压缩过程的收集器时,就要用指针碰撞分配算法,如果用Mark-Sweep算法的收集器时,通常采用空闲列表法。
指针碰撞:
Java堆由于压缩收集算法变得绝对规整,所有用过的内存都放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就是把那个指针往空闲的空间挪动了一段与对象大小相等的距离。
空闲列表:
Java堆内存不是规整的,已使用的内存和未使用的内存错综复杂,虚拟机必须维护一个列表,记录哪些内存块是可用的,哪些是不可用的,在分配时从列表中找到足够大的一块空间划分给对象实例,并更新列表。
对象创建划分操作的线程安全性
对象在虚拟机中创建是非常频繁的行为,即使是仅仅修改一个指针的位置,在并发的情况下也并不是线程安全的,可能给对象A分配内存还没分完,对象B也使用原来的指针来分配内存。解决这种并发的线程安全问题有两种方案
一、CAS操作:
对分配内存的动作进行同步处理,用CAS配上失败重试的方式保证更新操作的原子性
二、TLAB:
把内存分配的动作按照线程划分在不同的空间中进行,每个线程在JAVA堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完该分配新的TLAB时,才需要同步锁定。是否需要用TLAB。通过-XX:+/-UseTLAB参数来设定。
收尾:
内存分配完成后,虚拟机要将分配到的内存空间初始化为0值(不包括对象头),使用TLAB的话这一工作可以提前至TLAB分配时进行。这易操作使对象的实例字段在Java代码中不赋初值就可以使用。
之后虚拟机对对象的对象头进行设置
这时所有对象的初值都是空值,对象的产生才刚刚开始,之后会执行init(初始化)方法,将对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算是完全产生出来,
对象的内存布局
对象在内存中的布局分为三块区域:对象头(Header),实例数据和对齐填充。
对象头
对象头又分为两大部分:MarkWord 和类型指针
MarkWord:
对象头信息是与对象自身定义的数据无关的额外存储成本,所以MarkWord被设计成非固定的数据结构以便在极小的空间里存储耿肯多的信息,他会根据对象的状态复用自己的存储空间,
对象在未锁定的情况下:
25bit :对象的hash码;
4 bit : 对象的分代年龄
2 bit : 锁的标志位
1 bit : 0
类型指针:
对象指向她的类元数据的指针,虚拟机借助这个指针来确定这个对象属于哪个类的实例,看一个类的元数据信息不一定要经过对象本身
如果是数组的话,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以从对象的元数据那里确定对象的大小,但从数组的元数据那里获取不到数组的大小。
实例部分:
父类继承的和自己的字段内容都需记录下来
Java虚拟机中有默认的分配策略,一般相同宽度的字段总是被分在一起,在父类中定义的变量会出现在子类的之前。如果CompactField参数为true (默认)子类中较窄的变量也可能会插入到父类变量的空隙之中。
对齐填充:
HotSpot 规定对象的起始地址必须是8字节的整数倍,没有对齐时要用这部分对齐填充。
对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。
这个reference引用通过何种方式去定位对象的位置,有两种,使用句柄和直接指针
使用句柄:
Java堆中会划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象的实例数据与类型数据各自的具体地址。
直接指针
reference中存储就是在堆中分配的对象实例数据的地址;
而对象实例数据中需要有这个对象类型数据的相关信息(前面文章讨论了HotSpot使用对象头来存储对象类型数据地址);
这两种对象访问各有优势,使用句柄1访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集移动对象)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问的最大好处就是速度快。
Sun HotSpot而言,他是使用第二种方式进行对象访问的。