类加载本质
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行验证、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java虚拟机的类加载机制。
类加载过程
其实,虚拟机并没有明确规定类的二进制字节流从哪里获取,这也是Java虚拟机强大的原因,而许多Java技术的基础也是建立在这个基础之上的。比如:
类加载器
虚拟机设计团队将类加载阶段的类加载过程中的加载动作,放到虚拟机外部实现,也就是可以运行开发人员自己觉得如何获取所需要的类,这个加载动作的实现就是类加载器。
双亲委派模型
虚拟机提供了一个非强制性的类加载机制,即双亲委派模型,他的过程是如果一个类加载器收到了类加载的请求,它首先不会自己取尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该首先传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己取加载。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 类加载静态锁 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) { long t1 = System.nanoTime(); c = findClass(name); // 调用当前类加载器
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; }}
其实对于虚拟机来说,只有两种加载器,一种是启动类加载器,是有C++语言实现的,是虚拟机的一部分;另外一部分是所有的其他类加载器,由Java语言编写,独立于虚拟机之外,都继承自抽象类java.lang.ClassLoader。
另外,双亲委派模型还要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。这就形成了一种加载器的父/子关系。
类加载器的分类及作用
类加载器的补充和应用场景
类的唯一性是由类的全限定名和加载它的类加载器一同确定的,也就是被不同类加载器加载的两个相同的类,也必定不相等。
上下文类加载器
双亲委派模型可以解决大部分类加载的问题,但不能解决所有场景。虚拟机允许灵活的设置类加载器,从而使类加载体系更加灵活。接下来我们先了解一下线程上下文加载器,以及使用上下文加载器的一些场景。
SPI机制与JDBC驱动类加载
我们都知道Java的SPI机制是一种JDK内置的服务提供发现机制,比如java.sql.Driver接口,可以由不同的数据库厂商实现此接口,来提供具体数据库的驱动,并且需要按照SPI的规范,在jar包的META-INF/services/目录里,创建一个以服务接口命名的文件。
我们知道java.sql.Driver接口是在rt.jar下,也就是由启动类加载器负责加载,但是各种厂商的实现类在ClassPath下,不应由启动类加载器加载,这样就违背了双亲委派模型,这时我们看DriverManager.java的源码:
private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; }
AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); 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); } }}
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl);}
使用到ServiceLoader.load方法去加载具体的驱动实现,而ServiceLoader内部使用的是Thread.currentThread().getContextClassLoader();获取的类加载器,这个加载器就是上下文类加载器,完美的补充了双亲委派模型不能加载SPI机制实现类的问题。上下文类加载器默认就是AppClassLoader,当然我们可以自己实现上下文类加载器(TCCL),并通过Thread.currentThread().setContextClassLoader(ClassLoader cl)来灵活指定。
Tomcat类加载
Tomcat作为Web服务器,通常允许部署多个应用,那么多个应用之间怎么去做同一个类的隔离呢,这就需要由不同应用中不同类加载器来加载,才能实现隔离,而且多个应用也存在共用部分,下面我们结合Tomcat源码分析一下Tomcat的类加载器。
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;}
ClassLoader commonLoader = null;ClassLoader catalinaLoader = null;ClassLoader sharedLoader = null;
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); }}
Bootstrap.java中初始化Tomcat类加载器,分别是commonLoader、catalinaLoader、sharedLoader。通过设置上下文类加载器可以灵活的使用不同类加载器加载应用中的类,完成多应用部署和热部署。
commonLoader是Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader是Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader是各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader是各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃。当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
P.S. Tomcat不是彻底打破了双亲委派模型,而是灵活的使用上下文类加载器来解决热部署、多个应用部署等场景,顶层类加载还是遵循双亲委派模型的。
Dubbo类加载
Dubbo也是基于SPI机制实现的架构,JDK默认的SPI机制类加载器ServiceLoader,会在META-INF/services下获取接口的所有实现类,虽然也提供了延迟加载,但也基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。
Dubbo对JDK的SPI机制进行了扩展:
OSGI类加载
OSGI是Java模块化标准,而OSGI实现模块化热部署的关键则是它自定义的类加载器机制的实现,每个程序模块(OSGI中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热部署。在OSGI环境下,类加载器不再是双亲委派模型中的树形结构,而是进一步发展为更加复杂的网状结构,笔者没有做过OSGI相关开发,这里类加载过程不做赘述。
参考资料: