版权声明:本文来自 Crocutax 的博客 , 转载请注明出处 http://crocutax.com
Java是一门面向对象编程的语言,在Java程序运行过程中无时无刻都有对象被创建出来,那么在虚拟机层面,对象的创建到底涉及哪些方面?下面就从以下3个方面来了解下【对象】:
- 对象的创建
- 对象的内存布局
- 对象的访问定位
对象的创建
我们一般创建对象都是通过new的方式,而虚拟机在遇到一条new指令时,首先会去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析、初始化过。如果没有,则执行该类的加载过程。
类加载检查过后,对象所需的内存大小已经完全确定,虚拟机开始为对象分配内存,这里涉及两种内存分配方式:
方式1:指针碰撞
如果Java堆中的内存是规整的,使用中的内存放一边,空闲的内存放另外一边,中间放着一个指针作为分界点指示器,那么此时的分配内存其实就是把指针向空闲空间挪动对象大小的距离,这种分配方式成为“指针碰撞”。
方式2:空闲列表
如果Java堆中的内存是不规整的,已使用内存和空闲内存相互交错,那么此时虚拟机就需要维护一个列表,记录那些内存块是可用的,分配内存的时候从列表记录中定位到一块足够大的控件划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”。
内存分配的方式由Java堆是否规整决定,而Java堆是否规整由采用的垃圾回收器是否带有压缩整理功能决定。所以使用Serial、ParNew等带Compact过程的收集器时采用“指针碰撞”方式,而使用CMS这种基于Mark-Sweep算法的收集器时,则采用“空闲列表”方式。
内存分配过程中的并发问题解决
方案1:对分配内存空间的动作进行同步处理,即采用CAS配上失败重试的方式保证更新操作的原子性。
方案2:基于本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)将内存分配的动作按照线程划分在不同的空间之中进行。
对象初始化
默认初始化:
虚拟机对对象进行设置:对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄,是否启用偏向锁。。。
显示初始化:
经过上一阶段后,对象的所有字段还都是零,接下来会执行方法,进行显示初始化,之后该对象才能正常使用。
对象的内存布局
对象在内存中存储的布局可以分为3块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象头(Header)
该区域包含两部分。
第一部分:用于存储对象自身的运行时数据,例如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
第二部分:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
实例数据(Instance Data)
用于存储对象的有效信息,即代码中所定义的各种类型的字段内容。
存储顺序说道虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。默认分配策略为:
- 相同宽度的字段分配到一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields参数值为true(默认就是true),那么子类中较窄的变量也可能会插入到父类的变量空隙之中
对齐填充(Padding)
该区域仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是对象大小必须是8字节的整数倍,因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
虚拟机规范中只规定了通过一个指向对象的引用(reference类型)来访问对象,具体的对象访问方式由虚拟机的实现而定。目前主流的访问方式由两种:
- 句柄
- 直接指针
句柄访问
Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的内存地址值。
优点
reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时对象会频繁移动)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针
reference中直接存储对象地址值,对象中存储访问类型数据的指针 + 对象实例数据。
优点
速度更快,节省了一次指针定位的时间开销,鉴于对象的访问在Java中非常频繁,因此积累起来会是一项不小的性能优化。Sun HotSpot采用“直接指针”方式进行对象访问。