Java对象的创建、存储、访问

对象的创建流程

Java是一个面向对象的语言,在程序运行中时时刻刻都有对象被创建,创建对象通常只是一个new关键字,当虚拟机遇到new 的指令的时候,首先会去检查这个这个指令的参数能否在常量池中对应到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过。如果没有的话会去先执行相应的类加载过程。

当类加载检查通过后,虚拟机将为新生成的对象在Java堆中划分一块内存空间,所需的大小在类加载完成之后就可以确定下来。

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一个工作也可以提前到TLAB分配时进行,这一步操作保证了实例字段在Java代码中可以不赋初始值就可以使用,程序都可以访问到这些字段的数据类型所对应的零值。

然后虚拟机会对对象进行必要的设置,例如这个对象是哪个类的实例,怎么样才可以找到这个类的元数据,对象的哈希码,对象的GC分代年龄等信息。这些信息都存放在对象头中(Object Header)之中根据虚拟机当前的运行状态的不同,对象头会有不同的 设置方式。

最后就是执行Java的init(),把对象按照我们的意愿进行初始化,这样一个真正可用的对象才算真正的生产出来。
Java对象的创建、存储、访问

内存分配方式

指针碰撞:假设堆中内存是绝对规整的,内存空间分为两块,一边是已经使用的,一边是未使用的,中间放着一个指针作为分界点的指示器,这种分配方式就是指针碰撞。
Java对象的创建、存储、访问

空闲列表:假设堆中的内存不是规整的,已经使用的内存和未使用的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表上,列表上面记录哪块内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象的实例,并且更新列表。

Java对象的创建、存储、访问
选择哪种方式根据堆内存是否规整决定,而Java堆是否规整又根据所采用的垃圾收集器是否带有压缩整理决定。Serial、ParNew等带Compact过程的收集器采用的是指针碰撞,使用CMS这种基于Mark-Sweep算法的收集器采用的是空闲列表。

问题

对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针指向的位置,在并发的情况下也不是线程安全的,可能出现正在给A对象分配内存,指针还未修改,对象B又使用原来的指针来分配内存,那么该怎么解决呢?

解决方案1:对分配内存空间的动作进行同步处理。实际上是虚拟机采用CAS(Compare And Swap,比较再交换)配上失败重试的方式保证更新操作的原子性。
解决方案2:把内存分配的动作按照线程进行区分,不同线程的分配在不同的内存空间中进行,也就是说每个线程在Java堆中预留一块空间,这种方式称为本地线程分配缓冲(Thread Local Allocation Buffer TLAB)。只有TLAB用完并且分配新的TLAB的时候,才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

对象在内存中的存储方式

在HotSpot虚拟机中,对象在Java堆中存储的布局分为3个,对象头、实例数据和对其填充。

对象头

HotSpot虚拟机的对象头分为两部分:
存储对象运行时的数据:存储对象自身的运行时的数据,例如:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分的数据长度在32位和64位的虚拟机中分别是32bit和64bit,官方称为“Mark Word”。对象在运行中的数据很多,往往都超过了32位和64位Bitmap结果所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储更多的信息,它会根据对象的状态复用自己的存储空间。

类型指针:对象头的另外一部分就是类型指针,即对象指向它类元数据的指针,虚拟机就是通过这个指针来确定这是哪个类的实例。

并不是所有的虚拟机实现都必须在对象数据上保留类型指针,也就是说查找对象的元数据不一定要经过对象本身。另外,如果对象的类型是数组的话对象头还得存数组的大小,因为虚拟机可以通过元数据信息确定Java大小,但是无法从数组的元数据中确认数组的大小。

实例数据

实例数据当中存储的是对象真正存储的有效信息,无论是从父类继承下来的还是在子类当中定义的,都会记录起来。这部分存储的顺序会根据虚拟机分配策略参数(FiledsAllcationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/duobles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),相同宽度的字段会被分配到一起。在父类中定义的变量会出现在子类之前,如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也有可能会插入到父类变量的空隙中。

对齐填充

对齐填充不是必然存在的,它仅仅是起着占位符的作用。HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数,换句话说就是对象的大小必须是8字节的整数倍,而对象头的部分正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐时就需要通过对齐填充来补全了。

对象的访问定位

Java程序找寻对象的时候是通过栈上的reference数据来操作堆上的具体对象,对象访问方式取决于虚拟机实现而定,目前主流的访问方式就是句柄和直接指针。

句柄访问:Java堆会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。句柄的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
Java对象的创建、存储、访问

直接指针:直接指针方式的话reference中存储的就是对象地址,所以Java堆对象就需要考虑如何对类型数据进行布局了。直接指针方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。

Java对象的创建、存储、访问