Java虚拟机之对象的创建、内存布局和访问定位
一、对象的创建
在Java运行的过程无时无刻都有对象的创建,也存在的对象的销毁或者回收。
一个对象创建的过程:
当虚拟机遇到new指令时,首先要查看要创建的对象所属的类有没有被加载过,如果未被加载,就先去加载类,在类加载检验通过后,就开始为新生的对象分配内存空间,你可能会问对象还在创建中为啥就分配内存空间了呢? 其实在类的记载过程中,该类的实例对象所需的内存空间就已经确定了。
内存空间分配时会根据堆的规整与否来使用不同的分配方式。
当堆内存中的内存空间是规整的,也就是说违未被使用的内存区域在堆得一边,已经被使用的内存去域在堆得另一边,这时中间放一个指针就可将堆内存的使用过的内存区域和未使用过的内存区域分隔开,当给新对象分配内存时,就向未使用的内存空间位置移动对象大小的空间长度,这种分配方式称为“指针碰撞”。
当堆内存的内存空间是非规整的,也就是已经使用的内存和未被使用的内存混杂在一起时就不能使用指针碰撞的方式为对象分配空间,而是需要维护一张列表,列表中记载着哪些空间已被使用,哪些空间未被使用,当给新对象分配内存时,就去查看堆内存空间中哪些未使用的内存空间可以容纳下该对象,就将该内存空间分配给新对象,这种分配方式称为“空闲列表”。
到底是用哪种分配方式要根据对内存的规整程度来选择,这里其实堆内存的规整是和垃圾回收有关的,具体来说就是跟某些垃圾回收器有关,归根到底是和垃圾回收的算法有关,堆内存的垃圾回收有 标记清除算法,标记复制清除算法,标记整理清除算法。
标记整理算法:就是当虚拟机发现栈中不在存在该对象的引用时就将对象标记为可回收状态,当内存不足需要回收时就清除垃圾对象,因为删除的对象的所在的内存中的位置是随机的,因此在多个对象被回收后会出现大量的零碎空间,也即是堆内存的不规整。
标记复制清除算法:堆内存会分成大小相等的两块区域,一块区域用于给对象分配空间,另一块用于存活对象的复制,当虚拟机发现栈中不在存在该对象的引用时就将对象标记为可回收状态,当内存不足时,会将存活的对象复制到另一块区域中,然后将原来的区域清空,这样堆内存是规整的,。
标记整理清除算法:当虚拟机发现栈中不在存在该对象的引用时就将对象标记为可回收状态,当内存不足时,将存活的对象向堆的一端移动,然后清除掉端边界以外的内存空间,这种垃圾回收算法也是规整的。
具体垃圾回收请看 https://blog.****.net/IPI715718/article/details/84430110
二、对象的内存分布
对象一旦创建就存在于对内存中,对象在内存中是怎么存在的呢?
对象在内存中的分为三个区域:对象头,实例数据,对齐填充。
1. 对象头(对象头又分为运行时数据和类型指针)、
运行时数据(官方称为Mark word)存储着对象的 哈希码(hashCode),GC分代年龄,锁状态标志,线程所持有的锁(对象此时为线程对象),偏向线程ID,偏向时间戳等。如果是数组对象还会存储数组的长度。
在Mark Word 在内存中个存储内容在32位JVM和64位JVM中所占得位数不同。
根据markOop.cpp中的代码可以看出。
32 bits:
--------
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
64 bits:
--------
unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)
unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
[ptr | 00] locked ptr points to real header on stack
[header | 0 | 01] unlocked regular object header
[ptr | 10] monitor inflated lock (header is wapped out)
[ptr | 11] marked used by markSweep to mark an object
对象头的另一块区域--类型指针,他指向了方法区中的类元数据,目的是为了确定这个对象是哪个类的实例。
2. 实例数据
实例数据才是存储对象的真正的有效信息,也是在程序代码中定义的各种类型的字段内容,其中包括基本数据类型的信息和引用类型的实例的地址。 其存放的顺序是和虚拟机的分配策略有关,例如默认的分配策略longs/doubles、ints、shorts/chars、bytes/booleans、oops,可以看出这种策略是将等长度的数据放在一块。
3. 对齐填充
JVM规定对象的大小必须为8字节的整数倍,也即是说必须是64,128.192 ......位,当数据不能满足为8字节的整数倍时,就进行对齐填充。
三、对象的访问定位
对于对象的访问定位分为两种 1. 句柄方式。 2. 指针方式。
句柄方式:就是讲堆内存划分出一个句柄池和实例池,栈中的对象引用存储的是句柄池中对象的句柄地址,句柄中存储的指向对象实例数据的指针和指向对象的类型的指针。
直接指针方式:引用直接指向对象的实例数据和类型指针,类型指针指向对象类型。