• classLoader


    类加载器

    2020021613450663

    1、类的生命周期

    ​ 加载 => 验证 => 准备 => 解析 => 初始化 => 使用 => 卸载

    ​ 加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班开始。但解析阶段则不一定,在某些情况下可以在初始化阶段后开始,为了支持Java语言的运行时绑定

    1.1、加载:转存至方法区并生成代理

    ​ JVM是使用双亲委派机制进行类加载,在描述前,有必要了解下该机制:

    ​ 若classLoader收到类加载请求,首先它自己不会尝试加载该类,而是委托给父类去加载,每层加载器都是如此,最终会传递到顶层启动类加载器。当父类加载器反馈自己无法完成(它的搜索范围内没有找到所需加载类),子加载器会尝试自己加载。

    ​ 该机制的优势:确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在加载的时候只会加载其中一个。

    ​ 类加载阶段,JVM实际完成的任务:

    1)通过类的全限定性类名获取该类的二进制字节码

    2)将该字节码代表的静态存储结构转换为方法区的运行时数据结构(将类的结构信息放到方法去)

    3)在内存(堆)中生成代表该类的Class对象(仅仅时.class的对象,而非实例),用来封装类在方法区里的数据结构,作为方法区中类的各种数据访问接口

    ​ 从以上三步看出,通过Class获取类的数据,像镜子反射类的信息,也是为什么在用反射时要使用Class。类加载器根据类的全限定性名读取此类的二进制字节流到JVM,存储在运行时内存的方法区,并将其转换为与目标类型对应的java.lang.Class对象实例

    1.2、验证:一切以 JVM 为重

    tianshi_diaoxiang-011

    ​ 验证作为链接的第一步,该阶段的目的是确保Class文件的子节流中包含的信息符合当前虚拟机的要求,不会对虚拟机的自身安全产生威胁。之所以会准备这样的一步,是因为虚拟机能接受的Class并非都是Java源代码编译而来,也可以通过十六进制编译器直接编写来产生。若是不对输入的字节码检查,对虚拟机有威胁的子节流也会被载入,造成安全问题。

    ​ 格式校验:是否符合Class文件规范

    ​ 语义校验:被final标记的类型是否有子类;被final修饰的方法是否被重写;父类和子类无不兼容的方法声明

    ​ 操作校验:操作数栈中数据必须正确操作,对常量池中符号引用验证(解析阶段会用到)

    1.3、准备:小弟先行

    ​ 为类变量(静态变量)分配内存并设置初始值(数据类型对应的‘零’值)

    ​ 即为静态变量分配内存并对其初始值设为零,不包括静态代码库和实例变量。

    ​ (静态代码块在初始化的时候执行,实例变量在对象实例化的时候随对象分配到Java堆中)

    1.4、解析:引用替换

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

    ​ 解析操作往往伴随着 JVM 在执行完初始化后再执行

    ​ 解析动作主要针对类或者接口,字段,类方法,接口方法,方法类型等。对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info。

    ​ 符号引用:任何形式的字面量,能描述所引目标并能在使用时无歧义的定位到即可,跟内存没关系,有可能在硬盘或别的位置,任意数据存储位置都行

    ​ 直接引用:能够直接指向目标的指针、相对偏移量或任何间接定位目标的句柄。跟 JVM 的内存布局有关,引用的目标必在内存

    ​ 验证,准备,解析三阶段合称为链接阶段,链接阶段即将 JVM 中的二进制子节流的类信息 合并到 运行时状态中。

    1.5、初始化:跑 Java 代码了

    gexing_shizu_tuyaqiang-004

    ​ 类加载的最后一步,在之前的过程中,除了加载阶段用户可以定制自定义类加载器参与,别的都是 JVM 的主导。在初始化这步,开始真正执行 Java 代码。小弟先行阶段,静态变量其实已经被赋值过,但仅为默认值,在初始化阶段会将赋值程序中的指定值,再执行静态代码块。

    ​ 小小深入:初始化的阶段也可以理解为 执行类的构造器方法的过程 (非类的构造方法)。构造器方法是cinit()方法,由编译器自动收集类中所有静态变量的赋值动作和静态代码块中的语句合并而成。需要注意的是,编译器收集顺序是语句在程序中的出现顺序,因而静态代码块只能访问到在其之前定义的值,而不能访问到之后的值,但可以对之后的值进行赋值操作,因为在开始的时候变量已经被初始化了。

    ​ 构造器方法和构造函数不同,不需要显示调用父类的构造器,虚拟机会保证子类的构造器方法执行前,父类的已经执行过了,所以在 JVM 中第一个执行的构造器方法的类一定是大家的父类。由此引申到,子类的构造器可以引用父类构造好的值,但在实际测试中,子类构造器对父类的值会也不会发生影响(这里是个疑问)

    ​ 类的加载过程到这里就结束了,类加载的最终呈现效果是位于 堆中的Class对象,封装了方法区内类的数据结构,并提供访问方法区内数据结构的接口。但到目前为止,跟类的对象是没关系的,目前只有类的静态变量和静态方法可用,对象需要我们自己去产生,更别说用普通成员变量和方法了。

    2、类加载器角色

    classLoaderRole

    ​ ClassFIle 在影盘上,可以理解为设计图纸,这个图纸要加载到 JVM 中根据图纸创造多个实例

    ​ ClassFIle 加载到 JVM,被叫做DNA元数据模板,放在方法区

    ​ 文件 -> JVM -> 元数据模板,需要运输工具,类加载器在这中间扮演快递员角色

    3、创建对象

    1)堆区分配对象需要的内存 (内存包括父类的所有实例变量,不包括静态变量)

    2)对实例默认初始值 (方法区内实例变量的定义拷贝到堆区,并赋默认值)

    3)执行实例初始化代码 (初始化顺序先父类后子类,执行顺序先执行代码块后执行构造方法)

    ​ (若有对象的引用,在栈区定义该对象类型的引用,再将堆区对象的地址赋值给他,每个子类对象持有父类对象的引用,可在内部通过super关键字调用父类对象,但外界不可访问)

    createProcess

    补充:通过实例调用实例方法时,先从方法区的对象类型信息中查找,找不到再去父类类型信息中查找。

    若继承层次比较深,而调用的方法位置在比较上层的父类中,调用效率是比较低的。比如用自定义类型去调用object类的方法,这样查找的层次就比较多了,要一层一层往上找。此时系统一般会采用 虚方法表的方法优化调用效率。

    虚方法表:类加载时,为每个类创建表,包括该类的对象所有动态绑定的方法及地址,包含父类方法,一个方法一条记录,子类重写父类方法只会保留子类。当通过对象动态绑定方法时,查这个表就可以了,不用挨个查父类。

    4、扩展

    juxing_diaosu_silie

    Java中,类的实例化分为两个部分:类的加载和类的实例化,类的加载分为显式和隐式。

    平时常用的new创建实例,就是隐式的包含了类的加载过程。对于类的显式加载,比较常用的是Class.forName()。实际上都是通过调用ClassLoader类的loadClass()方法,完成实际的加载工作。

    类加载器classLoader加载类时有层次关系,Java中共4种类型:

    BootStrapClassLoader 启动类加载器 最高层次,加载jre/lib/rt.jar路径下类或-XbootclassPath指定包

    ExtensionClassLoader 扩展类加载器 默认加载%JAVA_HOME%lib/ext/*.jar

    ApplicationClassLoader 应用类加载器 默认加载环境变量CLASSPATH中的设定值

    CustomClassLoader 自定义类加载器 根据用户需求定制加载过程,运行期指定类的动态实时加载

    (热加载也是基于该类的特性,绕过Java既定的加载规格)

    类加载器层次顺序

    类加载时,首先自底向上挨个检查是否已经加载了指定类,若已加载,则直接返回该类的引用。若找到最高层也没找到加载过该类,则开始自顶向下挨个尝试加载,直到用户自定义类加载器,还不成功,就会抛出异常。

    参考文章

    对象是如何被创建的

    类加载机制

    Java类加载器——热替换

  • 相关阅读:
    .net core入门
    编码之Base64编码
    C++ 实现十大排序算法
    C++11 智能指针
    Object Pool 对象池的C++11使用(转)
    mac pro 1.5T内存是如何实现的
    Linux C/C++开发
    mac Chrome 快捷键
    C++11多线程
    Java项目压力测试(待补)
  • 原文地址:https://www.cnblogs.com/lifelikeplay/p/16082229.html
Copyright © 2020-2023  润新知