本文将学习对象是如何创建的, 对象的内存布局, 以及如何定位访问一个对象.
对象创建
当虚拟机碰到一个 new 指令时, 首先检查指令参数能否在常量池中定位一个类的符号引用, 并且检查该符号引用对应的类是否已经被加载, 解析和初始化. 当一切都确定完成后, JVM 就会为其分配内存 (需要分配的内存大小在现在就已经确定, 在 下面 中详细讲述).
对象的内存分配方式分为以下两种:
指针碰撞, 这种分配方式建立在堆内已用空间和剩余空间是完整的, 这样的话, 在两者之间放置一个指针作为分界点的指示器即可, 在分配空间时, 只需要移到一下指针位置就好了.
空闲列表, 如果 JVM 内的空间不是规整的, 那么就只能采用此方案了. 此时 JVM 会维护一个列表, 记录了哪些内存块是可用的, 在分配的时候划一个大小足够的区域给对象实例, 并更新列表即可.
以上两种方式采取哪种, 取决于 Java 堆是否工整, 而堆是否工整又取决于垃圾回收算法是否具有整理功能.
对象模型
前面说到对象在创建时就已经确定了内存大小, 那么 JVM 是怎么确定对象的大小呢? 对象在内存中又是如何存储的呢?
在 JVM 中 Java 的对象模型分为以下 3 块, 对象头, 实例数据, 对齐填充, 下面就让我们来分别介绍一下.
对象头
对象头的数据包括两部分. 一部分是用于存储自身运行时数据, 这部分数据被官方称为 "Mark World". 其中存储数据包括 Hashcode,GC 分代年龄, 锁状态标志, 线程持有锁, 偏向线程 ID, 偏向时间戳等等.
对象头的另外一部分是 类型指针, 即对象指向其类元数据的指针. 通过这个指针, 我们就可以知道该实例属于哪个类.
实例数据
实例数据就是对象真正存储的有效信息, 也就是代码中定义的各种类型的字段内容, 不论是父类的还是子类的, 都需要记录下来. 其存储顺序受到虚拟机分配策略和定义顺序影响.
对齐填充
对象填充不是必要数据. 在模型中只是起到占位符的作用. 因为 HotSpot 要求对象起始地址必须是 8 的整数倍, 这样在实例数据达不到要求的时候, 就需要通过对齐填充来补齐.
对象访问
对象访问的方式是通过引用来定位, 访问. 但 JVM 规范并没有强制要求该通过何种方式使用引用, 因此具体实现还是要依赖与具体虚拟机类型.
不过目前的主流访问方式就是以下两种.
使用引用. 其在 Java 堆中会独立创建一个句柄池, 引用指向句柄, 而句柄指向实例数据和类型数据.
使用这种方式来访问的优点是稳定, 例如在 GC 后, 实例数据需要移动, 那么只需要修改句柄池中的内容即可, reference 指向的是稳定的位置, 缺点是这种方式需要二次定位, 速度较慢.
直接指针访问, 引用直接堆中对象地址, 堆中保存了实例数据和类型数据指针, 指针直接指向另外存储的类型数据.
使用这种方式的优点是访问实例数据快, 因为 reference 指向直接的对象, 省去了一次内存定位开销. 但缺点就是不够稳定, 在对象移动后, reference 也需要修改值.
具体采用何种, 不同的虚拟机有不同的实现, 因为两者各有千秋, 并没有强烈的优缺点, 因此不同情况不同处理即可.
总结
在本文中介绍了对象的本质模型是什么, 以及对象是如何创建和访问使用的, 与上文的 JVM 内存模型结合来看, 可以让我们了解内存泄露产生的原因, 有助于高效地理解使用 Java 的自动内存管理机制.
文章在公众号 "iceWang" 第一手更新, 有兴趣的朋友可以关注公众号, 第一时间看到笔者分享的各项知识点. 谢谢! 笔芯!
本系列文章主要借鉴自《深入分析 Java web 技术内幕》和《深入理解 Java 虚拟机 - JVM 高级特性与最佳实践》.
来源: https://www.cnblogs.com/JRookie/p/11192748.html