《深入理解java虚拟机》读书笔记 第七章 虚拟机类加载机制

18 篇文章 0 订阅
7 篇文章 0 订阅

概述

1. 虚拟机把描述类的数据从Class文件加载到内存
2. 对数据进行校验、转换解析、初始化
3. 最终形成可被虚拟机直接使用的java类型

类加载的时机

"类从被虚拟机加载到内存,到卸载出内存的生命周期":
    加载(Loading)
    验证(Verification)
    准备(Preparation)
    解析(Resolution)
    初始化(Initialization)
    使用(Using)
    卸载(Unloading)
    
其中,验证、准备、解析统称为连接(Linking)

"类什么时候需要开始第一个阶段:加载?"
虚拟机规范并没有规定何时需要,
但严格规定了"有且只有5种情况必须立即对类"进行"初始化" 即在"对一个类进行主动引用"的情况下: (而加载 、验证、准备自然需要在此之前开始)
    1. 遇到new 、getstatic、putstatic或invokestatic指令时。
    (即在java中,new Object()、读取或设置一个类的静态字段、调用一个类的静态方法)
    2. 使用java.lang.reflect反射调用时,如果类没有进行过初始化,则需要先触发其初始化。
    3. 当初始化一个类时,发现其父类未进行初始化,则需要先触发其父类的初始化。
    4. 虚拟机启动时,用户需要指定一个主类(包含main方法的类),虚拟机会先初始化这个类
    5. 当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最
后的解析结果REF_getStatic、 REF_putStatic、 REF_invokeStatic 的方法句柄,并且这个方法
句柄所对应的类没有进行过初始化,则需要先触发其初始化。
    
除此之外,所有引用类的方法(被动引用)都不会触发初始化。

"接口与类的区别"是第三种:当一个类在初始化时,要求其父类全部初始化过;
但是一个接口在初始化时,并不要求其父接口全部初始化,"只有在真正使用到父接口的时候(如引用 父接口中定义的常量)才会初始化"

类加载的过程

加载

在加载阶段,虚拟机需要完成3件事。
    1. 通过一个类的全限定名来获取定义此类的二进制字节流。
    2. 将此字节流代表的静态存储结构转化为方法区的运行时数据结构。
    3. 在内存中声称一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
一个"非数组类"的加载阶段是开发人员可控性最强的,加载阶段可以使用系统提供的引导类加载器完成,也可以由用户重写一个类加载器的loadClass方法完成。

"数组类"本身不通过类加载器创建,由java虚拟机直接创建。其创建过程遵循以下规则:
    1. 如果数组的组件(即单个元素,如int[] 组件为int)类型是引用类型,就递归采用加载过程去加载这个组件类型。"数组将在加载该组件类型的类加载器的类名称空间上被标志(一个类必须与类加载器一起确定唯一性)"
    2. 如果数组的组件类型不是引用类型,虚拟机将会把数组标记为与引导类加载器关联。
    3. 数组类的可见性与组件类型可见性一致,如果不是引用类型,则默认为public。
    

验证

连接阶段第一步,目的:
    验证是连接阶段的第-一步,这一阶段的目的是为了确保Class文件的字节流中包含的信
息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证四阶段:

1. 文件格式验证:
    验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。只有通过这个阶段,字节流才会存储入方法区。
2. 元数据验证
    对类的元数据进行语义校验,以保证其符合java语言规范。
3. 字节码验证
    最复杂的阶段。
    对类的方法体进行校验,保证方法中不会做出危害虚拟机安全的事件。
4. 符号引用验证
    非常重要但不必要的阶段。
    发生在虚拟机将符号引用转化为直接引用的时候,这个动作将在解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行校验

准备

准备阶段是正式为类变量分配内存并设置类变量初始值得阶段,这些变量所使用内存都在"方法区"中。
两个容易混淆的概念:
    1. 这时候内存分配仅包括类变量(被static修饰的变量)。
    2. 初始值"通常情况"下是数据类型的零值。
    假设:
        public static int value=123;
    value 在准备阶段过后,初始值为0,而不是123。 但如果类字段存在ConstanValue属性,则准备阶段value会被初始化为ConstantValue属性所指定的值。
    假设:
        public static "final" int value=123;
    则准备阶段后,value值为123

在这里插入图片描述

解析

解析阶段是虚拟机将常量池内符号引用替换为直接引用的过程。
符号引用(Symbolic References):
    以一组符号来描述所引用目标。符号可以是任何形式字面量,只要使用时能无歧义的定位到目标。与内存布局无关
直接引用(Direct References):
    直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。与内存布局相关。如果有了直接引用,引用的目标必定已经存在于内存中。

虚拟机规范并未规定解析阶段发生的具体时间,只要求在操作符号引用的字节码之前,先对它们所使用的符号引用进行解析。
除invokedynamic外,虚拟机可以对第一次解析结果进行缓存。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符 7类符号引用。分别对应于常量池的CONSTANT_Class_ info、
CONSTANT_Fieldref_info、 CONSTANT_Methodref info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_ info、 CONSTANT_MethodHandle_info 和CONSTANT_InvokeDynamic_info 7种常量类型。

此章只解析前四个,即与invokeDynamic无关的

类或接口的解析

