类加载机制

一.类加载的生命周期

  这些阶段通常都是互相交叉的混合进行 会在一个阶段执行的过程中激活另一个阶段 为了支持java的动态绑定 解析可能会在初始化之后
 

二.生命周期各部分拆解分析

 1.加载 (参杂部分字节码文件格式验证过程)

  1)加载过程
   ①通过全限定名获取二进制字节流(不止class文件 还可从zip 动态代理 数据库等获取
   ②将字节流所代表静态存储结构转化为方法区运行时数据
   ③在内存中生成对应类的class对象 作为这个类数据访问入口
  2)数组类和非数组类加载
   ①非数组类:非数组类获取二进制流可由开发人员自由控制
   ②数组类:本身不是类加载器创建 是虚拟机在内存中构造的 但其元素类型还是依靠类加载器创建
   元素类型 数组类去掉所有维度的类型
   组件类型 数组类去掉一个维度的类型
   ①组件类型是引用类型 则遵循类加载过程 则标记在该组件类型的加载器的类名称空间上
   ②组件类型非引用类型 则将数组标记为与引导类加载器关联

 2.验证(符号引用验证发生在解析阶段 除文件格式验证都在方法区)

  1)文件格式验证 验证二进制字节流 包括魔数 主次版本号 常量等 通过才进入方法区存储
  2)元数据验证 语义验证 主要是元数据信息是是否符合java语言规范 包括是否有父类 是否继承不被允许的类等
  3)字节码验证 语义校验 通过数据流和控制流分析 主要是类的方法体(code属性)包括对指令的验证
     优化:由于数据流和控制流分析的复杂度 jdk6之后在code属性中加入子属性
     stackMapTable用来记录本地变量表和操作数栈的状态 从而只需校验
     stackMapTable种记录的合法性
  4)符号引用验证 发生在解析阶段 主要是验证该类是否缺少或者被禁止访问他依赖的某些信息 包括根据描述符能否找到对应的类 符号引用中的类的可访问性等

 3.准备

  1)为静态变量分配内存并设置初始值为0 赋值则在初始化阶段 但constantValue属性(即常量)会立刻赋值 所分配的内存 都在方法区 方法区是个逻辑区域
  2)分配的变量是类变量 实例变量在对象实例化的时候分配在堆上

 4.解析 (符号引用转换成直接引用的过程)

  符号引用:简单名称 描述符等字符常量 在class文件constant_class_info等常量 与内存布局无关
  直接引用:可以直接指向目标的指针 相对偏移量或能间接定位目标的句柄 与内存布局直接相关
  1)符号引用可在加载时就对常量池符号引用解析或在使用符号引用前解析
  2)符号引用多次解析 可对第一次解析结果缓存 避免重复解析 第一次解析成功就都成功 否则失败
  invokedynamic指令则对上述规则不成立 只有执行到该指令才可以进行解析动作
  3)解析针对的对象
   ①类或接口解析
    1.非数组 则使用所在类的类加载器加载 也可能触发父类接口的加载数组 则加载数组的元素类型 并由虚拟机生成数组对象
    2.符号验证 所在类是否能够访问该类
    3.jdk9模块引入后 若不在同一模块 则需要赋予所在类访问该类的权限
   ②字段解析
    1.先进行该字段所在类或接口的解析 即①
    2.依次按继承关系从下往上扫描所在类 其接口 其父类 包含字段简单名称和描述符立马返回
    3.成功返回直接引用 则权限校验 所在类是否对该字段有访问权限(同上校验模块)
    注:实际情况 若所在类无简单名称和描述符且父类和接口都有 则会因为不知道返回哪个直接引用 报错 the field is ambiguous
   ③类方法解析 (类的方法跟接口方法的描述符是区分开的)
    1.①
    2.检查类的方法表中的描述符索引是否是接口 是接口则报错incompatibleClassChangeError
    3.依次扫描所在类 其父类 有简单名称和描述符 立马返回(这里如果返回了就说明这个类是个普通类了 因为如果该类的父类实现了有方法的接口 该类就必须实现接口方法 所以如果到这步都没返回 这个时候走到第四步 接口中找到了方法 只有一种情况 即是个抽象类      抽象类可以不实现该类父类的接口中的方法 所以第三步这个时候没返回什么)
    4.扫描其接口 无匹配则报错NoSuchMethodError有则该类不是普通类 而是一个抽象类实现了接口 此时 抽象类方法不允许被调用报错abstractMethodError
    5.权限验证 访问权限(同上考虑模块)
   ④接口方法解析
    1.①
    2.检查接口方法表中的描述符索引是否是类 是则报错incompatibleClassChangeError
    3.依次扫描所在接口 有简单名称和描述符 立马返回
    4.扫描其父接口 无匹配则报错NoSuchMethodError 有则 由于可继承多个父接口 类似字段解析 可能会直接报错
    5.权限验证 访问权限(同上考虑模块)

 5.初始化

  1)真正开始执行应用程序的代码 初始化变量和其他资源 即执行类构造器<clinit>()方法的过程
  2)<clinit>()是由编译器自动收集类中的类变量的赋值动作和静态语句块合并产生
  3)静态语句块只能访问它之前的变量 但能给它之后的变量重新赋值
  4)不像构造器一样要先显示调用父类的构造器 它能保证一定先执行父类的<clinit>()
  5)接口也有<clinit>()方法 但只包括变量赋值动作 并且只有父类定义的变量被使用 才会初始化
  6)多个线程去初始化一个类 只有其中一个线程会执行一次<clinit>()方法 其他线程都要阻塞等待 并且其他线程唤醒后也不会重新初始化
  7)(主动引用)场景有且只有这6种情况 称为对一个类型的主动引用
   ①遇到new getstatic putstatic invokestatic四条字节码指令
    ①new关键字实例化对象(new 数组出发的是虚拟机)
    ②读取或者设置一个静态字段(final修饰 编译期放入常量池的常量除外)
    ③调用一个静态方法
   ②jdk7之后动态语言支持java.lang.invoke.MethodHandle实例解析出来的方法句柄是REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial
   ③对类型进行反射调用的时候
   ④类的父类必须全部初始化 而接口只需要在用到父接口时才初始化
   ⑤虚拟机启动 需要指定一个要执行的主类(main方法 也被static修饰)
   ⑥jdk8之后加入的被defalut修饰的方法的接口要在实现类被初始化之前
  8)被动引用场景
   ①通过子类调用父类的静态字段 只会初始化父类
   ②new数组 不触发类初始化 而是该数组的描述符(虚拟机自动生成)的初始化 使用命令newarray
   ③引用常量 编译期被放入常量池的常量

