JVM类加载器

文章目录

  • 一、类加载器
  • 二、类与类加载器
  • 三、双亲委派模型
  • 四、破坏双亲委派模型
    • 4.1、Tomcat
      • 4.1.1、WebApp类加载器
      • 4.1.2、Shared类加载器
      • 4.1.3、Catalina类加载器
      • 4.1.4、Common类加载器
      • 4.1.5、Jsp类加载器
    • 4.2、JDBC

一、类加载器

从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader
JVM类加载器_第1张图片

  • 启动类加载器(Bootstrap ClassLoader): 负责加载存放在 \lib 目录中的核心类库,如rt.jarresources.jar等(或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库)。这个加载器是 C++ 编写的,随着JVM启动。

  • 扩展类加载器(Extension ClassLoader): 负责加载\lib\ext 目录中的类库,(同样也可以用 java.ext.dirs 系统变量来指定路径)。

  • 应用程序类加载器(Application ClassLoader): 负责加载用户类路径 classpath 上所有的 jar 包和 .class 文件。

  • 自定义类加载器: 可以支持一些个性化的扩展功能。


二、类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。


这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里所指的“相等”,包括代表类的Class对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。


三、双亲委派模型

为了避免类的重复加载,确保一个类的全局唯一性,以及保护程序安全,防止核心API被随意篡改,JVM会采用双亲委派模型进行加载,双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
JVM类加载器_第2张图片
双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的夹杂请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。


这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码,源码如下:
JVM类加载器_第3张图片

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object ,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 classPath 中,那系统中将会出现多个不同的 Object 类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。


双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在 java.lang.ClassLoaderloadClass() 方法之中,代码逻辑主要为:先检查是否已经被加载过,若没有加载则调用父加载器的 loadClass() 方法,若父加载器为空,则默认使用启动加载器作为父加载器。如果父类加载器加载失败,抛出 ClassNotFoundException 异常后,在调用自己的 findClass() 方法进行加载。
JVM类加载器_第4张图片


四、破坏双亲委派模型

上面我们以及看过了 loadClass() 方法的源码,双亲委派的具体逻辑就实现在这个方法之中,JDK1.2之后就不提倡去覆盖 loadClass() 方法,而是应当把自己的类加载逻辑写到 findClass() 方法中,在 loadClass() 方法的逻辑里如果父类加载失败,则会调用自己的 findClass() 方法来完成加载,这样就可以保证新写出的类加载器是符合双亲委派规则的。


但是双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外。


4.1、Tomcat

Tomcat为什么会破坏双亲委派模型呢?因为Tomcat需要解决如下问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  2. 部署在同一个Web容器中相同的类库相同的版本可以共享,否则会有重复的类库被加载进JVM。
  3. Web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  4. Web容器支持jsp文件修改后不用重启,jsp文件也是要编译成 .class 文件的,支持HotSwap功能
    JVM类加载器_第5张图片

4.1.1、WebApp类加载器

针对上述问题一,需要Web应用层级的隔离,Tomcat给每个Web应用都创建一个类加载器实例(WebAppClassLoader),该加载器重写了 loadClass() 方法,优先加载当前应用目录下的类,如果当前找不到了,才会一层一层往上找。

4.1.2、Shared类加载器

对于问题二,另外并不是Web应用程序下的所有依赖都需要隔离的,部署在同一个Web容器中相同的类库相同的版本可以共享。否则如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。

这里Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载。

4.1.3、Catalina类加载器

问题三中需要隔绝Web应用程序与Tomcat本身的类,Tomcat会有类加载器(CatalinaClassLoader)来装载Tomcat本身的依赖。

4.1.4、Common类加载器

如果Tomcat本身的依赖和Web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享。

4.1.5、Jsp类加载器

JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个 .class 文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。 (JSP热部署原理)


4.2、JDBC

双亲委派模型很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为他们总是作为被用户代码调用的API,但是如果我们的基础类也要调用用户代码,那该怎么办?


如常见的数据库驱动加载,在使用JDBC写程序之前,通常会调用这行代码Class.forName("com.mysql.jdbc.Driver"),用于加载所需要的驱动类。
JVM类加载器_第6张图片
上述代码如果想要运行成功的话,肯定是需要引入mysql-connector-java依赖的,如下:


<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>5.1.45version>
dependency>

在引入依赖成功后,我们可以在mysql-connector-java包下查看到的/META-INF/services/java.sql.Driver文件
JVM类加载器_第7张图片
其实就是利用了Java SPI机制,文件中内容其实就是JDK中rt.jar类库中的一个接口,如下:
JVM类加载器_第8张图片

按照上述介绍的rt.jar类库应该有启动类加载器(Bootstrap ClassLoader)进行加载,但是其实该接口在rt.jar类库是没有任何实现类的,其实现类是在引入的mysql-connector-java包中。
JVM类加载器_第9张图片


这里就存在基础类也要调用用户代码,所以启动类加载器(Bootstrap ClassLoader)是无法进行加载的,那么SPI机制下JDBC是如何加载的呢?

有了解Java SPI机制的话,应该知道是使用 ServiceLoader 来加载,这里主要在 DriverManager 类中
JVM类加载器_第10张图片

在 DriverManager 类进行加载初始化时,一定会执行 static 代码块
JVM类加载器_第11张图片
JVM类加载器_第12张图片
在这里插入图片描述

这里可以看出为了解决上述问题,Java引入了一个线程上下文加载器,这个类加载器可以通过 java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器
JVM类加载器_第13张图片

你可能感兴趣的:(JVM,java)