zoukankan      html  css  js  c++  java
  • 类加载器介绍

    一、jvm类加载器

    类加载器是实现了"通过一个类的全限定名来描述此类的二进制字节流"这个动作的代码模块,简单的说就是将字节码class文件读取后加载到jvm内存中的类

    从jvm虚拟机的角度来讲,只存在两种不同的类加载器:

    一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用c++实现,属于虚拟机的一部分,并没有实现java.lang.classLoader类

    另一种是其他的类加载器,都由java语言实现,独立于虚拟机外部,并且全部继承自java.lang.classLoader类,java系统提供了2个实现:Extension ClassLoader和Application ClassLoader

    分别介绍一下这3个类加载器:

    启动类加载器(Bootstrap ClassLoader):负责将存放在JAVA_HOME/lib目录中的,或者被-Xbootclasspath参数特意指定的路径中的几个特定名称的jar包类库(比如rt.jar)加载到jvm内存中,这个启动器无法被java程序直接引用(只有native本地方法可以调用它加载类),如果用户在自定义类加载器时想委托它来加载类,可以直接使用null代替(后面会说为什么)

    扩展类加载器(Extension ClassLoader):这个类的实现在sun.mise.Launcher里面,它负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量特意指定的路径中的所有类库

    应用程序类加载器(Application ClassLoader):这个类的实现也在sun.mise.Launcher里面,它也是ClassLoader中getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器,它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用,如果应用程序没有自己定义过类加载器,这个就是程序中默认的了

    二、双亲委派机制

    每个类加载器都拥有一个独立的类名称空间,所以如果有2个不同的类加载器,即使它们加载了同1个class文件,得到的2个类都是不同的

    这种特性会产生一个坏处,比如用户自己另外写了一个java.lang.Object类用某个类加载器加载了,系统中将会出现2个Object类,应用程序的表现将会出现混乱,为了保证java程序的稳定,jvm的类加载器之间存在一种层次关系,叫做双亲委派机制(当然用java.lang.Object类只是举个例子,jvm中会判断如果类路径以"java"开头就不予加载并报错,这也是一种保护)

    通过这张有点包浆的图可以看到其实jvm的类加载器是有父子关系的,类加载器的基类ClassLoader中有一个parent属性:

    每个继承了ClassLoader的类加载器的parent属性就是它的父级,比如Application ClassLoader的parent属性是Extension ClassLoader,Extension ClassLoader的parent属性是null(为什么Bootstrap ClassLoader就是null了呢,因为它是c++实现的,java中没法直接引用)

    为什么要有这层父子关系呢?核心就在基类ClassLoader的loadClass方法里:

    方法的含义是当被调用时,先看一下自身类加载器有没有加载过这个类,如果加载过就返回,如果没加载过,不会自己加载,是先调用父类的loadClass方法,让父类去加载

    到了父类,同理,如果没加载过,就去找爷爷类,而不是自己加载,直到parent属性为null,表示到顶了爸爸是Bootstrap ClassLoader了,就调用findBootstrapClassOrNull这个native本地方法使用启动类加载器加载类试一试

    如果这个类是"java.lang.Object"这种放在JAVA_HOME/lib/rt.jar中的系统类的话,爷爷类就加载成功了,一路返回,如果这个类就是自己用户路径ClassPath下的,那爷爷类加载失败,再回到父类,如果还没有再回到当前类,然后才会往下走进入c==null的情况,调用findClass()方法自己去加载类

    再看一下书本中对双亲委派模型的描述就非常清晰了:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当附加再起反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

    这是一种使用组合关系来复用父加载器的逻辑,双亲委派的好处是:

    任何一个类加载器要加载java.lang.Object类的话,都会最终委派给处于模型最顶端的启动类加载器进行加载,而不是每个加载器自己加载一个Object造成混乱

    既然Application ClassLoader和Extension ClassLoader的实现都在sun.mise.Launcher里面,那最后我们来看看Launcher的代码:

    看到Launcher的无参构造函数中,创建了Extension ClassLoader和Application ClassLoader这对父子.. 它们的实现类是静态的,在Launcher中作为静态内部类,都继承自URLClassLoader,这里不展开,着重看一下创建方法的入参

    创建Extension ClassLoader的getExtClassLoader()方法是没有入参的,进去可以看到:

    一路看到最终调用了父类super的构造方法,第二个参数类加载器ClassLoader这里传了null,在最终的父类ClassLoader中看到:

    所以parent=null了,就表示Extension ClassLoader的爸爸是无法被引用的Bootstrap ClassLoader,再回过去看一眼前面的loadClass方法瞬间就明白了,同理如果你写自定义类加载器,父级想委托给Bootstrap ClassLoader的话,传null即可

    再看创建Application ClassLoader的getAppClassLoader(var1)方法,传入了var1参数,这个var1就是刚才创建的Extension ClassLoader,代码调进去看super构造方法第2个参数就是传的这个var1,这就是认爸爸了..

    三、破坏双亲委派模型

    双亲委派模型是在jdk1.2才被引入的,设计者推荐给开发者的一种类加载器实现方式,不是强制的,因为loadClass是可被重写的,在实际情况中也存在破坏这个模型的使用方式

    在jdk1.0版本时ClassLoader和它的loadClass方法就已经存在了,当时的自定义类加载器都是继承ClassLoader然后重写loadClass的,这些都不符合,后来jdk1.2才在ClassLoader中增加了findClass方法,不提倡用户去重写loadClass方法,而是改为重写findClass方法来符合双亲委派机制(loadClass中会先去父级找,再调用findClass)

    除了遗留代码,主要有这些打破双亲委派的实现场景:SPI、OSGi、Java Web服务器(tomcat),OSGi是java模块化的规范(实现了热拔插、热部署、模块化),它避免不了模块间类加载的问题,有自己的类加载逻辑,不是很了解就不说了,说一下另外2个:

    1、jdbc与类加载器

    SPI全名是service provider Interfaces,是java为很多服务提供的接口,允许第三方为这些接口提供实现,常见的SPI有JDBC、JCE、JNDI、JAXP、JBI等,反正我只认识JDBC(在springboot的ioc容器加载时也使用到了类似spi的机制,关于SPI以后还要细说)

    这些SPI的接口由java核心库来提供,而他们的实现代码则是作为java应用依赖的jar包被包含进应用路径ClassPath里的,这里有一个很普遍的场景,一些java核心代码使用了SPI的服务,而SPI服务的实现类是由三方服务商实现的,翻译一下就是一些使用启动类加载器(Bootstrap ClassLoader)和扩展类加载器(Extension ClassLoader)加载的代码要使用到应用程序类加载器(Application ClassLoader)才能加载到的类,这就很难受了,因为Bootstrap ClassLoader加载不了JAVA_HOME/lib路径以外的jar,而三方服务商的jar都放在应用路径ClassPath里呢

    java设计团队只好引入了一个不太优雅的设计:java.lang.Thread类中定义了一个线程上下文类加载器(Thread Context ClassLoader)简称TCCL,在jvm的主线程中这个类加载器默认就是应用程序类加载器(Application ClassLoader)(子线程也默认会从父线程中继承),看之前sun.mise.Launcher构造方法的源码:

    jvm的主线程在Launcher创建完ExtClassLoader和AppClassLoader以后,在这里设置了类加载器是AppClassLoader

    这样在jvm启动后,不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作

    这种行为打破了双亲委派模型的层次,让父级类加载器可以逆向使用子级类加载器,java中所有涉及SPI的加载动作基本上都采用这种方式来实现,下面看一下在JDBC中具体是怎么做的:

    java.sql.DriverManager类是java用来管理数据库驱动的,来看一下它是怎样加载jdbc驱动具体的实现类的

    在static静态代码块中,执行loadInitialDrivers方法加载(静态代码块是类被加载时在初始化阶段会被唯一执行一次的方法,在类加载机制介绍中会说)

    ServiceLoader是jdk提供服务服务实现类的查找工具类,红框内的含义是在约定好的位置(三方服务商jar包的META-INF/services/以服务接口命名的文件中)加载Driver.class这个接口的具体实现类

    2张图一个是mysql的jdbc实现,一个是postgresql的,可以看到文件中按照约定放了实现类的包路径,看一下ServiceLoader的load方法中是用哪个类加载器来加载这个具体实现类的:

     

    主线程的getContextClassLoader,也就是AppClassLoader了,实锤

    2、tomcat的类加载器

    Java Web服务器有一些场景比较特殊,只使用jvm自带的类加载器无法满足需求,比如:

    (1)Web服务器有可能需要运行多个Web应用程序,这些应用有可能依赖相同的类库的不同版本,需要保证每个应用程序的类库都是单独隔离的,以免互相影响

    (2)多个应用程序如果依赖完全相同的类库,这些类库需要能够共享,以免造成资源浪费,以及jvm方法区的过度膨胀

    (3)有些服务器自身也是java实现的,需要将自己使用的类库与应用程序的类库隔离开来

    (4)支持Jsp应用的Web服务器,需要支持HotSwap热加载功能,因为Jsp文件改动后要编译成java class才能被jvm运行生效,Jsp的页面在开发时修改又非常频繁,所以Web服务器需要支持Jsp编译后class的热加载

    主流的Java Web服务器比如Tomcat、Jetty、WebLogic这些,都实现了自己的类加载器,下面来看看tomcat具体是怎么做的:

    对于第(2)(3)需求tomcat有3组目录来对应,每个目录配有一个类加载器负责加载

    /common/*:目录中的类可被tomcat和所有Web应用共同使用,由CommonClassLoader类加载器加载

    /server/*:目录中的类只能被tomcat使用,对Web应用不可见,由CatalinaClassLoader类加载器加载

    /shared/*:相反,被所有Web应用共享使用,对tomcat不可见,由SharedClassLoader类加载器加载

    tomcat还会为每个Web应用创建一个WebAppClassLoader,来加载每个应用/WebApp/WEB-INF/*目录下的class和jar,在这一层如果jar是通过WebAppClassLoader加载的话,就会与其他应用隔离

    应用下的每个Jsp页面对应一个Jsp类加载器JasperLoader,Jsp页面最终也是编译成class文件的,Jsp热修改后即使重新编译,class的路径和类名还是没变,用同一个类加载器中无法再次加载(因为在方法区中已经有这个类了),所以tomcat为每个Jsp页面搭配一个JasperLoader,页面修改后相应的类加载器直接卸载,重新创建类加载器,重新加载Jsp页面

    来看一下刚才说的几个类加载器的关系:

    CommonClassLoader是继承自jvm的ApplicationClassLoader,整个结构这里看是完全符合双亲委派机制的

    在tomcat的6.x以后,为了简化大多数的部署场景对目录做了简化,/common、/server、/shared合并为一个/lib目录,而且默认不创建CatalinaClassLoader和SharedClassLoader了,都用CommonClassLoader来代替加载,当然也可以通过配置改为之前3个目录的方式

    前面说过tomcat是违背类加载机制的,是在WebAppClassLoader里面,为了实现更好的隔离性,其实WebAppClassLoader重写了ClassLoader的loadClass方法,默认优先加载应用程序自己的jar,没有的话再委托父类去加载,和双亲委派刚好相反,下面仔细看一下它的实现:

    WebAppClassLoader实际上都是由基类WebappClassLoaderBase实现的,来看一下start方法,这里是先读取/WEB-INF/class再读取/WEB-INF/lib的,是不是解开了一个多年的疑惑?想要重写三方jar包的某个类,只要拷出来在放到自己项目里同样的路径就好了,就是因为这里/classes里的先被类加载器加载了,/lib里有相同路径的类不会再被加载

    再看loadClass是怎么被重写的吧,代码有点长,分开看:

    先从本地缓存中查找是否已经加载过该类(已经加载了的类会被缓存在resourceEntries这个数据结构中),再检查当前类加载器是否已经加载过类

    获取系统类加载器(AppClassLoader)来加载这个类,直接用这个类加载器,主要是优先把jvm的基础jar先加载进来,防止被Web应用中的类覆盖了

    接下来就是破坏双亲委派的地方了,优先使用当前类加载器findClass去加载,如果没有的话再通过它的父级类加载器走双亲委派去加载

    当然这里有一个delegateLoad属性,在tomcat中的contex.xml中添加Loader delegate="true"这个配置,可以颠倒前面的加载顺序,使WebAppClassLoader符合双亲委派机制(如果这时使用了/common或/shared并且有一个类路径和Web应用中相同的话,就以/common或者/shared里的类为准了)

    3、spring中类加载的设计优化

    spring应用有一个特点,它会在程序启动时会去读取配置文件来加载类,在通过Web容器启动时,spring的jar可能是和Web应用放在同一目录中的,这没什么问题,但是如果spring的jar放在了/common或/shared中(为了多个Web应用资源共用目的),就会有一个似曾相识的尴尬问题出现:父级的CommonClassLoader或者SharedClassLoader想要加载WebAppClassLoader才能加载到的路径下的jar了

    来看一下spring是怎么解决这个问题的:

    spring最终要加载bean是在initWebApplicationContext中进行的,在红框内进行了bean的加载操作,继续调进去看:

    下图ClassUtils.getDefaultClassLoader()中可以看到是使用当前线程的类加载器,也就是WebAppClassLoader(在tomcat中启动应用的线程TCCL默认是WebAppClassLoader,不是jvm自带的AppClassLoader了)

    determineContextClass内方法的含义是如果用户在web.xml里配置过spring的类的应用上下文,就使用当前线程的类加载器来加载,如果这时spring的jar是在/common中加载的也没关系,上下文相关的类还是由WebAppClassLoader来加载的

    如果用户没有配置过,就使用ContextLoader.class.getClassLoader()这个加载了ContextLoader的类加载器,如果这时spring的jar是在/common中加载的,就是CommonClassLoader

    再回到initWebApplicationContext方法继续往下看:

    currentContext是ContextLoader中的一个静态属性,存放当前的应用上下文,currentContextPerThread是Map版的currentContext,使用当前类加载器作为key

    红框内的翻译过来就是如果加载了ContextLoader的类加载器等于当前线程的类加载器(spring的jar放在应用中的情况),就把前面创建好的应用上下文this.context设置给currentContext

    如果如果加载了ContextLoader的类加载器不等于当前线程的类加载器,那ContextLoader就是CommonClassLoader或者SharedClassLoader加载的(spring的jar放在外层公共目录的情况),就把应用上下文存进currentContextPerThread里,使用当前类加载器作为key来查找使用

    以上就是spring对于自己放在Web服务器中启动做的类加载优化,spring使用TCCL来判断处理自己的jar被放在/common或/shared中加载的情况,这样在这种场景下即使多个Web应用共用一套spring的jar,也不会互相影响到

  • 相关阅读:
    提升request模块的效率--线程池
    selenium的用法
    SVN 常用命令
    SVN 常见问题及解决方法
    Makefile 详解
    开心一刻(一)
    Vim配置及其他注意事项
    彩虹表
    C++学习之STL(二)String
    C++学习之STL(一)vector
  • 原文地址:https://www.cnblogs.com/ctxsdhy/p/12679370.html
Copyright © 2011-2022 走看看