在java程序中总是不断的有对象被创建出来,但是在虚拟机中对象的创建又是怎样一个过程呢?
符号引用:在java中,一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People类引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,即直接引用地址。
当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,接下来虚拟机将对新生对象分配内存,对象所需的内存大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块大小确定的内存从java堆中划分出来。假设java堆的内存是绝对规整的,使用的分配方式就是"指针碰撞”,如果java堆的内存并不是规整的,使用的分配方式就是“空闲列表”。选择哪种分配方式由Java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。除如何划分可用空间之外,还有另外一个需要考虑的问题就是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的问题。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程需要分配内存,就在TLAB上进行分配,只有TLAB用完并分配新的TLAB时,才需要进行同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使TLAB,这一工作过程也可以提前到TLAB分配时进行。
在HotSpot虚拟机中,对象在内存中的存储的布局分为3块区域:对象头,实例数据,对齐填充。
对象头: 对象头主要分为两部分。一部分是主要存储对象自身的运行数据。比如:哈希码、GC分代年龄、锁状态标志、线程持久的锁、偏向线程的ID、偏向时间戳等。对象头的另外一部分是对象类型,即对象指向它类元数据的指针。虚拟机通过它来确定该实例属于哪个类。
实例数据:对象真正存储的有效信息,也就是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数和字段在java在源码中定义的顺序的影响。相同宽度的字段总是被分配到一起。在满足这个前提下,在父类中定义的变量会出现在子类之前。
对齐填充:占位符的作用。对象的存储起始地址是8字节的整数倍。
我们的java程序需要通过栈上的reference数据来操作堆上的对象,但是reference数据在java虚拟机规范中只规定了一个指向对象的引用,而具体的访问方式由虚拟机的实现方式决定。但是目前主流的访问方式有使用句柄和直接指针两种。
句柄访问:java堆中会划分出一块内存来作为句柄池,reference存储的就是句柄的地址,而句柄里包含着对象的实例数据的地址和类数据的地址。
直接引用:reference对象存储的是对象的实例数据的地址。就HotSpot虚拟机来说,采用的是直接引用的方式。