说明:虚拟机我们讨论的是常用的HotSpot虚拟机,即,Sun JDK和Open JDK中带的虚拟机
Java是一门面向对象的编程语言,在语言层面上,创建对象通常是通过一个new关键字来实现,如,Student stu = new Student();来创建了一个学生对象。
那么在虚拟机中,对象(仅限于普通的Java对象,不包括数组和Class对象等)创建的过程是怎么样呢?
流程如下:
1. 检测该类是否被加载
当虚拟机遇到一条new指令时,首先会检查这个指令的参数,是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
2. 为新生对象分配内存
对象所需要的内存大小在类加载完成后便可以完全确定
对象内存空间分配的两种方式:
① 指针碰撞方式分配内存: 要求Java堆中的内存是绝对规整的,即,所有用过的内存都放在一边,空闲的放在另一边,中间放着一个指针作为分界点的指示器,新建对象需要分配内存时,其实就是把这个指针向空闲内存一侧移动一段和对象大小相等的距离,类似于进度条,如下图所示:
浅绿部分是已经占用的内存空间,41%是指针指示器,剩下的空白是空闲的内存空间,而为对象分配空间大小,就是让指示器向空闲移动,比如42%
② 空闲列表方式分配内存:Java堆中的内存并不是规整的,即,已经使用的内存和空闲的内存相互交错,此时,虚拟机需要维护一个列表,记录哪些内存块是可用的,在分配的时候,从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
通过生活中的例子辅助理解:比如你和你女朋友到一家餐厅用餐,这个时候餐厅里已经有人在用餐了,但是还有一些空余的座位可以使用,这时门口的服务员就会安排你们到至少可以坐下两个人的座位用餐。其中,对应关系如下:
餐厅:整个堆内存空间
之前已经在用餐的人:被占用的内存空间
空余的座位:空闲的内存空间
门口服务员:记录空闲内存空间的列表
说明:
选择哪种分配方式由 Java堆是否规整决定,而Java堆是否规整,又由所采用的垃圾收集器是否带有压缩整理功能所决定
内存分配时的并发问题
对象在虚拟机中创建是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。
解决方案
① 对分配内存空间的动作进行同步处理,即,采用CAS+失败重试的方式,保证更新操作的原子性。
② 预先在Java堆中,为每一个线程分配一小块内存,即本地线程分配缓冲(TLAB)。只有TLAB用完并分配新的TLAB时,才需要同步锁定。可以通过-XX:+/-UseTLAB参数来设定是否要使用TLAB
3. 初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
比如,Student这个对象中,包含了姓名和年龄两个属性,大多人常常的写法如下:
private String name;
private Integer age;
并没有对name和age进行初始化赋值,但我们在使用的过程中并不回报错,这是因为虚拟机会帮我们进行初始化赋值,name默认空,age默认0。
在《effective Java》中,作者建言我们应当对实例属性进行初始化赋值。
4. 虚拟机对实例对象进行必要的设置
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
5. 执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚开始,<init>方法还没有执行,所有的字段都还为零。
一般来说,执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
归纳总结
在虚拟机中,创建一个对象的过程如下:
- 检测该类是否被加载
- 为对象分配堆内存空间
- 将分配到的内存空间都初始化为零值(属性赋值)
- 对对象进行必要的设置
- 执行<init>方法把对象进行初始化