类加载器的作用
- 类加载的作用是实现类的加载动作,也就是实现类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”。
- 对于任意一个类,都由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。也就是说,在用Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceOf()方法判断时,只有被同一个类加载器加载的类才相等。
类加载器的分类
双亲委派模型
应用程序是由这几种类加载器互相配合进行加载的,这几种类加载器之间的层次关系如下图。这种层次关系,称为双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
注意:这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码。
// ClassLoader源码
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
...
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
双亲委派模型的工作过程:
如果一个类加载器收到了类加载的请求,它首先把这个请求委派给父类加载器去完成,每个层次的父类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
使用双亲委派模型的好处是java类随着它的类加载器一起具备了一种带有优先级的层级关系。
// 双亲委派模型的实现在java/lang/ClassLoader的loadClass()方法中,
// 过程是:先检查是否被加载,如果没有,则调用父类的loadClass()加载,若父类加载失败,再调用自己的findClass()去加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
破坏双亲委派模型的例子
1、SPI
双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐给开发者的一种类加载器实现方式。在java世界中大部分的类加载器都遵循这个模型,但也有例外。比如:java中涉及SPI的加载动作,如JNDI、JDBC等,这些代码由jvm提供统一标准,然后调用由独立厂商实现并部署在应用程序的ClassPath下的SPI代码,但是启动类不可能认识应用程序的代码。
为了解决这一问题,java设计团队引入了线程上下文类加载器Thread Context ClassLoader。这个类加载器可以通过java.lang.Thread的setContextClassLoader()方法进行设置。如果创建线程是还未设置,它将从父线程中继承,如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。
JDBC就是使用线程上下文类加载器去加载的SPI代码。
// DriverManager # getConnection()
// 获取连接时,获取线程上下文加载器,然后使用Class.forName()指定类加载器来加载数据库驱动。
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
...
}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
JDBC的加载过程可以参考: JDBC驱动加载机制
2、Tomcat
Tomcat也是破坏双亲委派模型的典型例子。
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
-
1.部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离;
-
2.部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享;
-
3.Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响;
-
4.支持JSP应用的Web容器,要支持JSP的热替换。 为解决上面的问题,tomcat为每个应用程序提供了一组classpath,每个classpath都有一个自定义的类加载器去加载下面的类库。
在Tomcat目录结构中,有3组目录(“/common/”、“/server/”和“/shared/”)可以存放Java类库,另外还可以加上Web应用程序自身的目录“/WEB-INF/”,一共4组,把Java类库放置在这些目录中的含义分别如下:- 放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用。 - 放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。 - 放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。 - 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如下图所示。
如果一个jar在应用程序的classpath和/common/*目录下都存在,应用程序会自己来加载这个类,传统的双亲委派模型是用父加载器去加载的,这就是违背双亲委派模型的地方。
3、OSGi 更加灵活的类加载器模型
OSGi的类加载器之间的关系不再是像双亲委派模型那样简单的树形结构,而是发展成一种更复杂的、运行时才能确定的网状结构。
OSGi的各Bundle类加载器之间只有规则,没有固定的委派关系。例如,如果一个Bundle声明了一个它依赖的Package,如果有其他Bundle声明发布了这个Package,那么所有对这个Package的类加载动作都会委派给发布它的Bundle加载器去完成。不涉及某个具体的Package时,各Bundle类加载器是平级关系,只有具体使用某个Package和class时,才会根据Package导入导出定义来构造Bundle之间的委派和依赖。
参考:《深入理解Java虚拟机》