在java中可以通过:
- new关键字
- 反序列化。ObjectInputStream.readObject()
- 反射。Class.newInstance()
- 对象克隆。Object.clone()
来创建一个对象
分配内存
当创建一个对象时,需要在堆中分配一块内存(当开启逃逸分析时,可能基于栈上分配标量替换等原因,直接在栈上分配),用于容纳此对象。从宏观上看,可以分为两部分:快速分配和慢分配。具体流程如下:
-
如果当前类还未加载,则会直接进入慢分配流程,先加载类,然后再执行对象的创建
-
如果当前方法已经被JIT编译为机器代码,切逃逸分析表明对象并未逃逸,且对象并不是太大,没有太多的实例字段,则会优先使用栈上分配,直接在当前方法栈上分配对象
-
如果未达到栈上分配的条件。则会优先使用TLAB。如果开启了TLAB(-XX:+UseTLAB,默认开启),即每个线程在Java堆的eden区中会预先分配一小块内存(默认是eden的1%),那么jvm会优先使用这部分内存,因为是线程私有,不需要cas或者锁操作,效率更高
-
如果TLAB条件也不满足,则会优先在eden区分配对象。此时,根据jvm所使用的gc有无Compact过程,分配内存的方式有两种:
- 如果gc后,会Compact,即将使用的内存放到一边,未使用的放到另一边。这种情况下,已使用和未使用的内存中会存在一个指针,分配指定大小内存仅仅就是将这个指针移动与对象大小相等的距离。这种方式称为指针碰撞
- 如果gc后不会Compact,此时jvm需要维护一个空闲列表,记录哪些内存是可用的,从可用的内存中找到一块足以容纳对象大小的内存,分配使用
由于内存分配是十分频繁的行为,所以jvm采用首先CAS机制保证分配的线程安全,如果失败则会采用互斥锁的方式。
-
如果eden分配失败(无空间)或者对象满足了直接进入老年代的条件,那就直接分配在老年代。
当内存分配完毕时,jvm会进行内存的清零操作,即将所有内存填充二进制的0.(如果是从TLAB分配的,则没有这一步,因为TLAB的内存已经清零了)
再之后,会设置对象的对象头,对象头中有一部分用于偏向锁的设置,如果开启了偏向锁,则会将分配内存的这个线程设置到对象头的偏向锁部分。
对象分配的完成流程如下:
对象的内存表示
在hotspot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(object header)、实例数据、对齐填充(padding)。
对象头包括两部分信息,第一部分用那个与存储对象自身的运行时数据,如锁信息、gc年龄等。这部分大小在32位和64位虚拟机中分别为32bit和64bit。官方称为 Mark Word;另一部分为类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也就是代码中一个类的实例字段,包括自己的和从父类继承的字段
padding并不是必须的,也无特殊含义,只是HotSpot要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。对象头部分的大小正好是8字节的倍数,因此当实例数据部分没有对齐时,就需要通过padding来对齐
参考
以上内容参考:
周志明:《深入理解Java虚拟机》
封亚飞:《揭秘Java虚拟机》