对象的创建过程
1.当虚拟机遇到new指令时, 首先将去检查这个指令参数能否定位到常量池对应的类的符号引用, 并检查这个符号代表的类是否被加载验证解析过, 如果没有先执行上述过程
2.类加载检查通过后, 接下来虚拟机将为新生对象分配内存, 对象所需的内存大小在类加载完成后便可完全确定
3. 对象在堆中分配内存的方式分为两种, 指针碰撞和空闲列表, 取决于你采用的垃圾收集器的算法实现是否有内存碎片化情况出现, Serial和ParNew等带Compact过程的收集时, 采用的分配算法是指针碰撞, 而是用CMS这种基于Mark-Sweep算法的收集时, 通常采用空闲列表
4.由于堆中大部分内存是线程共享的, 而且创建对象发生的一般都很频繁, 所以一般需考虑并发情况, 虚拟机采用的是CAS方式保证更新操作的原子性, 另一种是TLAB即本地线程缓冲区, 将创建内存的线程隔离开来使线程独立, 这种方式TLAB空间容量有限制, 所以当空间用光之后, 仍需要同步锁
5.内存分配完成后, 虚拟机将初始化的内存空间都初始化为零值
6.接下来, 虚拟机对对象设置一些信息, eg: 此对象是哪个类的实例, 如何才能找到类的元数据信息, 对象的哈希码, 对象的GC分代年龄信息(可通过参数设置阈值), 是否启用偏向锁等, 这些信息存在对象头
到此, 从虚拟机的视角看来, 一个对象已经产生, 而从Java程序角度看, 对象创建才刚刚开始, 字节码的角度看, 也就是new指令执行, 紧接着需要执行<init>方法, 将对象按照客户端程序的意愿进行初始化, 执行后一个对象才算真正创建完成
对象的内存布局
对象在内存中大致分为三个部分: 对象头, 实例数据, 对齐填充
1.对象头: 又分两部分信息, 第一部分用于存储对象自身的运行时数据, 如哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳等等, 另一部分是类型指针, 即对象指向它的类元数据的指针, 但并不是所有的虚拟机实现都在对象头上保留这个指针, 或者说, 查找对象的元数据信息并不一定经过对象本身
2. 实例数据: 也就是程序代码中定义的各种类型的字段内容, 无论是从父类中继承的还是子类中存在的都需要记录下来, 这部分的定义顺序收到虚拟机分配参数策略和Java源码中定义顺序的影响, HotSpot虚拟机默认的分配策略是longs/doubles, ints, shorts/chars, bytes, booleans, oops, 在满足这个条件的前提下, 父类中出现的变量会出现在子类前
3.对齐填充: 由于HotSpot要求对象的大小必须是8字节的整数倍, 而对象头的部分正是8字节的一倍或者两倍, 所以当实例数据没有对齐时, 需要通过对齐补充来不补全
对象的访问定位
Java程序中通过栈上的refernce数据来操作堆上的具体对象, 方式一般有两种
1.句柄: 这种方式在Java堆中会划出一块内存作句柄池, 其中包含每个对象的两种指针分别是: 到对象实例数据的指针和到对象类型数据的指针
优点: 在栈中的refernce存储的是稳定的句柄地址, 对象被移动时只会改变句柄中的实例数据指针, 而refence本身不改变

2.直接指针: reference中存的直接就是对象地址
优点: 速度更快
