JVM对象创建是指的java程序使用new操作符或者反射调用newInstance方法实例化对象时在JVM内存区域创建对象的过程,分配了对象的内存空间之后,JVM会给实例变量赋予初始化值,简要的图例如下:
简单来说整个过程就是对象创建会首先在Eden区进行内存分配,创建完成之后栈空间中的变量会对其进行引用。但是实际上整个过程远没有描述这么简单,下面来具体分析一下对象的创建过程。
首先创建的对象需要在Eden区分配内存空间,如果此时堆区的空间是完全规整的且连续的,那么创建的新对象就直接按照顺序分配到指定的位置上,然后通过指针移动就可以指向下一个新的未分配的位置,这种方式成为“指针碰撞”。但是如果堆区空间不连续,这个时候分配的新对象内存也是不连续的,这个时候就没有能力只通过指针移动指向来实现对象的内存空间分配,而必须开辟一个新的空间用于缓存当前整个堆区中未使用过的内存地址列表,通过这个列表来找到未分配的内存地址,这种方式成为“空闲空间”。是采用“指针碰撞”还是采用“空闲空间”是根据当前JVM堆空间的结构来进行的。以下图示阐述了两种方式创建对象时内存分配情况:
指针碰撞:
空间空间:
创建对象的并发问题:
上述两个过程为java对象在JVM内存结构中的详细创建过程,但是java程序天生就是多线程,所以对于创建对象来说会存在竞争(内存分配竞争),如果两个线程同时创建对象分配内存空间,就会出现相同的内存空间资源成为互相竞争的资源。JVM遇到这种情况有两种解决办法,一种的会采用CAS(compare and set)的方式进行内存的分配。另外一种称为TLAB(Thread Local Allocation Buffer)来解决竞争,这种方式是为每一个线程创建一个对应的堆空间,让分配内存的操作下放到TLAB中进行。
对象在堆区中的数据结构:
对象在堆区中主要有两部分组成,一部分是对象的头,一部分是实例数据。
对象的头包含了例如GC分代年龄, 对象的hash值,偏向锁的线程ID,轻量级和重量级锁(monitor)的指针等,这个部分也称为mark word,除此之外还有另外一个部分,这个部分是这个对象的类的指针,用于JVM知晓当前实例具体来自于哪一个类。
实例数据包括了对象真正存储的信息,包含本身字段的内容,从父类继承下来的内容等等,具体这里就不再详述。
对象的访问规则:
所谓的对象访问规则实际上指的是虚拟机栈中的本地变量如何引用对象实例的过程(如何通过引用找到实例本身),有两种方式可以实现,句柄池和直接引用。
句柄池:
在栈空间中并不直接获取指向堆区中对象实例的指针,而是通过在堆区中维护一个句柄池,通过句柄池的指针再指向最终实例对象,这种方式的好处在于栈空间的指针并不会随着对象的位置变动而发生变化,因为句柄池已经把这个变化给屏蔽掉了。
直接引用:
顾名思义就是没有维护中间引用而是直接在栈空间通过指针直接指向对象实例。
句柄池和直接引用这两种方式各有优劣,直接引用更加迅速,只有一次指针定位的开销,而句柄池更加稳定,对象被移动(垃圾回收)后也能够保证指针不变。
总结:对象的创建过程就是分配内存空间,赋予实例变量初始值并且建立与栈区变量的指针引用关系(通常来说这个关系应为GC Roots)