1. 对象的创建
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,在类的加载检查通过后,接下来java虚拟机会为新生对象会在堆中划分出一定的内存。对象所需内存大小在类加载完成后即可完全确定。
假设,java堆中的内存是绝对规整的,分配内存仅仅是把指针指向空闲空间那边挪动一段与对象大小相等的距离,叫“指针碰撞”,如果java堆中的内存不是规整的,则虚拟机需要维护一个列表记录哪些内存块可用,然后找到一个足够大的去空间划分给对象,然后更新列表记录,叫“空闲列表”。具体选择哪种分配方式由java堆是否规整决定,是否规整又由所采用的垃圾搜集器是否带有压缩整理功能决定。
内存分配的线程安全性:①.对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证操作的原子性;②.把内存分配的动作按照线程划分在不同的空间之中进行(即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),哪个线程需要分配内存,就在那个线程的TLAB上分配,只有当TLAB用完并分配新的TLAB时,才需要同步锁定虚拟机是否使用TLAB:可通过-XX:+/-UseTLAB参数来设定)。
内存分配完后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。如果使用TLAB,也可以提前到TLAB分配时。这操作保证了对象实例字段在Java代买中可以不赋初始值就直接使用。
虚拟机对对象进行的必要设置存放于对象头之中。
在完成new指令之后,紧接着会调用<init>方法将对象初始化,这时一个完整的对象才算创建了出来。
完结之后把对象的引用入栈。
2.对象的内存布局
对象在内存存储的布局可以分为三大类:对象头(Header),实例数据(Instance Data),和对齐填充(Padding)。
HotSpot虚拟机的对象头包含两部分信息:①用于存储对象本身的运行时数据(哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等),长度在32位和64位的虚拟机中分别为32bit和64bit,官方称“Mark
Word”。②是类型指针,即对象指向它的类源数据的指针虚拟机通过这个指针来确定对象是哪个类的实例。但是并不是所有的虚拟机实现都必须在对象数据上保留类型指针(即查找对象的元数据信息并不一定要经过对象本身,另外如果对象是一个数组,那么对象头中还必须要一块记录数组长度的数据,因为Java虚拟机可以通过普通java对象的元数据信息确定Java对象的大小,但是从数据元数据中却无法知道数组的大小)。
实例数据部分是对象真正存储的有效信息,也就是在程序代码中所定义的各种类型的字段内容。不论父类继承的还是子类定义的,其存储顺序会受到java虚拟机分配策略参数和字段在java源码中定义顺序的影响。HotSpot默认分配策略是:longs/doubles,ints,shorts/chars,bytes/booleans,oops(Ordinary
Object
Pointers)。相同宽度的字段总是分配在一起,在满足这个条件下,父类中定义的变量会出现在之类之前。如果CompactFields参数为true(默认的),那么子类中较窄的变量也可能插入到父类变量的空隙之中。
第三部分对齐填充不是必然存在的。仅仅是占位符作用。
3. 对象的访问定位
java程序需要通过栈上的reference数据来操作堆上的具体对象。reference类型在Java虚拟机规范中之规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中对象的具体位置。看虚拟机具体实现,主流有使用句柄和直接指针:
①使用句柄要从Java堆中划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据格子的具体地址,优点是在对象被移动时只会改变句柄中的实例数据指针,reference本身不需要修改。缺点是多了一次指针定位时间开销。
②直接指针访问,reference中存储的直接就是对象地址,好处是速度更快,节省一次指针定位的时间开销,但是对象移动需要修改reference本身。