三.类和类加载器

 类加载器是判断类是否相同的关键 我们可以用自己的类加载器加载

四.双亲委派模型

 
 1)jvm角度 可分为启动类加载器和非启动类加载器
 2)开发人员角度 可分为
  ①启动类加载器
   加载存放在lib目录中的类库(名字不符合的类库 即使在lib中也不不会加载) 放到虚拟机内存中无法被java程序直接引用 自定义类加载器委派给启动类加载 直接赋null值即可
  ②扩展类加载器
   加载放在lib/ext中的类库 加载一些扩展性的功能 jdk9后被模块化带来的扩展能力取代
  ③应用程序类加载器(系统类加载器)
   加载用户类路径上的所有类库(开发人员的代码) 无自定义类加载器 一般就是默认的加载器
  ④自定义加载器
   获得磁盘之外的class文件 实现类的隔离 重载等功能
 3)类加载器之间的关系是组合关系 复用代码 而不是继承
 4)加载机制:先检查请求加载的类型是否被加载 没有则委派给自己的父类去加载 父类没找到再自己加载 即最先的肯定是启动类加载器
 5)好处:java类随着它的类加载器具备一定的优先级层次关系 比如Object类 最终都必须由启动类加载器加载 保证了Object的唯一性

五.破坏双亲委派模型

 1)在引入双亲委派模型之前 为了兼容用户自定义类加载器的代码 添加了protected方法findClass()
 2)为了能够让基础类型调用用户代码 引入线程上下文类加载器 可通过Thread中的setContext-ClassLoader()方法进行设置 如果创建线程时还未设置 就从父线程中继承 如果在应用程序全局范围内都没设置过的话 上下文类加载器默认就是应用程序类加载器 如JDBC
       JNDI(java名称和目录接口)
 3)对用户程序动态性(热部署 热替换)的追求 OSGi实现模块化部署 每一个程序模块都有一个自己的类加载器 需要更换一个模块 就把模块连同类加载器一起换掉以实现代码热替换OSGi类加载过程只有1和2是符合双亲委派模型 其余的都是在平级类里加载

六.模块化下的类加载器

 
 1)扩展类加载器被平台类加载器取代 rt和tool类库被拆分为数十个JMOD文件 去掉了ext目录
 2)类加载委派关系演变为:(可视为第四次破坏双亲委派模型)
    在委派给父类加载器前 必须先判断该类是否能够归属到某一个模块中 如果可以 则要有限委派给负责那个模块的类加载器完成加载

七.模块化(封装隔离机制)

 1)可配置的封装隔离机制解决的问题
  ①基于类路径查找依赖的可靠性问题 启动模块对其他模块的显式依赖 启动时就能确定依赖关系
  ②public类型的可访问性问题 不再是全部都可访问 还需指定模块
 2)类似于类路径的模块路径 某个类库到底是模块还是传统jar包 取决于放在哪种路径上
  ①jar文件在类路径访问规则
   匿名模块 几乎没有隔离 可使用类和模块路径上所有的包
  ②模块在模块路径
   具名模块 只能访问到他依赖定义的模块和包 无法访问匿名模块
  ③jar文件在模块路径
   自动模块 访问到模块路径上所有的包以及自己的包
 
执行顺序:
静态代码块/静态变量 > main方法
创建对象的时候才会执行 构造块>构造方法

posted @ 2021-01-25 17:06  zys1314  阅读(71)  评论(0)    收藏  举报