java是一门面向对象的编程语言,在java程序运行的过程中无时无刻不在产生新的对象。在语言层面,java普通对象的创建仅仅是一个new关键字而已,而在虚拟机中,对象的创建(不包括数组和Class对象)又是一个怎样的过程呢??
Step1:当虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
Step2:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可以确定了,为对象分配空间的任务等同于从java堆中划分出一块确定大小的内存空间。
java虚拟机分配内存空间的方式通常有“指针碰撞”和“空闲列表法两种”。假设java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放一个指针作为分界点的指示器,在需要分配新的内存的时候就把指针往空闲内存那边移动一段与对象大小相等的距离,这种分配方式称之为“指针碰撞”。假设Java堆中的内存是不规整的,已使用的内存和空闲的内存杂乱交错,那么就没办法进行简单的“指针碰撞”来分配内存,java虚拟机就必须维护一个列表来记录哪些内存空闲,在进行内存分配的时候从空闲列表中找到一块足够大的内存分配给对象实例,并更新列表,这种分配方式就叫做“空闲列表”。到底采用何种方式进行内存分配,取决于java堆内存是否规整,而java对内存是否规整又取决于java虚拟机所采用的垃圾回收器是否带有压缩整理功能。
另外,除了考虑如何划分空间之外,另一个需要考虑的问题是java对象的创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置在并发情况下也并不是线程安全的,可能正在给对象A分配内存,指针位置还没来得及修改,对象B又使用了原来的指针来分配内存。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即在每个线程在java堆中预先分配一小块内存,成为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAb时才需要同步锁定。
Step3:内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一过程也可以提前到TLAB分配时进行。这一步操作保证了对象的实例字段在java代码中可以不赋初始值就可以使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的hash码,对象的GC分代年龄信息等。这些信息存放在对象的对象头中。
Step5:在上面的工作完成之后,从虚拟机的角度来讲,一个新的对象已经产生了,但是从java程序的角度来讲,对象创建才刚刚开始(初始化方法还没有执行,所有的字段都还是零)。所以一般来说,执行new指令之后会接着执行初始化方法,把对象按照程序员的意愿进行初始化,这样一个正真可用的对象才算完全产生出来。
总结:5个步骤,两个注意点
检查类加载,分配内存,内存初始化为零值,设置对象头,执行初始化方法。
内存分配方式:“指针碰撞”,“空闲列表”
内存分配线程安全:对分配动作进行同步处理,使用TLAB方式分配