1、对象的创建
本小节讨论普通对象,不包括数组和Class对象。
当遇到一条new指令时,有如下操作:
-
检查常量池中是否有这个对象所属类的符号引用;
-
若常量池中没有这个类的符号引用,说明类没有被定义!抛出ClassNotException;
-
若常量池中有这个类的符号引用,则进行下一步工作;
-
-
进而检查这个符号引用代表的类是否被JVM加载、解析和初始化;
-
如果类没有被加载,则找到该类的Class文件,并加载到方法区;
-
若该类已经被JVM加载,则准备为对象分配内存;
-
-
根据方法区该类的信息确定该类所需内存大小;
对象所需内存的大小在类加载完之后就可完全确定;一个类产生的每个对象的内存大小是一样的!
-
从堆中分配一块对应大小的内存空间给新的对象;
分配堆中内存有两种方式:
1)指针碰撞
适用于java 堆内存规整的情况,如:JVM的GC器采用复制算法或标记-整理算法,那么堆中空闲内存是完整的区域,并且空闲内存和已使用内存由一个指针标记。当为一个对象分配内存,只需移动指针即可。
2)空闲列表
如果JVM的垃圾回收器使用标记-清除算法,那么堆中空闲区域和已使用区域交错,因此需要维护一个“空闲列表”来记录堆中哪些区域是空闲区域,从而在分配对象内存时,可以通过空闲列表找到空闲区域,分配内存。
综上:采用何种分配方式,取决于它使用了何种垃圾回收器。
除了划分可用空间外,在并发情况下,并不是线程安全的。如:正在为对象A分配内存,指针还没来得及修改,对象B又用了原来的指针分配内存。两种解决方案:
1)对分配内存空间的动作进行同步处理----JVM采用CAS配上失败重试的方式保证更新操作的原子性;
2)每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有在TLAB用完并分配新的TLAB,才需要同步锁定。-XX:+/-UseTLAB参数来设定是否使用TLAB。
-
为对象中的成员变量初始化为零值(不包括对象头)
-
设置对象头中的信息
-
调用对象的构造函数进行初始化
此时,整个对象的创建过程就完成了。
2、对象的内存布局
对象在内存的存储布局分为3块区域:
-
对象头
-
实例数据
-
对齐填充
2.1 对象头
包含两部分信息:
-
对象在运行过程中所需要的一些数据:HashCode、GC分代年龄、锁状态标记、线程持有的锁、偏向线程ID、偏向时间戳等。
-
可能还包含类型指针,即对象指向它的类元数据的指针。通过这个指针JVM可以确定对象属于哪个类。
如果对象是一个数组,对象头还要包含数组长度。
2.2 实例数据
实例数据是对象真正存储的有效信息---成员变量的值,其中包含父类的成员变量和本类的成员变量。
2.3 对齐补充
并不是必然存在的,起着占位符的作用。用于确保对象的总长度是8字节的整倍数。
HotSpot VM要求对象的其实地址必须是8字节的整数倍,即对象大小必须是8字节的整数倍(对象头正好是8字节的整数倍,所以可能要对实例数据进行填充)。
3、对象的访问
Java程序是通过栈上的reference数据来操作具体对象,那么这个reference是如何去定位对象的?有两种对象访问方式:
-
句柄访问
需要在堆中划分一块“句柄池”,用于存放所有对象的地址和所有对象所属类的类信息。
引用类型的变量存放的是该对象在句柄池的地址。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中的对象地址再访问对象。
-
直接指针访问
引用变量存放的是对象的地址,从而直接通过引用能访问对象。
但对象所在内存空间需要额外的策略存储对象所属类信息的地址。
比较:
-
句柄的优势:引用变量存储的是稳定的句柄地址。在对象移动时(在gc中非常普遍)只会改变句柄中的实例数据指针,而reference本身不需要修改。
-
直接指针的优势:速度快。只需一次寻址操作。对象访问在Java中非常频繁,这类开销节省是非常可观的
HotSpot采用的是直接指针方式访问对象。