JVM—对象的创建流程与内存分配
创建流程
对象创建的流程图如下:
对象的内存分配方式
内存分配的方式有两种:
- 指针碰撞(Bump the Pointer)
- 空闲列表(Free List)
分配方式 | 说明 | 收集器 |
---|---|---|
指针碰撞(Bump the Pointer) | 内存地址是连续的(新生代) | Serial和ParNew收集器 |
空闲列表(Free List) | 内存地址不连续(老年代) | CMS收集器和Mark-Sweep收集器 |
指针碰撞
指针碰撞示意图如下:
内存分配安全问题:
虚拟机给A线程分配内存的过程中,指针未修改,此时B线程同时使用了该内存,就会出现问题。
解决方式:
- CAS乐观锁:JVM虚拟机采用CAS失败重试的方式保证更新操作的原子性;
- TLAB (Thread Local Allocation Buffer)本地线程分配缓存,预分配。
分配主流程
首先从TLAB里面分配,如果分配不到,再使用CAS从堆里面划分。
对象如何进入老年代
对象进入老年代流程如下:
-
新对象大多数默认都进入Eden;
-
对象进入老年代的四种情况:
-
年龄太大 MinorGC15次【
-XX:MaxTenuringThreshold
】; -
动态年龄判断:MinorGC后会动态判断年龄,将符合要求对象移入老年代;
MinorGC之后,发现Survivor区中的一批对象的总大小大于了这块Survivor区的50%,那么就会将此时大于等于这批对象年龄最大值的所有对象,直接进入老年代。
例子: Survivor区中有一批对象,年龄分别为年龄1+年龄2+年龄n的多个对象,对象总和大小超过了Survivor区域的50%,此时就会把年龄n及以上的对象都放入老年代。希望那些可能是长期存活的对象,尽早进入老年代。 比率可以由-XX:TargetsurvivorRatio指定
-
大对象直接进入老年代1M【
-XX:PretenureSizeThreshold
】;(前提是Serial和ParNew收集器)为了避免大对象分配内存时的复制操作降低效率。
避免了Eden和Survivor区的复制。
-
MinorGC后存活对象太多无法放入Survivor。
-
空间担保机制
空间担保机制:当新生代无法分配内存的时候,我们想把新生代的老对象转移到老年代,然后把新对象放入腾空的新生代。此种机制我们称之为内存担保。
空间担保流程图如下:
对象内存布局
对象内存布局示意图如下:
对象里的三个区
堆内存中,一个对象在内存中存储的布局可以分为三块区域:
堆内存中,一个对象在内存中存储的布局可以分为三块区域:
-
对象头(Header) : Java对象头占8byte。如果是数组则占12byte。因为JVM里数组size需要使用4byte存储。
-
标记字段MarkWord:
-
用于存储对象自身的运行时数据,它是synchronized实现轻量级锁和偏向锁的关键。
-
默认存储:对象HashCode、GC分代年龄、锁状态等等信息。
-
为了节省空间,也会随着锁标志位的变化,存储数据发生变化。
-
-
类型指针KlassPoint:
- 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 开启指针压缩存储空间4byte,不开启8byte。
- JDK1.6+默认开启
-
数组长度:如果对象是数组,则记录数组长度,占4个byte,如果对象不是数组则不存在。
-
对齐填充:保证数组的大小永远是8byte的整数倍。
-
-
实例数据(Instance Data):生成对象的时候,对象的非静态成员变量也会存入堆空间
-
对齐填充(Padding) :JVM内对象都采用8byte对齐,不够8byte的会自动补齐。
如何访问一个对象
有两种方式:
-
句柄:稳定,对象被移动只要修改句柄中的地址
-
直接指针:访问速度快,节省了一次指针定位的开销