参考https://blog.csdn.net/hbtj_1216/article/details/77599990
《深入理解java虚拟机:jvm高级特性和最佳实践》
对象创建
限于普通对象,不针对数组、Class对象等。
当JVM遇到一条字节码new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过,如果没有,首先执行相应的类加载过程。
类加载检查通过后,jvm为新生对象分配内存。对象所需的内存大小在类加载完成后就可以完全确定,为对象分配空间实际上等同于把一块确定大小的内存块从java堆上划分出来。假设java堆中的内存是绝对规整的,没有用过的内存放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,这种通过指针的移动来分配空间的方式称为“指针碰撞”。如果java堆不规整,通过一个列表记录空闲的内存块,这种分配方式称为“空闲列表”。一般具有空间压缩整理能力垃圾收集器(如Serial、ParNew)等采用指针碰撞;而CMS这种基于清除算法的收集器时,理论上只能用“空闲列表”方法(不过CMS中设计了一个分配缓冲区,可以使用指针碰撞)。
针对高并发的情况,使用CAS保证并发修改指针的安全性,也有采用TLAB为每个线程单独分配缓冲避免冲突(当TLAB用完后才锁定同步)。
内存分配完成后,jvm必须将分配到的内存空间(不包括对象头)都初始化为零值,这部操作保证了对象的实例字段在java代码中可以不赋初始值就可以使用,使程序能访问到这些字段的数据类型所对应的零值。
接下来,JVM需要设置对象头,包括类指针、hashcode、GC分代信息等。
以上工作完成,从JVM角度看一个新的对象已经产生,但是从java程序的角度看,这个对象的创建才刚刚开始--构造函数,即Class文件中的
对象内存布局
对象在堆内存中的存储布局可以划分为对象头、实例数据和对齐补充。
对象头
对象头包括两类信息,第一类时markword,包含hashcode、GC分代信息、偏向线程信息、锁状态信息等;另一个信息为类型指针,指向对象类型元数据的指针,通过这个指针判断该对象是哪个类的实例,但并不是所有的虚拟机实现都需要在对象头上保留类型指针;此外队组数组对象,还会有一块用于记录数组长度的数据。
实例数据
实例数据是对象真正存储的有效信息,即我们在程序代码所定义的各类字段内容,存储顺序默认为longs/doubles、ints、shorts/chars、bytes/booleans、opps,相同宽度的字段总是被分配到一起,一般父类定义的变量在子类前面,如果设置了-XX:CompactFields参数为true是,子类中较窄的变量允许插入父类变量的空隙中,以节约资源。
对齐填充
并非必然存在,占位符作用。HotSpot要求对象起始地址必须是8字节的整数倍,对齐补充是用来对齐的。
对象的访问定位
java程序通过栈上的reference数据来操作堆上的具体对象。访问方式分为句柄和直接指针两种。
句柄访问方式
Java堆中将会划分出一块内存来作为 句柄池,reference中存储的就是对象的句柄的地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
优点:reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变。
缺点:增加了一次指针定位的时间开销。
通过直接指针访问对象(HotSpot使用的方式)
reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。对象实例数据中有指向对象类型数据的指针。
优点:节省了一次指针定位的开销。
缺点:在对象被移动时reference本身需要被修改。