JVM-类加载器子系统

129 阅读11分钟

类加载子系统作用

  • 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识(也叫魔数,每个Class 文件的头4 个字节(u4)称为魔数),比如java的class文件魔数为CAFEBABE
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量是Class文件中常量池部分的内存映射

类的加载过程

类的加载过程为

graph LR
加载==>链接==>初始化==>类的使用==>类的卸载

其中,链接分为

graph LR
验证==>准备==>解析

加载

  • 通过一个类的权限定名获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构(这个类的信息,如全局变量,类中方法)转化为方法区的运行时数据结构,(JDK1.8之前叫永久代,之后叫元空间)
  • 内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口,指向方法区的数据结构。

特殊(数组类的加载)

创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组累的过程如下:

如果数组的元素类型是引用类型,那么就遵循定义的加载过程,递归加载和创建数组A的元素类型,JVM使用指定的元素类型和数组维度来创建新的数组类 如果使用的数组元素类型是基本数据类型,则由虚拟机预先定义。

验证

  • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全。
  • 主要包含四种验证,文件格式验证元数据验证字节码验证符号引用验证
  • 文件格式验证:是否是OXCAFEBABE开头, 主版本号和副版本号是否在当前Java虚拟机的支持范围内,数据中的每一项是否都有正确的长度等。
  • 元数据验证: java虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的虚拟机,也不会予以验证通过,比如是否所有的类都有父类的存在,除了Object类,是否一些被定义为final的方法或者类被重写或继承
  • 字节码验证: 字节码验证也是验证过程中最为复杂的一个过程,他试图通过对字节码流的分析,判断字节码是否可以被正确的执行, 比如在字节码的执行过程中,是否会跳转到一条不存在的指令, 函数的调用是否传递了正确类型的参数。
  • 符号引用验证: 校验器还将进行符号引用的验证, class文件在其常量池会通过字符串记录自己将要使用的其他类或或者方法,因此在验证阶段虚拟机就会检查这些类或者方法确实是存在的。并且当前类有权限访问这些数据,如果一个需要使用的类无法在系统中找到就会抛出NoClassDefFoundError如果一个方法无法被找到,只会抛出NoSuchMethodError。(此阶段在解析环节执行)

准备

  • 为类变量分配内存并且设置该类变量的默认初始值,即零值
  • 这里不包含用final修饰的static,因为final在编译的时候就会分配了, 如果是基本类型,准备阶段会显式初始化, 如果是引用类型, 则在初始化阶段显式赋值
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是回随着对象一起分配到java堆中。

解析

  • 常量池内的符号引用,符号引用就是一组符号来描述所引用的目标,转换为直接引用的过程,直接引用就是直接指向目标的指针,相对偏移量或句柄
  • 事实上,解析操作往往会伴随着jvm在执行完初始化之后在再执行
  • 解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等。

初始化

  • 初始化阶段就是执行类构造器方法< clinit >()的过程。
  • 此方法不需定义,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来,如果一个类中类变量或静态代码块,则无clinit方法
  • 如果一个类中有类变量,但是没有显式赋值,则不会生成< client >方法。
  • 构造器方法中指令按语句在源文件中出现的顺序执行
  • < clinit >()不同于类的构造器。(构造器是虚拟机视角下的< init >())
  • 若该类具有父类,jvm会保证子类的< clinit >()执行前,父类的< clinit >()已经执行完毕
  • 虚拟机必须保证一个类的< clinit >()方法在多线程下被同步加锁

类的使用

主动使用

