对象的创建
创建对象就是new一个对象
当jvm遇到new指令时:
- 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用
- 检查这个类是否被加载,如果没有,就要进行类加载
- 进行内存分配
- 赋零值,在内存分配完成之后对象被初始化零值。所以对象的实例可以不赋值就直接使用
- 设置对象,例如找到这个对象的类的元数据,对象的哈希码,对象的年龄和锁信息
- 执行<init>()方法,即java中类的无参构造,有参构造
分配内存问题
类加载之后,为对象分配内存(内存大小在类加载中可以完全确定),分配内存有两种情况:
- 指针碰撞法:堆中内存规整,使用过的内存放在一边,空闲的放在一边,中间放一个指针作为分界点指示器。
- 空闲链表法:内存不规整。虚拟机维护了一个列表,记录哪些内存块可用。
选择哪种方法由空间压缩算法决定,例如标记清除(只能用空闲指针)算法,标记复制算法,标记整理算法。
线程安全问题
例如线程A给对象A分配内存,指向了一个指针进行分配。同时,线程B在A指向但还没来得及更改指针时又分配了这个指针。
两种解决办法:
- 使用CAS失败重试保证更新操作的原子性。
- 把内存分配的动作按照线程分配在不同空间之上,即每个java堆中预先分配一块内存,优先在这块内存上分配空间,用完后才使用CAS分配内存。
对象的结构
- 对象头:
- MarkWork:存储对象自身的运行时数据,如哈希码,年龄,锁信息
- 类型指针:指向它的类型元数据的指针,通过它来确定它属于哪个类
- 如果对象是数组,还要有记录数组长度的数据。因为虚拟机可以通过元数据信息确定java对象的大小,但是数组长度不能确定
- 实例数据
- 对齐填充,任何对象的大小必须是8的整数倍
对象的访问定位
Java程序会通过栈上的reference数据来操作堆上的具体对象。即通过引用找到数据的具体位置,有两种主流访问方式:
句柄访问:
在堆中划出一部分作为句柄池,reference存储对象的句柄地址,句柄中包含对象的实例数据地址信息和对象类型数据地址信息。
好处:具有稳定的句柄地址,reference指向句柄地址不会变,对象被移动只需要改变句柄的指针。
指针访问:
可以通过reference直接访问对象实例,访问对象的类的数据的话还是需要指针。
好处:开销降低,指向实例数据的话只要一次指向。