对象创建
虚拟机遇到 new 指令,检查指令参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用的类是否被加载,解析和初始化过,若没有,执行响应的类加载过程。类加载检查通过后,为新生对象分配内存。对象所需内存大小在类加载完成后便可以确定。
内存分配两种模式:
① 指针碰撞: 若堆中的内存是绝对规整的(垃圾回收采用标记整理算法),所有用过的内存放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,分配内存时仅仅时把指针指向空闲空间那边挪动一段与对象大小相等的距离。
② 空闲列表: 若堆中的内存不是规整的(垃圾回收采用标记清除算法),已使用的内存和空闲内存相互交错。虚拟机维护一列表记录哪些内存块是可用的,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。(缺点:产生垃圾碎片)
选择哪种分配方式,由收集器决定。如Serial,ParNew 等带Compact过程的收集器,采用指针碰撞分配算法,而使用CMS这种基于Mark-Sweep算法的收集器,采用空闲列表。
保证修改指针在并发环境下安全的的两种策略:
① 采用CAS配上失败重试保证更新操作的原子性。
② 把内存分配的动作按照线程划分在不同空间上进行,即每个线程在堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完分配新的TLAB时,才需要同步锁定。
内存分配完成后,将分配到内存空间初始化零值(不包含对象头)。所以,在java中,实例字段时不需要赋值的,有默认零值。执行类构造器时再做具体初始化过程
对象访问
栈引用类型在Java虚拟机规范中只定义了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的定位方式有使用句柄和直接指针两种。
使用句柄最大的好处是栈中的引用是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针;直接内存的最大好处是速度快,节省了一次指针定位的时间开销。
① 使用句柄:Java堆中将会划分一个句柄池,栈中存放的就是句柄地址,句柄中包含了实例数据与类型数据各自的地址信息。
② 使用直接地址:栈中的指针直接就是对象的地址。
对象内存布局
在HotSpot中,对象在内存中存储的布局可分为3部分:对象头,实例数据, 对齐填充。
对象头
① 存储对象自身运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
② 类型指针,对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象时哪个类的实例。
实例数据
存储对象的有效信息,程序中定义的各种类型字段内容。
存储顺序受虚拟机分配策略参数(FieldsAllocationStyle)和字段在程序中定义的顺序影响。HotSpot默认策略为long/double,int, short/char, byte/boolean, oop 引用,相同宽度的字段总是被分配到一起。
对齐填充
起着占位符的作用。HotSpot 自动内存管理要求对象起始地址必须是8字节的整数倍,当实例对象没有对齐时,就需要对齐填充来补全。
采用对齐填充,寻址时直接取8字节的倍数,不用计算所占的字节数,提高性能。
参考资料:《深入Java虚拟机》 第2版 机械工业出版社