对象的创建
在Java中,对象的创建过程大致可以分为以下几个步骤:
- 类加载检查: 当Java虚拟机遇到一条new字节码指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用对应的类是否已经被加载,解析和初始化过。如果没有,先要进行类加载过程
- 内存分配: 类加载检查通过后,接下来虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以确定。如果内存空间是规整的,则通过指针碰撞来为对象分配内存,未被使用和已被使用堆内存分开来放置在两侧,中间通过一个指针来作为分界的指示器,分配内存时只需将分界指针往未使用内存空间移动出对象所需空间即可。如果内存空间是不规整的,则通过空闲列表来分配内存,虚拟机维护一个列表来记录哪些内存空间是未使用的,分配时在列表中找一个足够大的空间划分给该对象
- 零值初始化: 内存分配完成之后,虚拟机必须把分配的内存区域(对象头除外)都初始化为零值。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值
- 设置对象头: 进行对象的对象头设置,设置类型指针指向方法区的类型元数据,java虚拟机通过这个指针可以知道这个对象是哪个类的实例。设置mark word值,涵盖对象的hashcode,GC分代年龄和锁标识
- 执行init方法: 执行对象内部生成的init方法,初始化成员变量值,同时执行搜集到的{}代码块逻辑,最后执行对象构造方法。
内存分配
- 内存规整则使用指针碰撞方式:所有用过的内存放在一边,没有使用过的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把指针向空闲的内存空间移动与对象大小相等的距离
- 内存不规整则使用空闲列表方式:内存空间相互交错,虚拟机就必须维护一个列表,记录哪些内存区域是可用的,在分配时从列表找到一块足够大的内存分配给对象实例,并更新列表上的记录
内存分配线程安全
虚拟机创建对象是非常频繁的操作,有可能出现正在给A分配内存,还未来得及移动指针,对象B同时使用了原来的指针来分配内存的情况。
- 对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
- 另一种方案是把内存分配的动作按照线程划分在不同的空间之中执行,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存,哪个线程需要分配内存就在哪个线程本地线程分配缓存上分配,只有线程本地线程分配缓存用完并分配新的缓存时,才需要同步锁定
对象的内存布局
对象头
对象头包括三部分:MarkWord、元数据指针、数组长度。
- MarkWord:用于存储对象运行时的数据,好比 HashCode、锁状态标志、GC分代年龄等。这部分在 64 位操作系统下占 8 字节,32 位操作系统下占 4 字节。
- 指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。
这部分就涉及到指针压缩的概念,在开启指针压缩的状况下占 4 字节,未开启状况下占 8 字节。 - 数组长度:这部分只有是数组对象才有,若是是非数组对象就没这部分。这部分占 4 字节
实例数据
储存对象实例数据,也是在程序代码中定义的各种类型的字段内容,无论是父类还是子类都会记录
对齐填充
没有特殊含义,仅仅起着占位符的作用。
那么为何非要进行 8 字节对齐呢?这样岂不是浪费了空间资源?
由于 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好也是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行的情况,这叫做 缓存行污染
在32位虚拟机中,一个空的Java对象最少占用8个字节,在64位虚拟机中,如果开启了指针压缩就是Mark World8个字节,指针4个字节,再加上对齐填充4个字节,一共16个字节。
对象的访问定位
句柄访问
Java堆中将会划分一快内存来作为句柄池,虚拟机栈中的引用存储的就是对象的句柄池地址,句柄中包含了对象的实例数据与类型数据各自的具体地址,在对象移动的时候只更改句柄池内地址,引用本身不需要更改。
指针访问
虚拟机栈引用存储的直接就是对象地址,指针访问速度更快,节省了一次指针定位的时间