作者:muggle
对象的创建
在语言层面上,创建一个对象通常是通过new关键字来创建,在虚拟机中遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过;如果没有的话就会先加载这个类;类加载检查完后,虚拟机将会为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,在堆中为对象划分一块内存出来。
虚拟机给对象分配内存的方式有两种——“指针碰撞”的方式和“空闲列表”的方式。如果java堆内存是绝对规整的,所有用过的内存放在一边,未使用的内存放在另一边,中间放一个指针作为指示器,那分配内存就只是把指针向未使用区域挪一段与对象大小相等的距离;这种分配方式叫指针碰撞式,如图1所示。
我们知道,堆内存随时都可能被垃圾收集器回收的,当内存被回收后堆内存就可能不是连续的了,所以当采用指针碰撞的方式时,垃圾收集器必须有内存整理的功能,能对垃圾回收后的零散内存进行整理。而空闲列表的方式则不需要垃圾收集有这个功能,采用这种方式时虚拟机会维护一张表,用于记录那些内存是可用的,当需要分配内存时就从表中找出一块足够的内存进行分配,并记录在表上。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化;接下来虚拟机会对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希值、对象的GC的分代年龄等信息。这些信息存在对象的对象头之中。完成这些工作后,从虚拟机的角度来看一个新的对象就产生了,但从程序的角度来看对象创建才刚刚开始,对象尚未执行初始化方法,各个字段都还未赋值,接下来会执行初始化方法,只有在执行初始化方法后,一个真正可用的对象才算是被创建。
对象的内存
在HotSpot虚拟机中,对象在内存中分为三块区域:对象头、实例数据、和对齐填充。对象头包括两部分信息,第一部分用于存储对象自身运行的运行时数据,如哈希码、GC分代年龄、锁状态标志线程持有的锁等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
。接下来的实例数据部分是对象真正存储的有效信息,也是在代码中所定义的各个字段的内容。这些字段无论是在父类那继承过来的还是子类里定义都要记录下来。第三部分对齐填充不是必然存在的,它仅仅起占位符的作用,用以填充内存。
对象的访问定位
建立对象是为了使用对象,我们的java程序需要通过栈上的reference来操作堆上的对象。通过reference来访问对象的方法有两种——使用句柄和直接指针。在虚拟机执行一个方法时,虚拟机栈 中会为方法分配一个 局部变量表,一个操作数栈;局部变量表是用于保存函数的参数以及局部变量的,其保存的类型有boolean、byte、char、short、int、float、reference和returnAddress八种;方法在执行的过程中,会有各种各样的字节码指令往操作数栈中执行入栈和出栈操作,完成数据的运算。基本数据类型直接存储到变量表中。那reference是如何找到引用的对象的呢?
如果使用句柄的话,那么会在java堆中划分一块内存来作为句柄池,reference中存储的是句柄的地址,而句柄中包含了对象的具体地址信息,如图2所示
如果使用直接指针访问,那么java堆对象的布局则如图3所示;
小结
对象的内存分布使用情况就介绍到这,感兴趣的小伙伴可以自己画一画当虚拟机执行递归方法时的堆栈运行状况的示意图。