假设:
    当前代码所处类为D,要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,完成整个解析需要3个步骤:
        1. 如果C不是数组类型,虚拟机将代表N的全限定名传递给D的类加载器,加载这个类C。
        2. 如果C是数组类型,且数组的元素类型为对象。则会按照1 加载数组元素类型。
        3。 如果1、2步骤没有出现异常,则C在虚拟机中已经成为一个有效的类或接口。之后进行符号引用验证,确认D对C是否有访问权限。如果没有权限,则抛出java.lang.IllegalAccessError。

字段验证

首先对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析(也就是字段所属的类或接口的符号引用)。
解析成功完成后,将这个字段所属的类(或接口)用C表示,虚拟机规范要求按如下步骤对C进行后续字段搜索:
    1.如果C本身包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个子弹的直接引用,查找结束。
    2. 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    3. 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    4. 否则,查找失败,抛出java.lang.NoSuchFieldError 异常。
如果查找过程成功返回了引用,将对字段进行权限验证,如果没有权限,则抛出java.lang.IllegalAccessError。

类方法解析

类方法解析第一个步骤与字段方法解析相同,如果解析成功,依然用C表示这个类。后续搜索步骤:
    1. 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_
index中索引的C是个接口,那就直接拋出java.lang.IncompatibleClassChangeError异常。
    2. 如果通过了第1步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,
如果有则返回这个方法的直接引用,查找结束。
    3. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,
如果有则返回这个方法的直接引用,查找结束。
    4. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述
符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,
抛出java.lang.AbstractMethodError异常。
    5. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError.
最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果没有访问权限,将抛出java.langlllegalAcessError异常。

接口方法解析

接口方法解析第一个步骤与字段方法解析相同,如果解析成功,依然用C表示这个类。后续搜索步骤:
    1. 与类方法解析不同,如果接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
    2. 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则
    返回这个方法的直接引用,查找结束。
    3. 否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围会包括
    Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方
    法的直接引用,查找结束。
    4. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

接口中的方法默认为public,所以不存在访问权限的问题。

初始化

类加载过程的最后一步。
到了初始化阶段,才真正开始执行类中的java代码。
初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>():
    1. <clinit>()是由编译器自动收集类中的所有"类变量的复制动作""静态语句块(static{}块)"中的语句合并产生的。
    2. <clinit>()方法与类构造器(或者说是实例构造器<init>()方法)不同,它不需要显式调用父类构造器。虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()已经执行完毕。"在虚拟机中第一个被执行的<clinit>()方法的类肯定是Object"。
    3. 父类的<clinit>()限制性,说明"父类的静态代码块比子类的变量赋值操作"
    4. <clinit>()方法不是必需的。如果类中没有静态代码块,也没有对变量的赋值操作,编译器可以不为它生成<clinit>()方法。
    5. 接口中不能使用静态代码块,但可以有变量初始化的赋值操作。接口与类不同的是:
        执行接口的<clinit>()方法"不需要先执行"父接口的<clinit>()。只有当父接口中定义的变量使用时,父接口才会初始化。 另外,接口的实现类在初始化时,"也一样不会执行接口的<clinit>()方法"。
    6. 虚拟机保证一个类的<clinit>()方法在多线程环境中安全(加锁、同步)。 <clinit>()方法中可能造成线程阻塞。

类加载器

类加载器:
    "通过一个类的全限定名来获取此类的二进制字节流",以便让应用自己决定如何去获取所需要的类。

类与类加载器

虽然只用于实现类的加载动作,但在java程序中起到的作用,不仅仅在类加载阶段。
任意一个类,都需要它的类加载器和这个类本身一同确立在jvm中的唯一性。(比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义。)

相等:
    包括代表类的Class对象的equals方法、isAssignableFrom方法、isInstance方法的返回结果,也包括使用instanceof做对象所属关系判定等情况。

双亲委派模型

从jvm角度,"只存在两种不同的类加载器":
    1. 启动类加载器(Bootstrap ClassLoader),由C++实现,是虚拟机自身一部分。
    2. 所有其他类加载器,由java实现,独立于虚拟机外部,全部继承自java.lang.ClassLoader。 
从java开发者角度,大多是java程序都会用到以下3种系统提供的类加载器:
    1. 启动类加载器(Bootstrap ClassLoader)
    负责<JAVA HOME>\lib 目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且
    是虚拟机识别的( 仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录
    中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引
    用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直
    接使用null代替即可。
    2. 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.I auncher$ExtClassI oader
    实现,它负责加载<JAVA_ HOME>liblext目录中的,或者被java.ext.dirs系统变量所指定
    的路径中的所有类库,开发者可以直接使用扩展类加载器。
    3. 应用程序类加载器(Application ClassLoader)一般也称它为系统类加载器:这个类加载器由sun.misc.Launcher$App-ClassLoader实现。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。

下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。
    它要求"除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器"。这里类加载器之间的斧子关系一般不会以继承关系实现,而是使用"组合关系"来复用父加载器的代码。

在这里插入图片描述

双亲委派模型的工作过程

如果一个类加载器收到了类加载的请求,它首先将请求委派给父类加载器去完成。
因此所有的加载请求最终都应该传到顶层的启动类加载器。
只有当父加载器反馈自己无法完成此加载请求(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

双亲委派模型的优点、作用

"java类随着它的类加载器一起具备了一种带有优先级的层次关系"。
保证了java程序的稳定运行、java类型体系中最基础的行为。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值