普通对象的创建(不包括数组和class对象):
当虚拟机遇到new指令时,会在常量池中检查是否包含这个类的符号引用(全限定名),通过这个确定是否经过类加载的过程,如果true,为该
对象分配内存,对象大小在类加载过程就已经确定。如果false,需要进行类加载。
分配内存
1、分配内存的方式:
指针碰撞:如果内存是绝对规整的,使用过的在一边,未使用过的在另一边,中间有个指针作为分界点的指示器,为对象分配内存的时候,只需
要向未使用过的一边移动与对象大小相同的距离完成内存分配
空闲列表:内存非规整的情况下,jvm通过维护一个列表记录哪些内存可用,在分配内存的时候,从列表里面找到一个大于对象大小的内存进行
分配,并且更新列表记录使用哪种内存分配方式取决于使用哪种垃圾收集器:
Serial、ParNew等带有Compact过程,采用指针碰撞
类似CMS这种基于Mark-Sweep算法的收集器,采用空闲列表
2、并发环境下分配内存:
如果给对象A分配内存的时候,指针还没来及修改,对象B使用原来的指针来分配内存(类似脏读)
解决方案:
①.对分配内存的动作进行同步处理--通过CAS+失败重试的方式保证操作的原子性(了解CAS机制可以查看Atomic源码)
②.使用本地线程缓冲区TLAB,就是为每个线程在堆中预先分配一小块内存,各个线程分配内存相互不影响(类似ThreadLocal变量概念),只
有在TLAB用完重新分配新的TLAB时,才需要进行同步处理。通过-XX:+/-UseTLAB参数配置是否使用TLAB
上述分配内存动作完成。
初始化内存
此时,jvm将分配的内存空间初始化为零值(不包含对象头),如果使用TLAB,在分配TLAB时提前完成这一步
作用:
为了保证用户没有赋初值的情况下也是可以使用
然后设置对象头保存的一些信息,例如:这个对象是哪个类的实例,如何找到类的元数据信息、对象的哈希码、GC分代年龄等。
都完成之后,VM角度来看,一个对象已经产生,但程序的角度,对象创建才刚开始——
一个真正的对象才完成
对象的内存布局(HotSpot)
1、对象头Header:
①存储对象本身的运行时数据,如hashcode,GC分代年龄、锁状态标志、线程持有的锁等
②类型指针:对象指向本身类元数据的指针,虚拟机就是通过类型指针确定对象是哪个类的实例。但不是所有的对象都保存类型指针,所以查
找类的元数据也不一定通过对象本身
针对数组,Header还要一个记录数组长度的数据,因此无法通过数组的元数据确定数组大小,但是普通java对象是可以的
2、实例数据Instance Data:
对象真正存储的有效数据,也就是代码定义的各种类型字段,无论是从父类继承的,还是子类本身定义的。相同宽度的字段被分配到一起。满
足这个条件前提下,父类定义到子类前,但是如果CompactFields=true,子类较小的变量也可能插入到父类变量的空隙
3、对齐填充Padding:
不是必然存在的,没有特别的意义,起着占位符的作用,对象起始地址要是8字节的整数倍,就是对象的大小必须是8字节的整数倍,对象头是8
字节的1倍或2倍,当对象实例数据部分没有对齐时,需要对齐填充来补全。
对象的访问定位
java程序通过栈中的reference操作堆中的具体数据。
定位方式:
①.句柄访问(间接):堆中维护一个句柄池,本地变量表中reference存储的就是对象的句柄地址,句柄池包含了对象实例数据(堆)和类型数
据(方法区)各自的地址信息
②.直接指针访问:reference中存储的就是对象实例数据,实例数据有指向方法区中的对象类型数据的指针
对比:
句柄访问的优点:reference中保存的只是句柄地址,如果对象GC被移动只需要改变实例数据指针,reference本身不需要改变
指针访问的优点:速度更快。节省一次指针定位的时间开销,因为对象访问在java很频繁,所以节省的时间开销是很客观的。HotSpot采用指针访问