JVM笔记(二.对象的创建与访问)
1.对象的创建
首先,当虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用(即类名),并检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,则先执行类的加载。
在类的加载检查通过后,合法了,就得给人家一个地方住,所以接下来要做的就是给新生对象分配内存。对象所需的内存大小在加载完成以后虚拟机就全部知道了,接下来就是划分内存的过程,基本上有这么两种方法:
1.java堆中的内存是绝对规整的,用过的内存放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,那么分配内存的过程就是将该指针从分界点移动所需的内存大小的长度到空闲区域,这种分配方式叫做指针碰撞;
2.如果java堆中的内存不是规整的,已经用过的和空闲的内存区域相互交错,那就没有办法用指针碰撞了,必须维护一个列表,记录哪些内存块可用,哪些内存块已经被使用,在分配的时候,从空闲的内存块中挑一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式就叫做空闲列表。
除了划分空间外,因为对象的创建是非常频繁的行为,所以需要对划分内存的行为考虑并发的情况,确保线程安全。解决方案有两种:一种是对分配空间的动作进行同步处理;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存TLAB,哪个线程要分配内存,就在哪个线程的TLAB上分配,互不干扰。
内存分配完以后,需要对对象做初始化,将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象实例在java代码中可以不赋初值就使用。
接下来虚拟机还要对对象做必要的设置,比如需要设置这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等,这些信息存放在对象的对象头中。
上述步骤都完成以后,对虚拟机来说,对象的创建就完毕了,但是对于程序员来说,对象的创建才刚刚开始,还需要自己做初始化的工作,将对象改装成自己想要的样子。
2.对象的内存布局
在虚拟机中,对象在内存中存储可以分为3块区域:对象头,实例数据和对齐填充。
其中对象头包括两部分信息:一部分是对象自身的运行时数据,如GC年龄,锁状态标志等等;另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针可以知道这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,即代码中定义的各个类型的字段内容。
至于对齐填充并不是必须存在,仅仅起着占位符的作用,比如HotSpot虚拟机自动内存管理系统的要求是对象的起始地址必须是8字节的整数倍,而对象头正好是8字节的倍数,所以当实例数据的部分没有对齐时,就需要对齐填充来补全。
3.对象的访问定位
对象的使用是通过掉用栈上的引用数据来操作具体对象的。目前主流的访问方式有通过句柄访问以及直接指针访问。
1.句柄访问:java堆中划分出来一块内存作为句柄池,栈的引用中存储的就是对象的句柄地址,而句柄中又包含了对象的实例数据与类型数据各自的地址,即包含了对象的引用和类的引用。如下图
2.直接指针:java堆中的对象实例数据中存放对象类型数据的指针,而栈的引用中存储的就是对象实例的地址。
这两种访问方式各有优势,使用句柄的好处就在于栈中引用存放的是稳定的句柄地址,在对象被移动时只会改变句柄中实例数据的指针,而栈中的引用不需要修改。
使用直接指针的好处就在于少了一步引用,速度更快。