深入理解JVM——类加载器

前言

JVM的类加载机制是一个很经典的知识点,阅读本文,你将了解类文件的组成、JVM中的类加载过程、类加载和卸载的时机、类加载器的概念和作用以及双亲委派模型等知识点。

1、类文件

机器只认机器码,所谓的机器码, 就是机器识别的一堆有特殊意义的==二进制指令==。JVM作为虚拟的机器,也抽象出了一套自己的“指令集”,这些信息都存在Class文件里,Class文件是以二进制形式存储,并严格的规范了各个字段的语义, 兼容性和检查方法,Class 文件具体由:魔数、版本信息、常量池、访问标志、类索引、父类索引、接口索引集合、字段表集合、方法表集合、属性表集合构成,JVM8规范给出的结构如下:

ClassFile {
    u4             magic; //魔数 表示这个Class文件的类型,Java的魔数是0xCAFEBABE,翻译是咖啡宝贝
    u2             minor_version;//版本信息 Class的小版本号
    u2             major_version;//版本信息 Class的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池 主要存放两大常量:字面量和符号引用
    u2             access_flags;//访问标志  用于识别一些类或者接口层次的访问信息
    u2             this_class;//类索引  用于确定这个类的全限定名
    u2             super_class;//父类索引  用于确定这个类的父类的全限定名
    u2             interfaces_count;//接口索引数量
    u2             interfaces[interfaces_count];//接口索引集合 描述这个类实现了哪些接口
    u2             fields_count;//字段表集合数量
    field_info     fields[fields_count];//字段表集合 存储本类涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量
    u2             methods_count;//方法表集合数量
    method_info    methods[methods_count];//方法表集合信息
    u2             attributes_count;//属性表数量
    attribute_info attributes[attributes_count];//属性表信息 存储信息量最大的一块结构,里面包含了方法编译后生成的字节指令(Code), 注解信息等等
}
复制代码

2、类的生命周期

Java类从被虚拟机加载开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段;其中验证、准备和解析又统称为连接阶段。
类的生命周期

2.1、类加载和卸载

JVM将指定的class文件读取到内存里,并运行该class文件里的Java程序的过程,就称之为==类的加载==;反之,将某个class文件的运行时数据从JVM中移除的过程,就称之为==类的卸载==。

Java 虚拟机规范没有强制约束类加载过程的第一阶段(即:加载)什么时候开始,==类加载的最佳时机==是解析Java字节码类文件中常量池符号的时候,Class.forName()、ClassLoader.loadClass()、反射API和JNI_FindClass都可以触发类加载,Hot JVM自身启动的时候也会触发类加载。

卸载类即该类的 Class 对象被 GC,类的卸载跟采用的垃圾收集算法有关,在这里,我们只需要记住JVM中一个类的卸载要满足下面这3个条件就行:

  • 该类所有的实例对象都已被回收;
  • 该类的类加载器对象已经被回收;
  • 该类没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

从上面的条件中,我们知道,在 JVM 生命周期内,由 JVM 自带的类加载器加载的类是不会被卸载的,而我们自定义加载器加载的类是可以被卸载掉的。

3、四种类加载器

从JVM的角度看,类加载器主要有两类:Bootstrap ClassLoader和其他类加载器,Bootstrap ClassLoader是C++语言实现,是虚拟机自身的一部分;其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。
在这里插入图片描述
最上面是JVM 中内置了三个重要的 ClassLoader,我们也可以自定义类加载器,通常在以下几种情况下可能需要自定义类加载器:

  • 隔离加载类,某些框架为了实现中间件和应用程序的模块的隔离,就需要中间件和应用程序使用不同的类加载器;
  • 修改类加载的方式,类加载的双亲委派模型并不是强制的,用户可以根据需要在某个时间点动态加载类;
  • 扩展类加载源,例如从数据库、网络进行类加载;
  • 防止源代码泄露,Java代码很容易被反编译和篡改,为了防止源码泄露,可以对类的字节码文件进行加密,并编写自定义的类加载器来加载自己的应用程序的类。

3.1、类加载器的作用

在JVM中,一个类的唯一性是需要这个类本身和类加载一起才能确定的,每个类加载器都有一个独立的命名空间。不同的类加载器,即使是同一个类字节码文件,在JVM里的类对象也不是同一个。
因此,类加载器在JVM中的作用除了将类的字节码文件从JVM外部加载到内存中,还==确定了一个类的唯一性==。

3.2、双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的==好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系==。

3.3、破坏双亲委派模型

  • 双亲委派模型自JDK1.2以后才出现,而java.lang.ClassLoader在1.0就已经出现,所以JDK为了兼容,于是新加了一个findClass()方法, 1.2以后不推荐直接写loadClass方法, 而是推荐用findClass,这样可以保证新写出来的类加载器是符合双亲委派机制的。
  • 由于子类加载器会先将类交给父类加载器加载,解决了基础类的统一问题。如果基础类要调用用户类的代码,就无法处理。JDK为了满足这个需求(如在JNDI中),引入了Thread Context ClassLoader, 这个类加载器可以通过setContextClassLoader进行设置, JDBC用这个类加载器去加载SPI代码, 实际上违背了双亲委派机制。

结束语

阅读完本文,对于Java的动态性,如热部署,动态编译JSP, 运行时增强类功能等, 是不是又有了新的认识:)