java 类加载器
类加载器基本概念
什么是类加载器
顾名思义,类加载器(class loader)用来加载 Java 类到Java 虚拟机中。
具体的说就是:Java源程序(.java 文件)在经过Java 编译器编译之后就被转换成 Java 字节代码(.class文件)。类加载器负责读取 Java字节代码,并转换成 java.lang.Class类的一个实例。
系统提供的类加载器
系统提供的类加载器主要有下面三个
引导类加载器(bootstrapClassLoader):它负责将<Java_Runtime_Home>/lib目录下核心类库的jar包加载到内存中。引导类加载器是本地方法实现的
扩展类加载器(extensionsClassLoader):它负责将< Java_Runtime_Home >/lib/ext目录下的jar包加载到内存中。
系统类加载器(systemClassLoader):它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。
若一个class同时在多个加载器路径下,会被最上级加载器抢先加载
类加载器树状组织结构示意图
类加载器的代理模式
类加载器的代理模式又被称为双亲委派机制,类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。
这里还有两个概念定义类加载器和初始化类加载器。
比如我们要加载Y类,使用A加载器来加载,A委托B加载器加载。
那么Y类最终是被B加载器加载的,B加载器就是Y类的初始化类加载器,A加载器就是Y类的定义类加载器
双亲委派机制的动机
之所以有这个双亲委派机制,是因为java虚拟机判断两个java类是否相同,看两点
1.全名相同(包名+类名)
2.类的加载器是否一样(定义类加载器)
比如一个 Java 类 com...Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的
双亲委派机制的异类—线程上下文加载器
线程上下文加载器是什么?
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。
线程上下文加载器出现的背景
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、 JBI 等。
比如JDBC,驱动类接口是在java.sql.Driver,驱动类实现是在com.mysql.jdbc.Driver(类),然后把驱动类的实现交给java.sql.DriverManager管理
SPI是在rt.jar中,被启动类加载器所加载(最先被加载);
SPI实现类是在mysql-connection.jar中,被系统加载器所加载;
如果直接把系统类加载器加载好的类交给rt.jar中的类进行管理,显然行不通的(加载顺序);
如果让启动类加载器在加载SPI接口的时候同时也去加载SPI的实现类,那就得依靠系统类加载器去加载SPI实现类了(classPath路径不属于启动类加载器的加载范围),但是,让启动类加载器委托系统类加载器去加载SPI实现类,这显然违背了双亲委派加载机制
开发自己的类加载器
一般情况,系统默认提供的类加载器实现可以满足绝大部分需求,如果一定要开发自己的类加载器的话,继承java.lang.ClassLoader,然后覆写findClass(String name)方法即可。
在Java.lang.ClassLoader的loadClass()封装了前面说的双亲委派机制,因此为了类加载器都正确实现了双亲委派机制,在开发自己的类加载器的时候,不要覆写loadClass()方法。
类加载过程
类加载过程
类加载器与 Web 容器
对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。
以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
类加载器与 OSGi
OSGi™是Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse就是基于 OSGi技术来构建的。
假设有两个模块bundleA和 bundleB,它们都有自己对应的类加载器classLoaderA和 classLoaderB。在bundleA中包含类 com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB声明了导入bundleA提供的类 com.bundleA.Sample,并包含一个类com.bundleB.NewSample继承自com.bundleA.Sample。在bundleB启动的时候,其类加载器 classLoaderB需要加载类com.bundleB.NewSample,进而需要加载类com.bundleA.Sample。由于bundleB声明了类 com.bundleA.Sample是导入的,classLoaderB把加载类 com.bundleA.Sample的工作代理给导出该类的bundleA的类加载器classLoaderA。classLoaderA在其模块内部查找类com.bundleA.Sample并定义它,所得到的类com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以java开头的类,都是由父类加载器来加载的。如果声明了系统属性org.osgi.framework.bootdelegation=com.example.core.*,那么对于包com.example.core中的类,都是由父类加载器来完成的。
简短的说就是c1是A定义并被声明导出,c2是B声明并导入c1(c2继承c1),其中c2是B加载,c1是A加载。
OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在Java 虚拟机中,带来了很大的灵活性。