第7章_虚拟机类加载机制

概述

Java虚拟机把描述类的数据从Class文件加载到内存,并进行加载,连接,初始化.这些都是在程序运行期间完成的。

Java天生可以动态扩展的语言特性,就是依赖于运行期动态加载和动态连接实现的

Class文件,指的是一串二进制字节流,并非特指某个存在于具体磁盘中的文件

类加载的时机

  1. 整个生命周期:加载,连接(验证,准备,解析),初始化,使用,卸载
  2. 加载,验证,准备,初始化,卸载顺序是确定的,必须按照这种顺序按部就班地开始,但不是按部就班地进行或完成。他们通常相互交叉地混合进行,会在一个阶段的执行过程中调用激活另一个阶段
  3. 解析阶段有可能在初始化阶段之后开始,因为要支持运行时绑定(动态绑定)

立即对类进行初始化的六种情况:

有且仅有这六种.这些行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用

  1. 遇到new,getstatic,putstatic,invokestatic这四种字节码指令,如果没有进行过初始化,则需要先触发其初始化阶段,典型情况:

    1. 使用new关键字实例化对象
    2. 读取或设置一个类型的静态字段
    3. 调用一个类的静态方法
  2. 使用java.lang.reflect包的方法对类进行反射调用。如果类没有初始化,则需要触发

  3. 初始化类,其父类还没有初始化

  4. 虚拟机启动时,需要指定执行主类(main()),先初始化这个主类

  5. 使用JDK7新加入的动态语言支持,方法句柄对应的类没有进行过初始化

  6. 一个接口定义了JDK8新加入的默认方法(被default修饰),如果这个接口的实现类发生了初始化,则该接口要在其之前进行初始化

被动引用例子

  1. 通过其子类来引用父类中定义的静态字段,只会触发父类的不会触发之类的初始化。
  2. 创建类的数组,也只会自动生成一个继承于Object的数组类型,不会触发子类的初始化
  3. 引用类中定义的常量,因为编译期间通过常量优化,常量的值直接存储在使用的类的常量池中

类加载的过程

加载

任务

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

二进制字节流获取方式

  1. 从zip获取,如JAR
  2. 从网络中获取,如Web Applet
  3. 运行时计算生成,如动态代理技术
  4. 由其他文件生成,如JSP
  5. 从数据库中获取
  6. 从加密文件中获取,防Class文件被反编译的保护措施

数组类的加载

  1. 如果数组的组件类型是引用类型,则会递归采用之前所说的加载过程去加载该组件类型,数组类将被标识在加载该组件类型的类加载器的类名称空间上
  2. 如果数组的组件类型不是引用类型,数组类将与引导类加载器关联
  3. 数组类的可访问性与它的组件类型一致, 如果数组的组件类型不是引用类型,则默认为是public

加载结束

加载阶段结束后,二进制字节流就按照格式存储在方法区之中了,然后会在Java堆内存中实例化一个Class对象,作为程序访问方法区中的类型数据的外部接口

验证

为了确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害到虚拟机自身的安全

文件格式验证

验证字节流是否符合Class文件格式的规范,并且被当前版本的虚拟机处理(例如是否以魔数开头,常量池中常量是否有不被支持的常量类型,CONSTANT_Utf8_info型的常量是否有不符合Utf8编码的数据)

目的是保证输入的字节流能正确地解析并且存储在方法区之内,即格式上符合描述一个Java类型的要求

元数据验证

对字节码描述的信息进行语义分析,包括:

  1. 该类是否有父类
  2. 该类的父类是否继承了不允许继承的类(final修饰的类)
  3. 如果不是抽象类,是否实现了其父类或者接口要求实现的所有方法
  4. 类中字段,方法是否与父类产生矛盾(例如覆盖了父类的final方法,出现不符合规范的重载等等)

字节码验证

对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:

  1. 保证任意时刻操作数栈的数据类型与指令的代码序列都能配合工作。不会出现类似于在操作数栈放置了一个int型的数据,使用却按long类型来加载入本地变量表中
  2. 保证任何跳转指令都不会跳转到方法体之外的字节码指令中
  3. 保证方法体中的类型转换都是有效的

但不可能完全确保程序是安全的,因为停机问题理论:不能通过程序准确地检查出程序是否能在有效的时间之内结束运行

符号引用验证

发生在虚拟机将符号引用转化为直接引用的时候,发生在解析阶段。验证该类是否缺少或者被禁止访问它依赖的某些外部类,方法,字段等资源,通常检验:

  1. 符号引用中通过字符串描述的全限定名是否能找到对应的类
  2. 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
  3. 符号引用中的类,字段,方法的可访问性

准备

正式为类中定义的静态变量分配内存并设置类变量初始值的阶段
在JDK8中,类变量会随着Class对象一起放Java堆中
不包括实例变量,实例变量会在对象实例化时随着对象一起分配在堆中
通常情况下初始值是数据类型的零值,直到在类的初始化阶段通过类构造器的()方法赋值
但如果是final修饰,即存在ConstantValue属性,那在准备阶段就会被初始化为ConstantValue属性所指定的初始值

解析

  1. 将常量池内的符号引用替换为直接引用的过程

  2. 符合引用:以一组符合来描述所引用的目标,可以是任何形式的字面量,与虚拟机实现的内存布局无关

  3. 直接引用:可以直接指向目标的指针,相对偏移量,或者是一个能间接定位到目标的句柄,与虚拟机实现的内存布局直接相关。如果有了直接引用,那引用目标必定已经在虚拟机的内存中存在

  4. 在执行操作符号引用的字节码指令之前,必须先堆它所使用的符号引用进行解析。可以自行判断在加载时就解析还是在使用前才解析

