java对象的内存分配
1.类加载检查
JVM接收new指令时,首先检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查该符号引用代表的类是否已加载、解析和初始化。若未加载,需先执行类加载过程。
2.内存分配
对象所需内存大小在类加载完成后便可确定,内存的分配方式取决于java堆是否规整,针对规整的内存JVM默认采用指针碰撞方式分配内存,不规整内存则采用空闲列表方式分配内存。
- 指针碰撞(Bump the Pointer):如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。该方法适用于内存规整的垃圾收集器(如Serial、ParNew)。其特点是通过移动指针来分配内存,简单高效。
- 空闲列表(Free List):,虚拟机就维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。该方法适用于内存不规整的垃圾收集器(如CMS)。其特点是通过维护空闲内存块列表来分配内存,开销较大。
无论采用指针碰撞还是空闲列表,都会产生内存分配的并发问题,针对该问题,JVM提供一下两种方式来保证内存分配的线程安全:
- CAS(compare and swap)+失败重试:通过CAS+失败重试机制实现同步内存分配,G1、Shenandoah收集器均采用此方法完成内存分配
- 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):为每个线程预先在堆中分配一小块私有内存(默认占Eden区的1%),分配时无需锁定,可大幅提高分配效率。可通过
-XX:+/-UseTLAB
参数控制,通过-XX:TLABSize 指定TLAB大小。
内存分配的位置默认在堆区的Eden区,除了以下两种情况:
- jvm是否开启逃逸分析和标量替换,若jvm开启逃逸分析(-XX:+DoEscapeAnalysis)和标量替换((-XX:+EliminateAllocations),且当前对象未逃逸的情况下JIT将会在栈上分配空间存储对象,同时将其成员变量拆解为基本类型(标量),直接存储于栈中;这样做的优势是对象会随栈帧出栈而自动销毁,不参与GC过程,减少GC压力
- 对象的大小是否符合JVM的大对象定义,若对象大小符合大对象定义,则对象会直接进入老年代
3.内存空间初始化
JVM将分配的内存空间初始化为零值(不包括对象头)。若使用TLAB,该操作可提前至TLAB分配时进行
4.设置对象头
将当前对象的哈希码、GC分代年龄、锁标识、元数据指针等放入对象头中。对象头结构为:
- Mark Word(8字节):哈希码、锁状态等。
- 类型指针(4/8字节):指向类元数据。
- 数组长度(仅数组对象有,4字节)。
5.执行<init>方法
执行<init>方法,为属性赋值和执行构造方法
最后需要说明对象分配内存的操作默认情况下在64位的系统中开启了指针压缩功能,这样做的目的是为了减少64位平台下内存的消耗。