JVM系列(二):JVM的类加载子系统

作者:陆金龙    发表时间:2022-11-20 05:39   

关键词:类加载器  ClassLoader  

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

1.类加载过程

  从大的方面包含三个阶段:加载、连接(验证、准备、解析)、初始化。

  

1.1 类的加载阶段

通过一个类的全限定名来获取定义此类的二进制字节流。可以从ZIP包中读取(JAR、WAR),从网络中读取(Applet),运行时计算生成(动态代理技术),由其他文件生成(由JSP文件生成Class类),从数据库中读取等等。

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区中这个类的数据的访问入口。

1.2 连接阶段

验证:验证的目的是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。大致上会完成:文件格式验证、元数据验证、字节码验证、符号引用验证。

准备:该阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值,即数据类型的零值

解析:虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。

1.3 初始化阶段

到了初始化阶段,才真正开始执行类中定义的Java程序代码。

在初始化阶段,会根据类中定义的Java程序代码去初始化类变量和其他资源

初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。(静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。)

2.类加载器

JVM支持两种类型的类加载器,分别为启动类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
 
Bootstrap ClassLoader:启动类加载器,也叫引导类加载器,使用C和C++语言实现,是虚拟机自身的一部分。
 
User-Defined ClassLoader:这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
 
Java语言提供的自动义类加载器有扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。除此之外,用户可以定义自己的类加载器,继承ClassLoader。
 
自定义类加载器都直接或间接地继承ClassLoader。
扩展类加载器和应用程序类加载器都继承URLClassLoader,间接地继承了抽象类ClassLoader,它们的继承关系如下:
ExtClassLoader和AppClassLoader都是定义在Launcher类的内部。
 

2.1 启动类加载器(Bootstrap ClassLoader)

负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别)类库加载到虚拟机内存中。
 
例如:加载C:\Program Files\Java\jdk1.8.0_191\jre\lib下的jar类库。

2.2 扩展类加载器(Extension ClassLoader)

由sun.misc.Launcher$ExtClassLoader实现,开发者可以直接使用扩展类加载器。

ExtClassLoder中有一个parent变量是BootstrapClassLoader,但是不是继承(extends)关系。

ExtClassLoader负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
 
通过System.getProperty("java.ext.dirs").split(";"); 可以得到java.ext.dirs的路径,本机运行结果如下:
C:\Program Files\Java\jre1.8.0_191\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
 
例如:加载C:\Program Files\Java\jdk1.8.0_191\jre\lib\ext下的jar类库。

2.3 应用程序类加载器(Application ClassLoader)

由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。
 
AppClassLoader中有一个parent变量是ExtClassLoader,但是不是继承(extends)关系,二者都继承自URLClassLoader。
 
负责加载用户类路径(CLASSPATH)所指定的类库,或者系统属性java.class.path(系统类加载器加载字节码class的路径)指定路径下的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
 
注1:Java虚拟机在加载某个类时,会按照CLASSPATH指定的目录顺序去查找需要的类(.class)。这个路径可以是多个,用;隔开。需要把jdk安装目录下的lib子目录中的dt.jar和tools.jar设置到CLASSPATH中,当前目录“.”也加入到该变量中。JDK 5.0以后,默认会在当前的工作目录及JDK的lib目录中查找所需要的类,即使不设置 CLASSPATH 环境变量,也可以正常编译和运行 Java 程序。
注2:通过System.getProperty("java.class.path").split(";") 可以得到java.class.path的结果,本机运行结果如下:
C:\Users\Administrator\.m2\repository\*\*.jar(若干个)
C:\Program Files\Java\jdk1.8.0_191\lib\jconsole.jar
C:\Program Files\Java\jdk1.8.0_191\lib\tools.jar
E:\2.project-code\klcms\klcms-web\target\classes
E:\2.project-code\klcms\klcms-provider\target\classes
 
 

3.类的加载流程

应用程序都是由Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader这三种类加载器相互配合进行加载(有必要也可以加入自己定义的类加载器)。

3.1 类加载器之间的关系——双亲委派模型

类加载器之间的关系如下图所示,被称为类加载器的双亲委派模型。Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader、User Defined ClassLoader四者是包含关系,不是继承关系。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加器都要有自己的父类加载器(不是继承关系,是组合关系),父类加载器通过getParent()获取。由于Bootstrap ClassLoader是由C和C++语言实现的,ExtClassLoader使用getParent()得到null,并不能真正获取到Bootstrap ClassLoader。getParent()返回null,意味着父类加载器就是启动类加载器了。

3.2 类加载的流程——双亲委派模型的工作过程

一个类加载器收到了类加载的请求,不会自己直接加载这个类,而是把这个请求委派给父类加载器完成,每一个层次的类加载器都是这样,最终所有的类加载请求都传送到启动类加载器中。当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己加载。

双亲委派模型对于保证Java程序的稳定运行很重要。

(1)向上委派

