Java虚拟机:四、Java对象创建和对象访问

1.对象创建

Java是一门面向对象的语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(克隆、反序列化)就是一个new关键字而已,但是虚拟机层面上却不是如此。看一下在虚拟机层面上创建对象的步骤:

对象的建立过程 
 
              图一:对象建立过程 
1、类加载检查。 
当JVM检测到有一条new指令时,首先先检查该指令的参数是否在常量池中定位到一个类的符号引用,并检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果存在的话,JVM将直接使用已有的信息对该类进行操作。 
如果没有,则执行相应的类加载过程。 
2、虚拟机为新生对象分配内容(位于堆中)。 

类加载检查通过后,虚拟机为新生对象分配内存对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间无非就是从Java堆中划分出一块确定大小的内存而已。

不同的JVM垃圾收集器在分配内存时的表现也不相同,具体表现为两种:

(1)如果垃圾收集器选择的是Serial、ParNew这种基于压缩整理算法的,那么内存是规整的,虚拟机将采用的是指针碰撞法来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。

(2)如果垃圾收集器选择的是CMD这种基于标记-清除算法的,那么内存不是规整的,已使用的内存和未使用的内存相互交错,虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的以及内存块的位置和大小,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。

另外一个问题是new对象时的线程安全性,也就是内存分配时的同步问题。因为可能出现虚拟机正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存的情况。这种情况下虚拟机会通过两种方式进行同步:

a、CAS和失败重试机制:对分配内存空间的动作进行同步处理,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。CAS简单解释就是:比较并交换,通过3\操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

b、TLAB方式:把内存的分配动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中先预留一块本地线程分配缓冲(TLAB)。哪个线程分配内存时,就在哪个线程的TLAB分配,只有当TLAB用完并分配新的TLAB时,才需要同步锁定。

3、内存分配结束。

内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。

4、对对象进行必要的设置。 

对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。 
5、初始化对象(执行<init>方法)。 
当完成上述操作后,对象的内存便分配成功了,但是所有的字段都还是零。 
此时应该执行<init>方法,把对象按照程序员的意愿进行初始化,从而产生一个真正可用的对象。

下面我们再将上面的过程,重新画一张图,总结一下:

2.对象的内存布局

对象的内存布局分为三个区域: 
a、对象头,b、实例数据,c、对齐填充。

  • 对象头:非固定的数据结构。一来是用来存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。二来是类型指针,即对象指向它的类元数据的指针、JVM通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,则在对象头中还需要有一块记录数组长度数据。
  • 实例数据:存储对象真正有效的信息,也就是程序代码中所定义的各种类型的字段内容。不论是从父类继承下来的,还是在子类中定义的。这部分的存储顺序会受到Java源码中定义顺序的影响。
  • 对齐填充:不一定必须存在。启到占位符的作用。因为JVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。故当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.对象的访问方式

建立对象是为了使用对象,Java程序需要通过栈上的reference(引用)数据来操作堆上的具体对象。对象访问会涉及到Java栈、Java堆、方法区这三个内存区域。

如下面这句代码:

Object obj =  new Object();

上面对象实例化的其实有两部分内容,一部分是类数据(比如代表类的Class对象)、一部分是实例数据

假如这句代码出现在方法体中,"Object obj" 这部分会作为引用类型(reference)的数据保存在Java栈的本地变量表中。而"new Object()"这部分实例化对象将会反映到Java堆中,形成一块存储Object实例化对象的所有实例数据值的结构化内存,根据具体数据类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些数据类型则存储在方法区中。

reference类型在java虚拟机规范里面只规定了一个指向对象的引用地址,并没有定义这个引用应该通过哪种方式去定位,访问到java堆中的对象位置,因此不同的虚拟机实现的访问方式可能不同,

主流的方式有两种:使用句柄和直接指针:

1、使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,obj(reference引用)中存储的是对象的句柄地址,而句柄中包含了类数据的地址和对象实例数据的地址。
2、直接指针访问,Java堆中也就是对象中存储所有的实例数据和类数据的地址,此时obj(reference引用)存放的是对象地址。 
两种访问方式的对比:

  • 使用句柄时,当改变句柄中的实例数据指针时,reference本身不需要被修改。
  • 使用直接指针访问最大的好处在于速度较快,因为其节省了一次指针定位的时间开销。

目前使用直接指针访问的方式比较常用,HotSpot虚拟机采用的是后者,因为对象的访问在Java程序运行过程中是比较频繁的,积少成多也会造成太多的时间开销。不过前者的对象访问方式也是十分常见的。

 下面拷贝两张图,明白一下对象的两种访问方式:

转载:https://www.cnblogs.com/haitaofeiyang/p/7767919.html

posted @ 2020-08-03 13:27  路漫漫吾将上下而求索  阅读(87)  评论(0编辑  收藏  举报