class类只有在必须要首次使用的时候才会装载,Java虚拟机不会无条件的装载class类型, Java虚拟机规定一个类或接口,在初次使用前必须要进行初始化,这里的使用是指主动使用,主动使用只有下列几种情况,如果出现如下的情况,则会对类进行初始化操作,初始化操作之前的加载验证,准备已经完成。

  1. 当创建一个类的实力时,比如使用new关键字或者通过反射,克隆,序列化。
  2. 当调用类的静态方法时,即当使用字节码invokestatic指令。
  3. 当使用类,接口的静态字段时,比如getstatic或putstatic指令。
  4. 当使用java.lang.refect包中的方法反射类的方法时。
  5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。不适用于接口
  6. 如果一个接口定义的default方法,那么直接实现,或者间接实现该接口的类的初始化,该接口要在其之前被初始化
  7. 当虚拟机启动时,用户需要指定一个执行的主类main,虚拟机会先初始化这个主类。
  8. 当初次调用Methodhandle实例时,初始化该方法指向的所在的类

被动使用

除了以上的情况属于主动使用,其他的情况均属于被动使用被动使用,不会引起类的次数话,也就是说并不是在代码中出现的,那就一定会被加载 或者初始化,如果不符合主动使用的条件,类就不会初始化

1.当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。 但通过子类引用父类的静态变量,不会导致子类初始化。 2.通过数组定义类引用,不会触发此类的初始化 3.引用常量不会触发此类或接口的初始化,因为常量在链接阶段就已经被显式赋值了。 4.调用ClassLoader的类的loadClass()的方法加载一个类并不是对类的主动使用,不会导致类的初始化

类的卸载

类,类的加载器,类实例之间的引用关系

  • 在类加载器的内部实现中,用一个Java集合来存放所加载类的应用。另一方面,一个class对象总是会引用它的类加载器调用class对象的getClassLoader()方法,就能获得它的类加载器,由此可见,代表某个类的class实实例与其类的加载器之间双向关联关系。
  • 一个类的实例总是引用代表这个类的class对象,在Object类种定义了getClass()方法,这个方法返回代表对象所属类的class对象的引用,此外所有的Java类都有一个静态属性class,它引用代表这个类的class对象

类的生命周期

  • 当类加载,链接和初始化后,它的生命周期就开始了,当代表类的class对象不在被引用,即不可触及时,class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期,一个类何时结束生命周期取决于代表它的class对象何时结束生命周期。

关系图

qq_pic_merged_1659187474592.jpg

卸载条件

  • 该类所有的实例都已经被回收,也就是java堆中,不存在该类及其任何派生子类的实例。
  • 加载该类的那加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景, 否则很难达成。
  • 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

类加载器的分类

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用c/c++语言实现的,嵌套在jvm内部
  • 它用来加载Java的核心库,(JAVA_HOME/jre/lib/rt或sun开头的类)用于提供JVM自身需要的类
  • 并没有继承java.lang.ClassLoader,没有父加载器
  • 加载拓展类和应用程序类的加载器,并指定为他们的父类加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类

拓展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Lanucher$ExtClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lib/ext目录下加载类库。如果用户创建的jar放在此目录下,也会自动有扩展类加载器加载

应用程序类加载器(AppClassLoader)

  • Java语言编写,由sun.misc.Lanucher$AppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
  • 通过ClassLoader#getSystemClassLoader()方法可以获取到更改类加载器

双亲委派机制

Java虚拟机对Class文件采用的是按需加载方式,也就是说当需要使用改类时才会将它的class文件加载到内存生成的class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。

工作原理

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。

流程图

  • qq_pic_merged_1659252241913.jpg

优势

  • 避免类的重复加载,确保一个类的全局唯一性
  • Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 保护程序安全,防止核心API被随意创改

弊端

  • 检查类是否加载的委托过程是单向的,这个方式虽然从结构上来说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
  • 通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。

结论

  • 推荐但不一定要使用双亲委派机制。
  • 如tomcat是先子ClassLoader自己加载,加载失败再放给父ClassLoader

破坏的场景

线程上下文加载器

代码热替换,模块热部署

自定义加载器

  • 隔离加载类
  • 修改类加载方式
  • 扩展加载源
  • 防止源码泄漏