目录
本文以常用的虚拟机HotSpot和常用的内存区域java堆为例,研究HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
4.1、对象的创建
对于Java程序员来说,只要使用new关键字就可以创建一个对象,但是在虚拟机中,对象(只是包括普通的Java对象,不包括数组和Class对象等)的创建往往比较复杂,它的过程如下:
1、虚拟机遇到一个new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被类加载、解析和初始化过,如果没有,那么必须执行相应的类加载器。
2、在类加载过后,接下来虚拟机将为新生对象分配内存。对象分配的内存大小,在类加载完全后便可以确定。内存分配分为指针碰撞和空闲列表两种。
指针碰撞:如果Java虚拟机内存是比较规整的,已使用过的内存放在一边,未使用的内存放在另外一边,中间放置一个指针用作为分界点,当创建一个新对象需要分配内存时,只需要将指针像未使用的内存一边移动和该对象相同大小的空间即可。
使用指针碰撞分配内存的有Serial、ParNew等基于整理(Compact)算法实现的垃圾收集器。
空闲列表:如果虚拟机内存是不规整的,空闲内存和已使用的内存在内存中散乱分布,虚拟机就必须维护一个列表,用于记录那些内存块是可以使用的,在分配的时候从列表中找到一块足够容纳该对象的内存块,并更新列表上的记录。
使用空闲列表分配内存的有CMS等基于“标记-清除(Mark-Sweep)”算法实现的垃圾收集器。
3、除考虑如何分配内存外,还有另外一个需要考虑的问题是对象创建时内存分配时的线程安全问题,比如在给A对象分配内存的时候,指针还没来的急修改,对象B又同时使用该指针来分配内存。有两种办法解决该问题:
①、对分配的内存空间的动作做同步处理;
②、本地线程分配缓冲(TLAB):把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存。虚拟机是否使用TLAB,可以通过-XX+/UseTLAB参数来设定。
4、在内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值(基本类型(不包括封装类型、boolean)为0,boolean为false,而引用类型(包括封装类型)为null)。
5、设置对象的基本信息,如这个对象属于哪个类的实例、如何找到类的元数据信息、对象的哈希码,GC分代年龄等信息。这些信息放在对象头中。
6、最后一步就是,Java对象在创建完毕后,需要调用<init>初始化方法,按照程序员的意愿去初始化对象实例的属性值。
4.2、对象的内存布局
Java对象再内存中存储的布局可以分为三个区域:对象头(Header)、实例数据(Instance)以及对齐填充(Padding)。
对象头:对象头占用的空间为8bit的倍数,对象头包括两部分数据:
①、第一部分:用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。
②、第二部分:对象指向它的类元数据的指针(类型指针),虚拟机通过这个指针确定这个对象是哪个类的实例,如果对象是一个数组,那在对象头中还必须有一块用来记录数组长度的数据,因为Java虚拟机可以从通过普通对象的元数据中获取Java对象的大小,但是无法从一个数组的元数据中获取数组的大小。
实例数据:实例数据部分是Java对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段(Java对象的属性);
对齐填充:对齐填充没有什么实在的意义,只是一个占位符,由于Java虚拟机内存管理需要Java对象的大小必须是8bit的倍数,而对象头正好是8bit的倍数,实例数据块无法保证是8bit的倍数,所以需要对齐填充来满足这一要求。
4.3、对象的访问定位
对象的访问有使用句柄和直接指针两种:
句柄:Java堆独立划分出一块内存作为句柄池,引用(reference)中存储的是对象的句柄池,句柄中包含了对象的实例数据与类型数据。
优点:在对象被移动(GC时,垃圾收集器移动对象)时,只需要修改句柄中的实例数据的指针,并不需要改动引用(reference)。
直接指针:Java对象需要考虑如何放置访问类型数据相关信息(对象头的部分),而引用存储的是对象的地址。
优点:访问数据快;缺点:引用多的话,开销大,正大内存的压力。