这一节将分析,Tomcat启动以及请求处理过程中所涉及到的类与对象,是由谁加载的,Tomcat的类加载器的特点。
4.1 JAVA的类加载过程
1)类加载load:从字节码二进制文件.class文件将类加载到内存将内存中的class放到运行时数据区的方法区内。类的初始化过程会在堆区建立一个java.lang.Class对象,用来封装该类相关的数据结构。
2) 连接:连接又分为以下小步骤
a) 验证:出于安全性的考虑,验证内存中的字节码是否符合JVM的规范,类的结构规范、语义检查、字节码操作是否合法、这个是为了防止用户自己建立一个非法的XX.class文件就进行工作了,或者是JVM版本冲突的问题,比如在JDK6下面编译通过的class(其中包含注解特性的类),是不能在JDK1.4的JVM下运行的。
b) 准备:将类的静态变量进行分配内存空间、初始化默认值。(对象还没生成呢,所以这个时候没有实例变量什么事情)
c) 解析:不同的JVM实现可能选择不同的解析策略。一种做法是递归的把所有依赖的形式引用(只是申明,并不适用类)都进行解析。而另外的做法则可能是只在一个形式引用真正需要的时候(也就是说在所引用类的对象被创建)才进行解析。也就是说如果一个Java类只是被引用了,但是并没有被真正用到,那么这个类有可能就不会被解析。当前的JVM一般都采用了第二种策略。
3) 类的初始化: 将类的静态变量赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值。
4.2 类加载器的加载模式
Parent First加载模式(也就是在加载的时候会委托给父亲类加载器优先加载的模式)只是JVM的一种默认实现,并不是所有的类加载都需要实现该模式的。
Parent Last加载模式,Tomcat的应用类加载器,WebappClassLoader,每一个Context都会尝试优先加载所需要的类,只有在无法加载到的时候才委托给父亲加载器完成加载操作。之所以实现这样一种策略为了在多应用的环境中,例如应用A,B使用了默认的数据库连接实现,而应用C需要定制,那么这样C应用中只要包含该实现就可以实现覆盖默认的实现。
4.3 Tomcat7的类加载器的层级结构
Tomcat7运行时类的加载说明:
1)Bootstrap Class Loader是JVM的内核由C实现的,加载了JVM的核心包rt.jar。rt.jar中的所有类执行其class的getClassLoader()方法都将返回null,例如String.class.getClassLoader()。
2)Extension Class Loader主要加载了JVM扩展包中相关的jar包。例如运行下列代码将System.out.println(ZipPath.class.getClassLoader());将得到如下的运行结果:sun.misc.Launcher$ExtClassLoader
3)System Class Loader加载CLASSPATH相关的类,例如在Tomcat的Bootstrap的main方法中执行System.out.println(Bootstrap.class.getClassLoader());则将得到:sun.misc.Launcher$AppClassLoader
4)Common Class Loader,Tomcat7中的CATALINA_HOME/lib下的jar包。注意Tomcat在启动文件中将启动时配置了-classpath "%CATALINA_HOME%\lib\catalina.jar"因此catalina.jar中的类虽然指定使用类加载器Common Class Loader,但是按JVM的委托加载原则System.out.println(Bootstrap.class.getClassLoader());得到的类加载器是:sun.misc.Launcher$AppClassLoader。
5)Webapp Class Loader, 主要负责加载Context容器中的所有的类。实际上该加载器提供了参数delegateLoad供用户设定是否使用parent-first加载。默认该值为false,默认用parent-last加载。出于安全性的考虑对于核心类WebappClassLoader是不允许加载的。包括:java.,javax.servlet.jsp.jstl,javax.servlet.,javax.el.
Webapp Class Loader的过程如下:
- 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在
resourceEntries
这个数据结构中),如果已经加载即返回,否则 继续下一步。 - 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
- 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
- 最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。
4.4 类加载器的相关类结构图
4.5 类加载器的相关源代码
(一)ClassLoader的load方法
- 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) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- }
- 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)在该方法中,首先检查是否已经加载了该类,这里有个问题JVM如何判断一个类是否被加载过的?这里涉及到了类的命名空间问题。在JAVA中判断一个类是否相同不仅看类名是否相同,还要看其类加载器是否相同。同一个类可以被不同的类加载器所加载,并且认为是不同的。该问题可以分解下面两个方面看。
a) 单一加载原则:在加载器链中,一个类只会被链中的某一个加载器加载一次。而不会被重复加载。实现类的共享,Tomcat多个应用,如果需要共享一些jar包,那么只需要交给commonClassLoader加载,那么所有的应用就可以共享这些类。
b) 可见性原则:父加载器加载的类,子加载器是可以访问的。而自加载器所加载的类,父加载器无法访问。不同加载器链之间其是相互不可见,无法访问的。实现隔离,Tomcat就是应用该特性,为每一个Context容器创建一个WebappClassLoader类加载器对象,从而实现了应用间的相互隔离。应用间的类是不可见的所以无法相互访问。
2) 如果步骤一中无缓存,查看该类父加载器,如果存在那么委托给付加载器。如果没有父加载器那么认为BootstrapClassLoader是其父加载器,委托进行加载。
3)如果父加载器无法加载则抛出ClassNotFoundException,调用抽象方法findClass方法。
4)此处的resolveClass方法指的是上文类加载过程中连接的第三步操作。resolve该类的形式引用等等。
(二)类URLClassLoader的findClass方法
- protected Class<?> findClass(final String name)
- throws ClassNotFoundException
- {
- try {
- return AccessController.doPrivileged(
- new PrivilegedExceptionAction<Class>() {
- public Class run() throws ClassNotFoundException {
- String path = name.replace('.', '/').concat(".class");
- Resource res = ucp.getResource(path, false);
- if (res != null) {
- try {
- return defineClass(name, res);
- } catch (IOException e) {
- throw new ClassNotFoundException(name, e);
- }
- } else {
- throw new ClassNotFoundException(name);
- }
- }
- }, acc);
- } catch (java.security.PrivilegedActionException pae) {
- throw (ClassNotFoundException) pae.getException();
- }
- }
该方法的核心是获取到JAVA类的字节码,然后调用父类的defineClass方法完成类的构造过程。defineClass是由JVM实现的,不允许被覆写,因此用户类文件就必须遵循JVM的文件规范才能被正确的解析。
(三)WebappClassLoader重新覆写了ClassLoader的loadClass方法(删除了部分代码)
- @Override
- public synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- Class<?> clazz = null;
- ………
- // (0) 当前对象缓存中检查是否已经加载该类
- clazz = findLoadedClass0(name);
- // (0.1) 检查JVM的缓存,是否已经加载过该类
- clazz = findLoadedClass(name);
- // (0.2) 防止加载一些系统相关的类
- try {
- clazz = system.loadClass(name);
- if (clazz != null) {
- if (resolve)
- resolveClass(clazz);
- return (clazz);
- }
- } catch (ClassNotFoundException e) {}
- boolean delegateLoad = delegate || filter(name);
- // (1) 如果配置了parent-first模式,那么委托给父加载器
- if (delegateLoad) {
- ClassLoader loader = parent;
- if (loader == null) loader = system;
- try {
- clazz = Class.forName(name, false, loader);
- if (clazz != null) {
- if (resolve) resolveClass(clazz);
- return (clazz);
- }
- } catch (ClassNotFoundException e) {}
- }
- // (2) 从应用环境中查找类,主要是应用下的/lib目录与classes目录
- try {
- clazz = findClass(name);
- if (clazz != null) {
- if (resolve)
- resolveClass(clazz);
- return (clazz);
- }
- } catch (ClassNotFoundException e) {}
- // (3) 如果在当前应用下无法找到所需要的类,再委托给父加载器加载(parent-last)
- if (!delegateLoad) {
- ClassLoader loader = parent;
- if (loader == null)
- loader = system;
- try {
- clazz = Class.forName(name, false, loader);
- if (clazz != null) {
- if (resolve)
- resolveClass(clazz);
- return (clazz);
- }
- } catch (ClassNotFoundException e) {}
- }
- throw new ClassNotFoundException(name);
- }
4.6 总结
本节介绍了JVM的类加载器原理,Tomcat的类加载器结构,类加载器的实现,以及类加载器的部分源代码。重点需要理解的是类加载器的命名空间这一设计特性。所以的其他内容都是服务该特性的,从而实现了类加载的安全性,可见性等。