1:Java对象的创建
- 判断是否已经执行类加载
当虚拟机遇到一条new指令时 ,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
- 内存分配
当已经执行过类加载过程后,会为新对象在Java堆中分配一个大小已经确定的内存,具体的内存分配规则有两种:
- 指针碰撞(Bump the Pointer)
如果Java堆中的内存是绝对规整的,所有用过的内存放一边,空闲的内存放到一边,中间放着指针为分界点,分配内存就是把指针向空闲的一边挪动一段与对象大小相等的距离。见图1 - 空闲列表 (Free List )
如果Java堆中的内存并不是规整对的,已使用的内存和空间相互交错,虚拟机会将可以用的内存维护到一个列表上,在分配内存时从这个列表中找到一块足够大的空间划给对象。然后更新列表记录。
图1
Java堆中的内存是否是规整的是根据虚拟机所采用的垃圾收集器是否带有压缩整理功能决定的。Serial、ParNew带压缩整理的分配内存用指针碰撞,CMS这种通常用空闲列表方式分配内存
防止并发
在虚拟机上创建对象是非常频繁的行为,所以要做到防止并发,有以下两种方式可实现:
- 堆分配内存空间的动作进行同步处理,实际上JVM采用CAS(Cmpare And Set)配上失败重试的方式保证更新操作的原子性;
- 把内存分配的动作按照线程划分在不同的空间之中进行,即为每个线程在java堆中预先分配一块小内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB)。分配内存时在线程的TLBA上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。JVM是否使用TLAB可以通过-XX:+UseTLAB参数来设定。
初始化对象内存空间
内存分配完成后,JVM将分配到的内存空间都初始化为零值(不包括对象头)。
对象头的设置
将对象的类、哈希码、对象的GC分代年龄等信息设置到对象头之中。
对象的内存布局
创建完对象后,对象对分配给自己的内存是如何布局的呢?下面来介绍一下。
对象在堆内存中的布局可分为三部分:对象头(Header),实例数据(Instance Data),对齐填充(Padding)。
对象头:对象头包含两部分,第一部分存储自身运行时数据,如哈希码,GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等,官方称为“Mark Word”。
第二部分是类型指针,即对象指向它的类元数据的指针,通过此指针来确定是哪个类的对象。
实例数据:存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints、 shorts/chars,bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到 ⼀起,便于之后取数据。⽗类定义的变量会出现在⼦类定义的变量的前⾯。
对齐填充:并不是必然存在的,当对象实例数据部分没有对齐时,进行对齐补全。分仅仅起到占位符的作⽤
对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。reference数据只是一个指向对象的引用,具体的对象访问根据不同虚拟机有不同的实现,主流的访问方式有两种:使用句柄和直接指针。
使用句柄:使⽤句柄访问对象。即reference中存储的是对象句柄的地址,⽽句柄中包含了对象实例数据
与类型数据的具体地址信息,相当于⼆级指针。
直接指针:直接指针,就是指reference中直接存储对象的地址。但是Java堆对象的布局中就必须考虑如何防止访问类型数据相关信息。直接指针访问对象。即reference中存储的就是对象地址,相当于⼀级指针。
对⽐
垃圾回收分析:⽅式 1(使用句柄)当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址,所以当垃圾回收的时候就直接需要修改对象句柄的地址就可以了;⽅式2(直接指针)垃圾回收时需要修改reference中存储的地址,在垃圾回收的时候要判断实例数据是否存在,是否被垃圾回收过等做一系列判断。所以说垃圾回收的效率⽅式2高于⽅式 1。
访问效率分析,⽅式2优于⽅式1,因为⽅式2只进⾏了⼀次指针定位,节省了时间开销, ⽽这也是HotSpot采⽤的实现⽅式。