类和接口的解析

假设当前代码所处的类为D,要把从未解析过的符号引用N解析为一个类或接口C

  1. 如果C不是一个数组类型,虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C
  2. 如果C是一个数组类型,并且数组的元素类型为对象,会先按上面的方法加载数组元素类型,再由虚拟机生成一个代表该数组维度和元素的数组对象
  3. 在解析完成之前,要进行符号引用验证,确认D是否具备对C的访问权限

字段解析

  1. 会先对字段所属的类或接口的符号引用进行解析
  2. 如果C本身包含了简单名称和字段描述符都与目标相匹配的字段,则返回该字段的直接引用
  3. 否则,则按照继承关系从下往上递归搜索各个接口和它的父接口
  4. 否则,则按照继承关系从下往上递归搜索父类
  5. 找不到,则抛出NoSuchFieldError
  6. 查找成功并返回引用后,将对这个字段进行权限验证,如果没有访问权限,则抛出IllegalAccessError

方法解析

  1. 会先对方法所属的类或接口的符号引用进行解析
  2. 发现C是接口,抛异常
  3. 如果C本身包含了简单名称和字段描述符都与目标相匹配的字段,则返回该方法的直接引用
  4. 父类中递归查找
  5. 实现的接口列表和父接口中递归查找
  6. 权限认证

接口方法解析

  1. 会先对方法所属的类或接口的符号引用进行解析
  2. 发现C是类,不是接口,抛异常
  3. 如果C本身包含了简单名称和字段描述符都与目标相匹配的字段,则返回该方法的直接引用
  4. 父接口中递归查找
  5. 因为允许接口的多继承,所以如果在不同的父接口中找到多个,则从多个方法中返回其中一个
  6. 因为接口所有方法默认public,所以JDK9之前没有模块化访问约束,不存在访问权限问题

初始化

  1. 初始化阶段就是执行类构造器<clinit>()方法的过程,它是Javac编译器的自动生成物,是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中语句合并而成的。收集的顺序由在源文件出现的顺序决定
  2. 静态语句块只能访问到定义在静态语句块之前的变量,定义在之后的变量,只能赋值,不能访问
  3. 父类的<clinit>()先执行,所以父类中定义的静态语句块优先于之类的变量赋值操作
  4. <clinit>()方法不是必须的,如果没有类变量的赋值动作和静态语句块,则不为该类生成<clinit>()方法
  5. 执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有父接口的变量被使用时,父接口才会被初始化。接口的实现类在初始化时也不会执行接口的<clinit>()方法
  6. 虚拟机保证一个类的<clinit>()方法在多线程环境被正确的加锁同步

类加载器

类加载器:通过一个类的全限定类名来获取描述该类的二进制字节流 在Java虚拟机外实现

类与类加载器

每个类加载器都拥有一个独立的类名称空间

一个类由他的类加载器和这个类本身一同确定其唯一性

即使两个类来源于同一个类加载器,被同一个Java虚拟机加载,只要加载他们的类加载器不同,则着两个类必定不相等

相等,包括了equals()方法,instanceof关键字等等

双亲委派机制

虚拟机角度

启动类加载器(Bootstrap ClassLoader) 使用C++实现,是虚拟机自身的一部分

其他所有类加载器:由Java实现,独立于虚拟机外部,全都继承自抽象类java.lang.ClassLoader

开发人员角度

保持着三层类加载器,双亲委派机制的类加载结构

启动类加载器(Bootstrap ClassLoader)

负责加载存放在<JAVA_HOME>\lib目录下,而且是虚拟机能够识别的类库加载到虚拟机内存中

启动类加载器无法被Java程序直接引用,如果需要把加载请求委派给启动类加载器处理,则用null代替

扩展类加载器(Extension ClassLoader)

负责加载<JAVA_HOME>\lib\ext目录下类库

是一种Java系统类库的扩展机制

允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能

应用程序类加载器(Application ClassLoader)

也称为系统类加载器。

负责加载用户类路径(ClassPath)上所有的类库。如果没有定义自己的类加载器,则它就是程序默认的类加载器

双亲委派模型

除了顶层的启动类加载器,其余的类加载器都应有自己的父类加载器,通常使用组合关系来复用父加载器的代码

工作过程

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

优势

类随着他的类加载器一起具备了一种带有优先级的层次关系

例如Object类,无论哪个类加载器去加载他,都会委派到启动类加载器进行执行,因此Object类在不同的类加载器环境下都能保证是同一个类

破坏双亲委派机制

  1. 第一次被“破坏”:兼容双亲委派机制出现之前已经存在的用户自定义类加载器的代码。按照loadClass()方法的逻辑,如果父类加载失败,会调用自己的findClass()方法来完成加载。这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派加载的
  2. 第二次被“破坏”:由于模型自身的缺陷导致的,如有基础类型又要调回用户的代码。但是显然启动类加载器不可能认识,加载这些代码。使用线程上下文类加载器,用父加载器去请求子类加载器去完成类加载的行为违背了双亲委派机制的一般性原则
    按照loadClass()方法的逻辑,如果父类加载失败,会调用自己的findClass()方法来完成加载。这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派加载的
  3. 第二次被“破坏”:由于模型自身的缺陷导致的,如有基础类型又要调回用户的代码。但是显然启动类加载器不可能认识,加载这些代码。使用线程上下文类加载器,用父加载器去请求子类加载器去完成类加载的行为违背了双亲委派机制的一般性原则
  4. 由于用户对程序动态性的追求导致的。如代码热替换,模块热部署等等。此时的类加载器不再是双亲委派机制推荐的树状结构,而是更加复杂的网状结构

猜你喜欢

转载自blog.csdn.net/weixin_42249196/article/details/108164100