双亲委派模型
类加载器的划分
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用程序类加载器(Application Classloader)
启动类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
扩展类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。 应用程序类加载器由sun.misc.Launcher$App-ClassLoader实现。应用程序类加载器也称之为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。应用程序中如果没有自定义类加载器,那一般情况下应用程序类加载器就是程序中默认的类加载器。双亲委派模型的工作过程
双亲委派模型要求除了顶层的启动类加载器外,其余的父类加载器都应当有自己的父类加载器。这里的父类加载器之前的父子关系一般不会以继承的关系来实现,而都使用组合关系来复用父加载器的代码。 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法中:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的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; }}复制代码
双亲委派模型的作用
Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序在程序的各种类加载器环境中都是同一个类。
破坏双亲委派模型
Java有许多核心库提供的SPI接口,允许第三方为这些接口提供实现,常见的有JDBC、JNDI等。问题是SPI接口是核心库的一部分,是由启动类加载器来加载的,而SPI接口的实现类是由应用程序类加载器来加载的。根据双亲委派模型,Bootstrap ClassLoader无法委派Application Classloader来加载类(有关Java的SPI可以参考中2.1节的Java SPI例子)。为了解决这个问题,我们引入一个线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
我们来看下JDBC的驱动管理类DriverManager的代码: 初始化jdbc驱动类/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the { @code ServiceLoader} mechanism */static { loadInitialDrivers(); println("JDBC DriverManager initialized");}复制代码
private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() AccessController.doPrivileged(new PrivilegedAction () { public Void run() { ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } }}复制代码
注意上面的ServiceLoader的load方法,这里就用到了线程上下文类加载器。
public staticServiceLoaderload(Classservice) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl);}复制代码
Tomcat的类加载架构
Tomcat作为一个web容器需要解决下面几个问题:
- 部署在同一服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离,因为两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。
- 部署在同一服务上的两个Web应用程序所使用的Java类库可以互相共享。
- 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。一般来说,服务器所使用的类库应该与应用程序的类库互相独立。
- 支持JSP应用的Web服务器,大多数需要支持Hotswap功能。
Tomcat类加载架构如下图:
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader分别加载/common/*、/server/*、/shared/*(Tomcat6已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和JSP类加载器通常会存在多个实例,每个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个JSP类加载器。- CommonClassLoader加载路径中的class可以被Tomcat和所有的Web应用程序共同使用
- CatalinaClassLoader加载路径中的class可被Tomcat使用,对所有的Web应用程序都不可见
- SharedClassLoader加载路径中的Class可被所有的Web应用程序共同使用,但对Tomcat自己不可见
- WebappClassLoader加载路径中的Class只对当前webapp可见,对Tomcat和其他Web应用程序不可见
从委派关系可以看出:
- 各个WebappClassLoader实例之间相互隔离
- CommonClassLoader和SharedClassLoader实现了公有类库的共用
- CatalinaClassLoader和SharedClassLoader自己能加载的类与对方相互隔离
- JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class
先从Tomcat的启动方法bootstrap.main方法入手
/** * Main method and entry point when starting Tomcat via the provided * scripts. * * @param args Command line arguments to be processed */public static void main(String args[]) { synchronized (daemonLock) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } daemon = bootstrap; } else { // When running as a service the call to stop will be on a new // thread so make sure the correct class loader is used to // prevent a range of class not found exceptions. Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } } try { String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; daemon.load(args); daemon.start(); } else if (command.equals("stopd")) { args[args.length - 1] = "stop"; daemon.stop(); } else if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); if (null == daemon.getServer()) { System.exit(1); } } else if (command.equals("stop")) { daemon.stopServer(args); } else if (command.equals("configtest")) { daemon.load(args); if (null == daemon.getServer()) { System.exit(1); } System.exit(0); } else { log.warn("Bootstrap: command \"" + command + "\" does not exist."); } } catch (Throwable t) { // Unwrap the Exception for clearer error reporting if (t instanceof InvocationTargetException && t.getCause() != null) { t = t.getCause(); } handleThrowable(t); t.printStackTrace(); System.exit(1); }}复制代码
看下init方法,init方法中执行Thread.currentThread().setContextClassLoader方法,将当前线程上下文类加载器设为catalinaLoader。
/** * Initialize daemon. * @throws Exception Fatal initialization error */public void init() throws Exception { initClassLoaders(); Thread.currentThread().setContextClassLoader(catalinaLoader); SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method if (log.isDebugEnabled()) log.debug("Loading startup class"); Class startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); // Set the shared extensions class loader if (log.isDebugEnabled()) log.debug("Setting startup class properties"); String methodName = "setParentClassLoader"; Class paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance;}复制代码
看下initClassLoaders方法。
private void initClassLoaders() { try { commonLoader = createClassLoader("common", null); if( commonLoader == null ) { // no config file, default to this loader - we might be in a 'single' env. commonLoader=this.getClass().getClassLoader(); } catalinaLoader = createClassLoader("server", commonLoader); sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); }}复制代码
可以看到commonLoader、catalinaLoader、sharedLoader都是在initClassLoaders方法中初始化的。对于Tomcat6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和shared.loader项后才会真正建立CatalinaClassLoader和SharedClassLoader的实例,否则会用到这两个类加载器的地方都会用CommonClassLoader的实例代替,而默认的配置文件没有设置这两个loader项,所以Tomcat6.x把/common、/server和/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用。
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"server.loader=shared.loader=复制代码
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; value = replace(value); Listrepositories = new ArrayList<>(); String[] repositoryPaths = getPaths(value); for (String repository : repositoryPaths) { // Check for a JAR URL repository try { @SuppressWarnings("unused") URL url = new URL(repository); repositories.add(new Repository(repository, RepositoryType.URL)); continue; } catch (MalformedURLException e) { // Ignore } // Local repository if (repository.endsWith("*.jar")) { repository = repository.substring (0, repository.length() - "*.jar".length()); repositories.add(new Repository(repository, RepositoryType.GLOB)); } else if (repository.endsWith(".jar")) { repositories.add(new Repository(repository, RepositoryType.JAR)); } else { repositories.add(new Repository(repository, RepositoryType.DIR)); } } return ClassLoaderFactory.createClassLoader(repositories, parent);}复制代码