对象的创建
通常情况下,我们是通过new指令完成一个对象的创建的。
虚拟机在接受到一个new指令会做如下操作:
-
判断对象的类是否加载、链接、初始化, 虚拟机在接收到一条new指令时,首先会检查这个指令的参数是否在常量池中定位到一个类的引用,并且检查这个符号引用代表的类是否被类加载器加载、链接和初始化。如果没有,那必须先执行相应的类加载过程。
-
为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存区域分配给对象。内存分配根据Java堆是否规整,有两种方式,而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理" ,值得注意的是,复制算法内存也是规整的。:- 指针碰撞:如果Java堆整的内存是规整的,则分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离。
- 空闲列表:如果Java堆是不规整的,则需要Java虚拟机维护一个列来记录哪些内存是可用的,在分配的时候从列表中查询到足够大的内存分配给对象,并更新列表记录。
-
处理并发安全问题
创建对象是一个频繁的操作,所以要解决并发问题,有两种方式:- 对分配空间的动作进行同步,保证操作的原子性
- 每个线程在Java堆预先分配一小块内存,这块内存叫做本地分配缓冲(TLAB)。线程需要分配内存时,就在对应线程的的TLAB上分配内存,当TLAB用完并且分配到了新的TLAB时,这时才需要同步锁定。
-
初始化分配到的内存空间
将分配到的内存,除了对象头都初始化为零值。 -
设置对象的对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,将对象的所属类、对象的HashCode和对象的GC分带年龄等数据存储在对象的对象头。 -
执行init方法进行初始化
执行init方法,初始化对象的成员变量、调用类的构造方法,这样就完成了一个对象的创建。
对象的堆内存分布
Java对象在堆内存中内存布局分为三个区域,分别是对象头、实例数据、对齐填充。
- 对象头:对象头包括两部分信息,分别是Mark World和元数据指针。
Mark World用于存储运行时数据,比如hashcode,锁状态标志、GC分带年龄、线程持有的锁等等。
元数据用于指向方法区中目标类元数据的指针,通过元数据可以确定对象的具体类型。 - 实例填充:用于存储对象中的各种类型的字段信息。
- 对齐填充:不一定存在,通常起到一个占位的作用。
Mark world在HotSpot中的实现类为markOop.hpp,markOop被设计成一个非固定的数据结构,这是为了在极小的控件中存储尽量对的数据。
数据类型解释:
- hash:对象的哈希码
- age:对象的分代年龄
- biased_lock:偏向锁标示位
- lock:锁状态标示位
- JavaThread*持有偏向锁的线程ID
- epoch:偏向时间戳
对象的访问定位
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种 。
1. 句柄
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
2. 直接指针
如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
oop-klass模型
oop-klass模型是用来描述Java实例对象的一种数据,它分为两个部分:
Klass: 包含元数据和方法信息,用来描述Java类。一般jvm在加载class文件时,会在方法区创建instanceKlass,表示其元数据,包括常量池、字段、方法等。
Ordinary Object Pointer (普通对象指针),它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象。Klass是在class文件在加载过程中创建的,OOP则是在Java程序运行过程中new对象时创建的。
之所以采用这个模型是因为HotSopt JVM的设计者不想让每个对象中都含有一个vta