jvm学习篇04 - 类加载机制

《深入理解Java虚拟机》读后速记。

类加载机制的概念

虚拟机把描述类的数据从Class文件中加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就称之为虚拟机的类加载机制。

类加载的时机

一个类的生命周期包括如下七个阶段:加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析统称为连接。一般来说这些过程都是按顺序开始执行的(注意这里是“开始”,因此每个过程都不必等待前一个过程完成之后才能开始执行,他们是相互交叉混合进行的),除了解析这个过程,它也有可能在初始化后再执行。

《Java虚拟机规范》中没有约束类加载第一个过程“加载”的时机,因此可以由虚拟机自行把控。

但是在《Java虚拟机规范》中严格规定了 有且仅有 六种情况需要立即对类进行“初始化”:

① 遇到四条字节码指令时,若类型还未初始化,则需要先执行它的初始化。

  • new():使用new关键字实例化对象时
  • getstatic、putstatic:读取或者设置一个类的静态变量时(被final修饰、在编译阶段已被放入常量池中的静态字段除外)
  • invokestatic:调用类的静态方法时

② 使用java.lang.reflect的方法对类进行反射调用时,若类型还未初始化,则需要先执行它的初始化。
③ 当初始化一个类的时候,若发现它的父类还未初始化,则需要先执行父类的初始化。
④ 虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类)虚拟机会先初始化这个类。
⑤ …
⑥ 当一个接口定义了jdk8引入的默认方法(default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那么接口要在其之前进行初始化。

类加载过程

类加载过程主要包括:加载、验证、准备、解析、初始化。

加载

加载阶段虚拟机主要执行三件事情:

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

验证

验证是连接的第一步,确保Class文件的字节流中包含的信息符合《Java虚拟机规范》,保证这些信息被当作代码运行后不会危害虚拟机的安全。
主要包括:

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候——解析阶段。符号引用验证可以看作是对自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
    主要验证以下内容:
  • 符号引用中通过字符串描述的全限定名能否找到对应的类
  • 符号引用中的类、字段、方法的可访问性(private、protected、public、package)是否可被当前类访问

符号引用验证的主要目的是为了确保解析行为的正常执行。这一阶段对于类加载机制来说是非必须的,因此若程序中的所有代码都已经别反复使用和验证过,那么在生产环境下可以考虑使用-Xveryfy:none参数来关闭大部分类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段正式为类中定义的变量(静态变量)分配内存并设置初始值。
初始值一般情况下为零值,但是如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段就会设置初始值为ConstantValue属性所指定的初始值。

解析

解析阶段就是虚拟机中将常量池中的符号引用转化为直接引用的过程。

  • 符号引用指用一组符号来描述所引用的目标。
  • 直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

解析动作主要针对以下7类符号引用:(后面是其相对的常量池中的常量类型)

  1. 类或者接口CONSTANT_Class_Info
  2. 字段CONSTANT_Fieldref_Info
  3. 类方法CONSTANT_Methodref_Info
  4. 接口方法CONSTANT_InterfaceMethodref_Info
  5. 方法类型CONSTANT_MethodType_Info
  6. 方法句柄CONSTANT_MethodHandle_Info
  7. 调用点限定符

Java是静态类型的语言,这里主要说前四种:

类或接口的解析

针对CONSTANT_Class_info符号引用进行解析。

假设当前所处类为D,要将其中一个从未解析过的符号引用N解析成类或接口C的直接引用,那么虚拟机解析过程分为以下三个步骤:

  1. 如果C不是数组类型,那么虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C,加载这个类C的过程中又可能去加载其他的类。倘若在加载过程中发生任何异常,则宣告解析失败。
  2. 如果C是一个数组类型,并且数组元素类型为引用类型,例如N的描述类型如[Ljava.lang.Integer这种形式,那么虚拟机会先去加载元素类型,如加载java.lang.Integer这个类 ,接着再由虚拟机生成一个代表该 数组维度和元素 的数组对象
  3. 若以上两步未发生异常,那么虚拟机中已经存在一个有效的类或者接口了,但在解析完成之前,还要执行前面验证阶段所说的符号引用验证,确人D是否有C 的访问权限。
字段解析

首先了解字段表结构如下

CONSTANT_Fieldref_info {  
   u1 tag;  //值为9
   u2 class_index;  //指向声明 字段的类或接口的描述符CONSTANT_Class_info的索引项  
   u2 name_and_type_index;  //指向该字段描述符CONSTANT_NameAndType的索引项
} 

//其中的CONSTANT_NameAndType结构如下
CONSTANT_NameAndType_info{  
   u1 tag;  //值为12
   u2 class_index;  //指向该字段或方法 【名称】 常量项的索引
   u2 name_and_type_index;  //指向该字段或方法 【描述符】 常量项的索引
} 

字段解析过程:

  1. 首先对字段表内class_index中该字段 所属的类或接口的符号引用,即上述字段表中的CONSTANT_Class_info符号引用进行解析,在解析过程出现任何异常都会导致解析失败;(解析成功后这里将该类或接口用C表示)
  2. 《Java虚拟机规范》要求对C进行如下后续搜索:
  • 如果C本身就包含了 简单名称和字段描述符 都与目标相匹配的字段,就返回这个字段的直接引用
  • 否则,如果C实现了接口,那么将会按照从下往上递归按照继承关系递归地搜索接口和它的父接口,如果在接口中包含了 简单名称和字段描述符 都与目标相匹配的字段,就返回这个字段的直接引用
  • 否则,若C不是java.lang.Object,则会按照继承关系自底向上递归搜索其父类,,如果在父类中包含了 简单名称和字段描述符 都与目标相匹配的字段,就返回这个字段的直接引用
  • 否则,搜索失败,抛出java.lang.NoSuchFieldError异常

如果在搜索阶段成功返回直接引用,那么还要对这个字段进行权限验证,如果发现不具备对字段的访问权限,抛出java.lang.IllegalAccessError异常。

方法解析

方法表结构如下

CONSTANT_Methodref_info {  
   u1 tag;  //值为9
   u2 class_index;  //指向声明方法的类描述符CONSTANT_Class_info的索引项  
   u2 name_and_type_index;  //指向名称及类型描述符CONSTANT_NameAndType的索引项
} 

//其中的CONSTANT_NameAndType结构如下
CONSTANT_NameAndType_info{  
   u1 tag;  //值为12
   u2 class_index;  //指向该字段或方法 【名称】 常量项的索引
   u2 name_and_type_index;  //指向该字段或方法 【描述符】 常量项的索引
} 

方法解析过程:

  1. 首先对方法表内class_index中该方法 所属的类或接口的符号引用,即上方法表中的CONSTANT_Class_info符号引用进行解析,在解析过程出现任何异常都会导致解析失败;(解析成功后这里将该类或接口用C表示)
  2. 接下来虚拟机还要进行后续的搜索…(暂略)

最后如果查找成功并返回了直接引用,将会对这个方法进行权限验证,如果发现C不具备对这个方法的访问权限,将抛出java.lang.IllegalAccessError异常。

接口方法解析

大体上同方法解析。(暂略)

初始化

类的初始化是类加载过程的最后一个阶段,初始化阶段就是执行类构造器方法<cinit>()的过程。
其中<cinit>()方法是javac编译器自动生成的。

  1. <cinit>()方法是由编译器自动收集类中所有类变量赋值动作和静态代码块中的语句合成并产生的。
  2. <cinit>()与类的构造函数不同(虚拟机视角下的实例构造器()方法),它不需要显式的调用父类的构造器,虚拟机会保证再子类的<cinit>()方法执行之前,父类的<cinit>()方法已经执行完毕(这也意味着父类中的静态代码块和赋值动作优先于子类进行),因此再Java虚拟机中第一个执行的<cinit>()方法是java.lang.Object的。
  3. 类中若没有静态代码块、也没有赋值动作,那么可以不生成<cinit>()
  4. 接口不能使用静态代码块,但是可以有赋值动作,因此也可以有<cinit>()方法。不同之处在于子接口中执行<cinit>()方法时,父接口<cinit>()方法不用先执行,只有父接口中的变量被使用时才会执行<cinit>()
  5. Java虚拟机必须保证一个类的<cinit>()方法在多线程环境下被正确地同步加锁,即如果多个线程同时去初始化一个类,那么只有一个线程可以成功执行<cinit>()方法,其他线程进入阻塞状态,直到活动线程执行完<cinit>()方法。

类加载器

对于任意一个类,必须由这个类的加载器和该类本身共同确定该类在虚拟机中的唯一性。

Java虚拟机的角度看,只有两种加载器:

  1. 启动类加载器(Bootstrap ClassLoader),这个加载器时由C++实现的;
  2. 其他类加载器,Java语言实现,全部继承自java.lang.ClassLoader。

Java开发人员的角度看,分为3种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):负责加载 存放在<JAVA_HOME>/lib目录、或者-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机可以识别的类库加载到虚拟机内存中。
  2. 扩展类加载器(Extesion ClassLoader)::负责加载<JAVA_HOME>/lib /ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
  3. 应用程序类加载器(Application ClassLoader):也成为“系统类加载器”,它负责加载 用户类路径上的所有的类库。一般来说,这个就是程序中默认的类加载器。

双亲委派机制

双亲委派机制工作过程:如果一个类加载器收到了类加载的请求,它不会立即自己去加载这个类,而是将该请求委托给它的父类加载器去完成,每一个层次的加载器都是如此,只有因此所有的类加载请求都会被传送到最顶层的启动类加载器中,只有当类父类加载器反馈自己无法完成该加载请求时,子类才会去加载该类。
双亲委派机制带来的好处:Java中的类跟随它的类加载器一起具备了一种带有优先级的层次关系。例如Java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载该类,都是委托给最顶层的启动类加载器,这样就保证了系统中只有一个Object类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值