对象的创建
jvm遇到字节码new指令时 先检查该指令参数能否在常量池找那个定位到该类的符号引用(该类是否存在 找到了说明你定义过这个类,基本上可以消除编译错误了,没有就会有编译错误)并检查该符号代表的类是否已经被加载解析和初始化过 如果没有则先执行类加载过程 接下来分配内存(可直接确定大小)
分配内存就是把指针向空闲内存移动一段对象大小相同的距离 该分配模式成为“指针碰撞” (前提是内存绝对规整) (Serial ParNew等带压缩整理过程的收集器采用该分配模式)
所以虚拟机必须维护一个列表 记录哪块内存是可用的 划分给对象实例并更新列表的记录 这种分配方式称为 “空闲列表”(CMS这用基于清除算法的收集器采用空闲列表分配内存)
选择哪种分配方式由java堆是否规整决定 而是否规整由采用的垃圾收集器是否带有空间压缩整理的能力所决定
为了处理并发情况的线程安全
第一种方式:对分配内存空间的动作进行同步处理(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性)
另一种方式:把内存分配的动作按照线程划分在不同的空间进行 即每个线程在java堆中预先分配一小块内存 称为本地线程分配缓冲 分配新的缓冲区才需要同步锁定
接下来对对象进行设置 如哪个类的实例如何找到类的元数据 信息对象的哈希码 对象的GC分代年龄等
此时虚拟机的视角对象已经产生了 但是java程序视角来说才刚开始——构造函数还没执行 初始化后 一个对象才算完全被构造出来
对象的内存分布
HotSpot中对象在堆内存的存储布局分为三个部分:对象头 实例数据 对其填充
对象头包括两类信息
第一类用于存储对象自身的运行时数据(如哈希码 GC分代年龄 锁状态标志 线程持有的锁等)官方称为“Mark Word”(有动态定义的数据结构 以便在极小的空间内存存储尽量多的数据)
另一部分是类型指针 即对象指向他的类型元数据的指针 jvm通过这个指针来确定该对象是哪个类的实例
注意:并不是所有的虚拟机实现都需要在对象数据上保留类型指针 换句话说查找对象的元数据信息并不一定要经过对象本身
此外如果对象是数组 还需要一块用于记录数组长度的数据
实例数据 对象真正存储的有效信息 即在程序代码中定义的各种类型的字段内容 存储顺序受虚拟机分配策略参数和字段在java源码中定义的顺序影响
在该条件下父类定义的变量会在子类前面
对齐填充并不是必然存在的 起占位符的作用 任何对象的大小必须是8字节的整数倍 对象头已经被设置为8字节的整数倍 如果示例数据部分没有对齐则需要对其填充补全
对象的定向访问
java程序通过栈上的reference数据来操作堆上的具体对象 但是《java虚拟机规范》没有定义应通过什么方式去定位访问到堆中的对象位置 所以对象访问方式也是由虚拟机实现而定的 主流的访问方式主要是使用句柄和直接指针两种
句柄 java堆中将可能划分一块内存来作为句柄池 reference中存储的就是对象的句柄地址 句柄中包含了对象实例数据和类型数据各自具体的地址信息
直接指针 reference中存储的直接就是对象实例数据 如果只是访问对象本身就不需要多一次间接访问开销
区别:
句柄访问最大好处是reference中存储的是稳定句柄地址 在对象被移动时(垃圾收集时移动对象)只会改变句柄中的实例数据指针 而reference本身不需要被修改
直接指针最大好处是速度更快节省了一次指针定位的时间开销 对于HopSpot而言主要是用直接指针进行对象访问