AppClassLoader是加载一个新的class类时,不直接进行加载,而是向上委派给ExtClassLoader,查找ExtClassLoader是否缓存了这个class类,如果有则返回。如果没有则继续委派给BootstrapClassLoader,如果BootstrapClassLoader中有缓存则返回,没有缓存则BootstrapClassLoader查找%JAVA_HOME%/lib下的jar与class类文件,如果有则加载返回,如果没有则开始向下查找。

(2)向下查找

BootstrapClassLoader中不能加载这个类,则向下委派给ExtClassLoader执行加载,尝试查找ExtClassLoader对应路径%JAVA_HOME%/lib/ext下(或者被java.ext.dirs系统变量指定的路径下)的文件,如果有则加载返回,没有则继续向下到AppClassLoader查找加载,负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库。如果AppClassLoader找不到则会抛出找不到class类异常。

双亲委派模型进行类加载的好处:避免类的重复加载;保护程序安全,避免核心API被随意篡改;避免用户自定义类使用核心API的包名。

4.使用类加载器

4.1 获取类加载器

方式一:获取当前类的classLoader
this.getClass().getClassLoader ()
 
方式二:获取系统的classLoader
ClassLoader.getSystemClassLoader ()
 
方式三:获取当前线程上下文的ClassLoader
Thread.currentThread () .getContextClassLoader ()
 
方式四:获取调用者的classLoader
DriverManager.getCallerclassLoader ()

4.2 类加载器的方法

getParent()      返回该类加载器的父类加载器

loadClass(String name)    加载名称为name的类,返回java.lang.Class类的实例

findClass(String name)     查找名称为name的类,返回java.lang.Class类的实例

findLoadedClass(String name)   查找名称为name的已被加载过的类,返回java.lang.Class类的实例

defineClas(String name,byte[],int off,int len) 把字节数组b中的内容转换为一个Java类,返回java.lang.Class类的实例

resolveClass(Class<?> c)

5.用户自定义类加载器

继承抽象类ClassLoader(或者继承URLClassLoader),JDK1.2之前,重写loadClass方法。JDK1.2之后,重写findClass方法。

6.为什么要破坏双亲委派模型

原因其实很简单,就是使用双亲委派模型无法满足需求了,因此只能破坏它。

我们知道 Tomcat 容器可以同时部署多个 Web 应用程序,多个 Web 应用程序很容易存在依赖同一个 jar 包,但是版本不一样的情况。例如应用1和应用2都依赖了 spring ,应用1使用的 3.2.* 版本,而应用2使用的是 4.3.* 版本。
 
如果遵循双亲委派模型,这个时候使用哪个版本了?
 
其实使用哪个版本都不行,很容易出现兼容性问题。因此,Tomcat 只能选择破坏双亲委派模型。

如何破坏双亲委派模型

上面的类加载方法源码(loadClass)的方法修饰符是 protected,因此我们只需以下几步就能破坏双亲委派模型。
 
1)继承 ClassLoader,Tomcat 中的 WebappClassLoader 继承 ClassLoader 的子类 URLClassLoader。
 
2)重写 loadClass 方法,实现自己的逻辑,不要每次都先委托给父类加载,例如可以先在本地加载,这样就破坏了双亲委派模型了。
 
Tomcat 的类加载过程,也就是 WebappClassLoaderBase#loadClass 的逻辑如下。
 
1)首先本地缓存 resourceEntries,如果已经被加载过则直接返回缓存中的数据。
2)检查 JVM 是否已经加载过该类,如果是则直接返回。
3) 检查要加载的类是否是 Java SE 的类,如果是则使用 BootStrap 类加载器加载该类,以防止 webapp 的类覆盖了 Java SE 的类。
4)针对委托属性 delegate 显示设置为 true、或者一些特殊的类(javax、org 包下的部分类),使用双亲委派模式加载,只有很少部分使用双亲委派模型来加载。
5)尝试从本地加载类,如果步骤4中加载失败也会走到本步骤,这边打破了双亲委派模型,优先从本地进行加载

7.JDBC 使用线程上下文类加载器的原理

JDBC 功能相关的基础类是由 Java 统一定义的,在 rt.jar 里面,例如 DriverManager,也就是由 Bootstrap ClassLoader 来加载,而 JDBC 的实现类是在各厂商的实现 jar 包里,例如 MySQL 是在 mysql-connector-java 里,oracle、sqlserver 也会有各自的实现 jar。
但是 Bootstrap ClassLoader 是不认识也不会去加载这些厂商实现的代码的。
 
因此,Java 提供了线程上下文类加载器,允许通过 Thread#setContextClassLoader/Thread#getContextClassLoader() 来设置和获取当前线程的上下文类加载器。如果在应用程序的全局范围内都没有设置过的话,那这个上下文类加载器默认就是应用程序类加载器(Application ClassLoader)。
 
JDBC 可以通过线程上下文类加载器,来实现父类加载器“委托”子类加载器完成类加载的行为,这个就明显不遵守双亲委派模型了。