在之前的文章中,我们介绍了类加载的过程和 JVM 内存布局相关的知识。本篇我们综合之前的知识,结合代码一起推演一下对象的真实创建过程,以及对象创建完成之后在 JVM 中是如何保存的。
在 Java 中,创建对象的方式有很多种,比如最常见的通过new xxx()
来创建一个对象,通过反射Class.forName(xxx).newInstance()
来创建对象等。其实无论是哪种创建方式,JVM 底层的执行过程是一样的。
对象的创建过程,可以用如下图来简要概括。
创建对象大致分为 5 个步骤:
下面我们一起来看下每个步骤具体的工作内容。
当需要创建一个类的实例对象时,比如通过new xxx()
方式,虚拟机首先会去检查这个类是否在常量池中能定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化,如果没有,那么必须先执行类的加载流程;如果已经加载过了,会在堆区有一个类的 class 对象,方法区会有类的相关元数据信息。
为什么在对象创建时,需要有这一个检查判断?
主要原因在于:类的加载,通常都是懒加载,只有当使用类的时候才会加载,所以先要有这个判断流程。
关于类的加载过程,在之前的文章中已经有所介绍,有兴趣的朋友可以翻看之前的文章。
类加载成功后,虚拟机就能够确定对象的大小了,此时虚拟机会在堆内存中划分一块对象大小的内存空间出来,分配给新生对象。
虚拟机如何在堆中分配内存的呢?
主要有两种方式:
下面我们一起来看看相关的内存分配方式。
如果内存是规整的,那么虚拟机将采用指针碰撞法来为对象分配内存。
指针碰撞法,简单的说就是所有用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存时会把指针向空闲一方挪动一段,直到能容纳对象大小的位置。
如果垃圾收集器选择的是 Serial、ParNew 这种基于压缩算法的,虚拟机会采用这种分配方式。
如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用空闲列表法来为对象分配内存。
空闲列表法,简单的说就是在虚拟机内部维护了一个列表,会记录哪些内存块是可用的,在分配的时候会从列表中找到一块能容纳对象大小的空间,划分给对象实例,并更新列表上的内容。
如果垃圾收集器选择的是 CMS 这种基于标记-清除算法的,虚拟机会采用这种分配方式。
我们知道,虚拟机是支持多个线程同时分配内存的,是否会有线程安全的问题呢?
答案是:肯定存在的。比如用指针碰撞法时,虚拟机正在给对象 A 分配内存,但指针还没来及修改,此时又有一个线程给对象 B 分配内存,同时使用了原来的指针来分配,最后的结果就是这个区域只分配来一个对象,另一个对象被覆盖了。
针对内存分配时存在的线程安全问题,虚拟机采用了两种方式来进行处理:
以上就是虚拟机解决对象内存分配时存在的线程安全问题的措施。
初始化零值,顾名思义,就是对分配的这一块内存初始化零值,也就是给实例对象的成员变量赋于零值,比如 int 类型赋值为 0,引用类型为null
等操作。这样对象就可以在没有赋值情况下使用了,只不过访问对象的成员变量都是零值。
初始化零值完成之后,虚拟机就会对对象进行必要的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息,这些信息都会存放在对象头中。这部分数据,官方称它为“Mark Word”。
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头 (Header)、 实例数据 (Instance Data) 和对齐填充位 (Padding)。
以 32 位的虚拟机为例,对象的组成可以用如下图来简要概括。(64位的虚拟机略有不同)
各部分区域功能如下: