这里研究的是普通 java 对象,不包括数组和 Class 对象等。
1. 对象的创建
对象创建流程1.1. new
我们创建对象大多都是通过 new 关键字(复制、反序列化等情况除外),在虚拟机层面,当遇到一条字节码 new 命令后,创建对象的动作就开始了。
1.2. 类加载检查 & 类加载
如题,虚拟机在遇到 new 指令后,会先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且需要检查该类是否被夹在、解析、初始化过。如果没有,则必须限制性类的加载。
1.3 分配内存
通过类加载检查后,虚拟机姜维新生对象分配内存。对象所需的内存大小,在类加载完成后便可确定。
这里涉及到可用内存划分的问题,针对不同的垃圾收集策略有不同划分方法。
1.3.1 可用内存划分方法:
a. 指针碰撞
就是用一个指针,将内存隔开,一边是可用的内存,一边是不可用的内存。每当分配内存之后,就将指针移动对应的距离。
特点:需要内存是连续的空间,所以要求垃圾收集要有压缩整理的能力,例如 Serial、ParNew 等带有压缩整理的收集器,系统才用的是指针膨胀,简单高效。
b. 空闲列表
空间不连续的情况,就需要对可用空间进行记录。虚拟机会维护一个列表,记录着可用空间,空间被占用或释放,也要更新这张表。
内存划分还有个问题,线程安全问题。就按指针碰撞
来讲,只有一个指针,但是有多个线程,A 线程分配了内存,但还没有修改指针位置,B 同时使用原来的位置分配内存,肯定会冲突。解决方法:一种是对分配内存空间的动作进行同步处理(实际上采用的是 CAS 加失败重试);另一种是吧内存分配的动作按照线程划分在不同的空间之中进行,就是每个线程预先分配一小块内存(本地线程分配缓冲,Thread Local Allocation Buffer,TLAB),那个线程要分配内存,先用自己的 TLAB,用完了在分配新的 TLAB(此时才同步锁定,类似分段锁的思想)。可通过-XX: +/-UseTLAB
来控制虚拟机是否启用 TLAB。
1.4. 初始化内存空间
将内存空间(不包括对象头)都初始化为零值(如果启用了 TLAB,则这一步会提前至 TLAB 分配时进行),这个初始化只是保证对象的实例字段都有默认的初始值,但并不是按照代码中的进行初始化。例如:String 的成员变量赋值为 null,int 成员变量赋值为 0.
1.5. 必要设置
除了数据,虚拟机还要为对象进行必要的设置,例如这个对象是属于哪个类,如何才能找到类的元数据、哈希码、GC 分代信息等。这些存放在对象头。
1.6. 构造函数
1.4 中的初始化只是赋初值,但是将数据按照程序员意愿赋值,是在当前这一步做的,执行构造函数,即 Class 文件的 init() 方法。
2. 对象的内存布局
对象在堆内存中的布局可分为对象头
,实例数据
,对齐填充
。
2.1. 对象头
对象头包括两部分信息。
2.1.1. 对象自身 的运行时数据
官方称为 “Mark Word”,它存储的是对象自身
的运行时数据,如:
哈希码、
GC分带年龄、
锁状态标志、
线程持有的锁、
偏向线程ID、
偏向时间戳等。
对象的运行时数据很多,但这里只是存储对象自身
的运行时数据。
2.1.2 类型指针
指向类型元数据的指针,用来表示这个对象属于哪个类。但是并不是所有虚拟机都保存类型指针,因为查找类型元数据不一定要通过对象。
如果对象是数据,对象头中还需有一块空间用于记录数组长度。因为普通对象通过类的元数据就能确定长度,但是数组不行,需要额外的空间记录长度。
2.2. 实例数据
这部分是真正存储有效信息的,即程序里定义的字段内容,包括自身的和从父类继承的。
2.3. 对齐填充
这部分不是必然存在的,也没有特别含义,就是占位符。
HostSpot 自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,所以如果 (对象头)+(实例数据)!=(8 字节整数倍),就需要补齐。对象头已经被设置成 8 字节的整数倍,所以实际上是如果实例数据不满足条件,就需要补齐 。
3. 对象的访问定位
主要有句柄和直接指针两种。之前一直以为所有的对象引用都直接指向对象实例(也就是直接指针),原来还有个句柄的方式。。。。
句柄
句柄的优势,就是当对象实例移动时(例如GC),只需要改变句柄的指针,而 referrence 不需改变。
*疑问:句柄不会被回收、整理么?如果会,那 reference 指向的句柄不也是要变么?而且还多了一层,真的好么?
直接指针的优势,少了一层,速度快。
网友评论