- 类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)
- 启动(Bootstrap)类加载器启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
- 扩展(Extension)类加载器:扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
- 系统(System)类加载器也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。
- 理解双亲委派模式:JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用),因此所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载,因为这些基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API(当然,也存在基础类回调用户用户代码的情形,即破坏双亲委派模型的情形)。
- 1.系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
- 2.扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null(null 本身就代表着引导类加载器)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
- 事实上,这就是启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系。
- 一般用户自定义类加载器的工作流程
- 1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;
- 2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
- 3、调用本类加载器的findClass(…)方法,试图获取对应的字节码。如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败, 向上抛异常给loadClass(…), loadClass(…)转而调用findClass(…)方法处理异常,直至完成递归调用。
- Java SPI:
- 我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。
- 约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。
- 问题所在:SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;而SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的(因为它只加载 Java 的核心库),按照双亲委派模型,启动类加载器无法委派系统类加载器去加载类。也就是说,类加载器的双亲委派模式无法解决这个问题。线程上下文类加载器正好解决了这个问题。线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。
- 线程上下文类加载器:Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。
- 违背双亲委派案例之JDBC:连接接口必须由bootstrpClassLoader加载器加载,但是连接实现不再rt.jar中。实现类需要其他类加载器帮助加载(上下文加载器),
- 1、JDBC驱动注册的常用几种方式:Java数据库连接(Java Database Connectivity,简称 JDBC)是Java语言用来规范客户端程序如何访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。 JDBC驱动包就是上述接口的实现,由数据库厂商开发,是java和具体数据库之间的连接桥梁。每一种数据库对应一款驱动jar,甚至每一个版本的数据库都有自己对应版本的驱动。我们知道,JDBC规范中明确要求Driver(数据库驱动)类必须向DriverManager注册自己,所以在与数据库交互前必须完成驱动注册,那么先来看看平时我们是如何注册JDBC驱动的。
- 方式一:Class.forName(“com.mysql.jdbc.Driver”):使用该方式注册的关键在于 Class.forName(driver);,这句话的作用是加载并初始化指定驱动。mysql jdbc正是在Driver初始化的时候完成注册:
- 方式二:System.setProperty(“jdbc.drivers”,“com.mysql.jdbc.Driver”):这种方式是通过系统的属性设置注册驱动,最终还是通过系统类加载器完成。
- 方式三:SPI服务加载机制注册驱动:
- 各位可以发现,这种方式与第一种方式唯一的区别就是经常写的Class.forName被注释掉了,但程序依然可以正常运行,这是为什么呢?这是因为,从JDK1.6开始,Oracle就修改了加载JDBC驱动的方式,即JDBC4.0。在JDBC 4.0中,我们不必再显式使用Class.forName()方法明确加载JDBC驱动。当调用getConnection方法时,DriverManager会尝试自动设置合适的驱动程序。前提是,只要mysql的jar包在类路径中。
- 方式一:Class.forName(“com.mysql.jdbc.Driver”):使用该方式注册的关键在于 Class.forName(driver);,这句话的作用是加载并初始化指定驱动。mysql jdbc正是在Driver初始化的时候完成注册:
- 2、SPI服务加载机制注册驱动原理分析
- DriverManager.getConnection()中。我们知道,调用类的静态方法会初始化该类,而执行其静态代码块是初始化类过程中必不可少的一环。DriverManager的静态代码块;初始化方法loadInitialDrivers()的代码我们其实已经见过了,第二种和第三种的驱动注册逻辑都在这里面;
- 从上面可以看出,JDBC中的DriverManager加载Driver的步骤顺序依次是:通过SPI方式,读取 META-INF/services 下文件中的类名,使用线程上下文类加载器加载;通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。driversIterator.next()这条语句完成了驱动的注册工作
- 1)加载是实现类,此时没有初始化,以JDBC为例,此时没有完成驱动注册
- 2)判断SPIservice是否为驱动器实现
- 3)若判为真,.newInstance()出发初始化动作JDBC完成注册;
- 4)因为Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader(启动类加载器只能加载java核心类库)。这时候只能使用线程上下文类加载器了:把自己加载不了的类加载到线程上下文类加载器中(通过Thread.currentThread()获取),而线程上下文类加载器默认是使用系统类加载器AppClassLoader。回头再看ServiceLoader.load(Class)的代码,的确如此:
- 5)JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载。但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的线程上下文类加载器里,后续你想怎么操作就是你的事了。
- DriverManager.getConnection()中。我们知道,调用类的静态方法会初始化该类,而执行其静态代码块是初始化类过程中必不可少的一环。DriverManager的静态代码块;初始化方法loadInitialDrivers()的代码我们其实已经见过了,第二种和第三种的驱动注册逻辑都在这里面;
- 1、JDBC驱动注册的常用几种方式:Java数据库连接(Java Database Connectivity,简称 JDBC)是Java语言用来规范客户端程序如何访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。 JDBC驱动包就是上述接口的实现,由数据库厂商开发,是java和具体数据库之间的连接桥梁。每一种数据库对应一款驱动jar,甚至每一个版本的数据库都有自己对应版本的驱动。我们知道,JDBC规范中明确要求Driver(数据库驱动)类必须向DriverManager注册自己,所以在与数据库交互前必须完成驱动注册,那么先来看看平时我们是如何注册JDBC驱动的。
- Tomcat与Spring的类加载器案例
- 在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:
- 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用;
- 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见;
- 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见;
- 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
- 为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示; CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/、/server/、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。
- 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?spring根本不会去管自己被放在哪里,它统统使用线程类加载器来加载类,而线程类加载器默认设置为了WebAppClassLoader。也就是说,哪个WebApp应用调用了Spring,Spring就去取该应用自己的WebAppClassLoader来加载bean。
- 通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:
- 1)当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
- 2)当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
- 在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:
JVM类加载器
最新推荐文章于 2024-02-13 19:44:32 发布