上篇文章简单介绍了Java虚拟机运行时数据区之后,我们大致了解到了虚拟机内存的概况,下面我们就一起看一下对象的创建,布局以及访问过程。
对象的创建
Java是一门面向对象语言,在Java程序里无时无刻都有着对象被创建。通过Java语法创建对象通常就是通过new 关键字。
通过下面例子我们探讨一下对象创建的过程:
Object ob=new Object();
当虚拟机遇到一条new指令时
1)首先回去检测这个指令的参数是否能够在常量池中定位到一个类的符号引用,检查这个符号引用代表的类是否已被加载,解析,初始化。
2)如果没有,则先执行类加载过程。 (类加载机制以后总结)
内存分配
如果类加载检查通过后,虚拟机接下来就会为新生对象分配内存,对象所需内存的大小在类加载完成之后就确定下来了,为对象分配空间实际就是把一块去定大小的内存从Java堆里划分出来。
根据堆中内存是否是规整的有两种不同的分配方法,指针碰撞和空闲列表
指针碰撞
如果堆中内存空间是规整的,就可以通过指针碰撞的方式分配内存。
指针碰撞是通过把所有空闲内存放一边,使用过的内存放在另一边,中间放着一个指针作为一个分界点,分配内存只是把指针向空闲内存移动一段与对象大小相等的距离。
空闲列表
当堆中的空间不是规整的时候,虚拟机就需要去维护一个可用内存空间的列表,当为对象分配内存时,从列表中找到一块足够大的空间划分给对象实例,并更新列表。
如下图:假设宽度代表内存大小
选择哪种方式是由内存空间是否规整来决定的,而内存空间是否规整又是由采用的垃圾收集器是否带有压缩整理功能决定。
线程安全
除了如何划分空间之外,还需要考虑在并发情况下线程安全的问题,可能出现正在给A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
两种解决方案 :
采用 CAS配上失败重试方式保证更新操作的原子性
什么是CAS 可以参考这篇文章: https://blog.csdn.net/QuinnNorris/article/details/80965959
本地线程缓存区(TLAB)
为每个线程在Java堆中预先分配一小块内存,即本地线程缓存区。分配内存时就在TLAB上进行内存分配,只用当TLAB中的空间用完并需要分配新的TLAB时,才需要同步锁定。 虚拟机是否使用TLAB,可以通过参数来设定。
初始化
初始化零值
内存分配完成后,虚拟机需要将分配到内存空间都初始化为零值(不包括对象头),如果使用TLAB,初始化也可以提前到TLAB分配时进行,这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
对对象设置
对象是哪个类的实例,如果能找到类的元信息,对象的哈希码,对象的GC分代年龄。这些信息存放在对象的对象头(object header) 里.
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头,实例数据,和对齐填充。
对象头
对象头包括两部分信息:
1)第一部分用于存储对西那个自身运行时数据(哈希码,分代年龄,锁状态标志,线程持有实例,偏向线程ID,偏向时间戳等)
2)第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个数组,在头对象中还必须有一块用于记录数组长度的数据。
实例数据
实例数据是对象真正存储的有效信息,程序中所定义的各种类型字段的内容。
对齐填充
对齐填充并不是必然存在的,没有特别的含义,仅仅起着占位符的作用。
对象的访问
在Java语言中对象访问是如何进行的?
我们来看这样一段代码:
Object obj = new Object();
这段代码的执行会涉及 Java 栈、Java 堆、方法区三个最重要的内存区域。
假设该语句出现在方法体中, obj 会作为引用类型(reference)的数据保存在 Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”会在 Java 堆形成一块存储oject类型的结构化内存,保存该引用的实例化对象,Java 堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类型数据则保存在方法区中。
由于 reference 类型在 Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到 Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
使用句柄
Java堆中会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示:
直接指针
使用直接指针的方式,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址,如图:
这两种对象访问定位方式有各自的优势:
使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,reference本身不需要修改
使用直接指针访问的最大好处就是速度快,节省了一次指针定位的时间开销。