《深入理解Java虚拟机》--对象探究

类的加载:

Java虚拟机规范中,没有强制约束什么时候要开始加载,但是,却严格规定了几种情况

必须进行初始化(加载,验证,准备则需要在初始化之前开始)

  • 遇到 new、getstatic、putstatic、或者invokestatic 这4条字节码指令,如果没有类没有进行过初始化,则触发初始化。
  • 使用java.lang.reflect包的方法,对垒进行反射调用的时候,如果没有初始化,则先触发初始化
  • 初始化一个类时候,如果发现父类没有初始化,则先触发父类的初始化。


类从被加载到虚拟机内存开始,直到卸载出内存为止,它的整个生命周期包括:

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中,验证、准备和解析统称为连接(Linking)。过程如下图所示:


加载

加载阶段会做3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

验证

验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。

准备

为类的静态变量分配内存,并初始化默认值,这些内存是在方法区中分配,需要注意以下几点:

  • 此处内存分配的变量仅包含类变量(static),而不包括实例变量,实例变量会随着对象实例化被分配在java堆中。
  • 这里默认值是数据类型的默认值(如0、0L、null、false),而不是代码中被显示的赋予的值。
  • 如果类字段的字段属性表中存在ConstatntValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

初始化算是类加载过程的最后一个阶段,在这个阶段在是真正的开始有java代码主导。将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作


对象的创建:

创建对象(克隆、反序列化)一般是一个newkeyword而已,而在虚拟机中,对象的创建步骤例如以下: 

①当虚拟机遇到new指令时。首先将去检查这个指令參数能否在常量池中定位到一个类的引用符号,而且检查这个符号引用代表的类是否被载入、解析和初始化过。假设没有。那必须先执行相应的 类载入过程


②在类载入检查通过以后。接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类载入后便确定。为对象分配空间的任务等同于把 一块确定大小的内存从Java堆划分出来

  ②.①创建对象的过程其实也是一个非线程安全的过程,所以也需要考虑线程安全的问题。可能出现正在给对象A分配内存,指针还没来得及改动,对象B又同一时候使用了原来的指针来分配内存的情况。解决这一问题的方案是: 

  方案一、对分配内存空间的动作进行同步处理--实际上虚拟机採用CAS配上失败重试的方式,保证更新操作原子性 。

  方案二、把内存分配的动作依照线程划分在不同空间之中进行。即每一个线程在Java堆中预先分配一小块内存。称为本地线程分配缓存(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,仅仅有TLAB用完并分配新的TLAB时,才须要同步锁定。虚拟机是否使用TLAB,能够通过-XX:+/-UseTLAB參数来设定。


③内存分配完毕以后。虚拟机会将分配到的内存空间都初始化为零值(不包括对象头),假设使用TLAB,这一工作过程也能够提前至TLAB分配时进行,这一步操作保证了对象实例字段在Java代码中能够不赋初始值就能直接使用,程序能訪问到这些字段的数据类型所相应的零值


④接下来虚拟机要对对象进行必要的设置,比如:这个对象是哪个类的实例、怎样才干找到类的元数据信息、对象的哈希码、对象GC分代年龄信息等。这些信息存放在对象的信息头之中。依据虚拟机执行状态的不同。如是否使用偏向锁等,对象头会有不同的设置方式。 


上述工作完毕以后,从虚拟机角度来看,一个新的对象已经产生了,可是从Java程序来看,对象才刚刚开始——(init)方法还没有执行。全部的字段都还为零,所以,一般来说。执行new命令后。会接着执行init方法。把对象依照程序猿的意愿进行初始化,这样一个真正可用的对象才算全然产生出来。 


对象的内存布局:

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

HotSpot虚拟机的对象头包括两部分信息:

第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等,这部分数据称为Mark Word。  

对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类 型的字段内容。

对齐填充:对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。


对象的访问定位:

使用对象时Java程序需要通过栈上的reference数据来操作堆上的具体对象。

目前主流的访问方式有使用句柄直接指针两种

  • 如果使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。   如图所示:


  • 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的 相关信息,而reference中存储的直接就是对象地址,如图所示:



猜你喜欢

转载自juejin.im/post/5d5511475188257761189dca