对象的创建
虚拟机在遇到一条new指令时,首先先检查该指令的参数能否在常量池中定位到这个类的符号引用,并检查该类是否被加载,解析,初始化过,若没有,则先加载。
若类加载检查通过,则将为新生对象分配内存,对象所需内存的大小在类加载完后就可确定,有两种方式,第一种是“指针碰撞”,指针碰撞方式指在分配内存时,就像一边是用过来的,一块是没用过的,中间有一个指针是分界线,在需要新分配一块内存时,指针就像空闲内存这块移动与所需内存相同大小的距离,当然需要java内存是规整的。第二种方式是”空闲列表”,就是如果java内存不是规整的,就是空闲内存和已用内存是交错的,那么肯定不能使用“指针碰撞“,此时需要一个列表来记录哪些是空闲内存块,在分配内存的时候就找到相同大小的块去分配,然后在列表中更新就OK了。这两种方式的采用取决于java内存是否规整,java堆是否规整又是取决与垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial,ParNew带Compact(复制)算法的过程的收集器的时候,如CMS垃圾收集器采用的是标记-清理算法就采用”空闲列表“。
注意,对象创建如果在虚拟机是非常频繁的行为,在并发情况下也不是安全的,可能出现在给A对象分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存,解决这两个问题,一种是对分配内存空间的动作进行同步处理,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种方式就是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。
对象的内存布局
对象在内存中存储的布局可以分为:对象头,实例数据和对齐填充。
对象头
存储两部分信息,第一部分用于存储对象自身的运行时数据,如哈希吗,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向时间戳等。第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
存储的是对象真正的有效信息,包括程序代码中所定义的各种类型的字段,包括从父类继承下来的,和在子类中定义的(才可以使用多态),存储顺序受虚拟机分配策略参数和字段在Java源码定义的顺序影响,Hotspot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oop(Ordinary Object Pointers),可以看出,相同字段的宽度总是被分配到一起,这也是我们在定义变量时要尽量采用最小变量的原因。父类的变量会出现在子类之前。弱CompactFields为true,那么子类中较窄的变量也可能会插入到父类变量的空隙之中。
对齐填充
并不是必须存在的,仅仅起着占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,而对象头部分正好是8字节的背书,因此,当对象实例数据部分没有对齐时,就需要对齐补充来补全。
对象的访问定位
使用对象就要访问对象,java程序通过栈上的reference数据来操作堆上的具体对象,根据虚拟机的具体实现,现在主流的访问方式有两种,句柄与直接指针。
句柄的方式会在java堆中划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包括了对象实例数据与类型数据各自的具体地址信息。
直接指针就是reference直接指向堆中具体的对象
句柄最大的好处是reference存储的是稳定的句柄地址,对象被移动只会改变句柄中的实例数据指针,直接对象访问就是速度快,节省了一次指针定位的时间开销。