Java类加载

Java类加载

概述

  • 虚拟机将类的描述文件class文件加载到内存,并且进行安全校验、数据类型解析、内存分配以及初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就是虚拟机的类加载机制。
  • 与解释执行语言不通,Java语言是编译型语言,类型的连接(即加载、连接、初始化过程)是在程序运行期进行的,会增加运行开销,但程序设计更灵活。
  • Java文件从编码完成到最终执行,一般主要包括两个过程:编译、运行
    • 编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
    • 运行,则是把编译声称的.class文件交给Java虚拟机(JVM)执行。
  • 我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
  • JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
  • .class是jvm将类装入内存,是在编译期

类加载过程

  • 类加载过程图:https://blog.youkuaiyun.com/qq_36182135/article/details/81946152
  • 加载-》验证-》准备-》解析-》初始化-》使用-》卸载
  • 类加载时机:以下几种场景称为对类的主动引用,除此之外,其他对类的引用称为被动引用
    • Java虚拟机规范并没有强制规定类加载时机,这个情况需要具体的虚拟机进行自由实现
    • 遇到new、putstatic、getstatic及invokestatic这4条字节码指令时,如果类没有初始化,则立即进行初始化
    • 使用java.lang.reflect包的方法对类进行反射调用的时候
    • 当初始化一个类的时候,发现其父类没有初始化
    • 当虚拟机启动时,需用将执行启动的主类(有main()方法的那个类)进行初始化;
    • 当使用动态语言时,如果一个java.lang.invoke.MethodHandle实例最终的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic句柄时,并且这个句柄对应的类没有初始化。
加载
  • 加载指的是把class字节码文件从各个来源通过类加载器装载入内存中;加载,是指Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。
    • 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件、从jar包中抽取的.class文件、从远程网络,以及动态代理实时编译
    • 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
    自定义类加载器:一方面是由于Java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。另一方面也有可能从非标准的来源加载代码,那就需要自己实现一个类加载器,从指定源进行加载。
    
  • 类加载阶段,加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:
    • 通过“类全名”来获取定义此类的二进制字节流。
    • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
    • 在java内存中实例化中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
  • 相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。
  • 加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。
  • 类加载器分类
    • 启动类加载器/Bootstrap ClassLoader,Bootstrap ClassLoader用C++语言编写并嵌入JVM内部,主要负载加载JAVA_HOME/lib目录中的所有类,或者加载由选项-Xbootcalsspath指定的路径下的类;
    • 拓展类加载器/ExtClasLoader,ExtClassLoader继承ClassLoader类,负载加载JAVA_HOME/lib/ext目录中的所有类型,或者由参数-Xbootclasspath指定路径中的所有类型;
    • 应用程序类加载器/AppClassLoader,ExtClassLoader继承ClassLoader类,负责加载用户类路径ClassPath下的所有类型,一般情况下为程序的默认类加载器;
    • 自定义加载器,Java虚拟机规范将所有继承抽象类java.lang.ClassLoader的类加载器,定义为自定义类加载器;
连接
验证
  • 主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
  • 包括对于文件格式的验证
  • 对于元数据的验证
  • 对于字节码的验证,保证程序语义的合理性
  • 对于符号引用的验证
准备
  • 主要是为类变量static(注意,不是实例变量)分配内存,并且赋予初值。实例变量会在对象实例化时随着对象一块分配在Java堆中。
  • 初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
  • 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。
解析
  • 将常量池内的符号引用替换为直接引用的过程。
  • 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
  • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
  • 对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存,从而避免解析动作重复进行。
  • 解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。
  • 在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
  • 类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
  • 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。
  • 类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
  • 接口方法解析:与类方法解析步骤类似,知识接口不会有父类,因此,只递归向上搜索父接口就行了。
初始化
  • 这个阶段主要是对类变量初始化,是执行类构造器的过程。
  • 换句话说,只对static修饰的变量或语句进行初始化。
  • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
  • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
  • 初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。
  • 在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器()方法的过程。
  <clinit>()方法的执行规则:
  1.<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,
  静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
  2.<clinit>()方法与实例构造器<init>()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,
    父类的<clinit>()方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
  3.<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,
    那么编译器可以不为这个类生成<clinit>()方法。
  4.接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成<clinit>()方法。但是与接口类不同的是:执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
  5.虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,
    其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,
    那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
双亲委派模型
  • 上文几种类加载器的层次关系如下图所示:https://img-blog.youkuaiyun.com/20140105211242593
  • 这种层次关系称为类加载器的双亲委派模型。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。
  • 并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式
  • 如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。
  • 工作流程:
    • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,
    • 因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,
    • 只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
  • 好处
    • 使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。
    例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。
    
静态加载和动态加载
  • 定义:Java初始化一个类的时候可以用new 操作符来初始化,也可通过Class.forName的方式来得到一个Class类型的实例,然后通过这个Class类型的实例的newInstance来初始化.我们把前者叫做JAVA的静态加载,把后者叫做动态加载.。
  • 静态加载的时候如果在运行环境中找不到要初始化的类,抛出的是NoClassDefFoundError,它在JAVA的异常体系中是一个Error.
  • 动态加载的时候如果在运行环境中找不到要初始化的类,抛出的是ClassNotFoundException,它在JAVA的异常体系中是一个checked异常,在写代码的时候就需要catch.
总结

类加载过程只是一个类生命周期的一部分,在其前,有编译的过程,只有对源代码编译之后,才能获得能够被虚拟机加载的字节码文件;在其后还有具体的类使用过程,当使用完成之后,还会在方法区垃圾回收的过程中进行卸载。

参考文章

https://blog.youkuaiyun.com/u010942465/article/details/81709246
https://blog.youkuaiyun.com/qq_32534441/article/details/96632513
https://blog.youkuaiyun.com/weixin_41563161/article/details/103550512

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值