ClassLoader是个抽象类,它还有很多子类,我们如果要实现自己的ClassLoader,一般都会继承URLClassLoader这个字类,因为这个类已经帮我们实现了大部分工作,我们只需要在适当的地方做些修改就好了,就像我们要实现Servlet时通常会直接继HttpServlet —样 。
ClassLoader的方法简介
defineClass 将byte字节流解析成JVM能够识别的Class对象,有了这个方法意味着我们不仅仅可以通过class文件实例化对象,还可以通过其他方式实例化对象,如我们通过网络接收到一个类的字节码,拿这个字节码流直接创建类的Class对象形式实例化对象。注意,如果直接调用这个方法生成类的Class对象,这个类的Class对象还没有resolve,这个resolve将会在这个对象真正实例化时才进行.
findClass defineClass通常是和findClass方法一起使用的,我们通过直接覆盖ClassLoader父类的findClass方法来实现类的加载规则,从而取得要加载类的字节码。然后调用defineClass方法生成类的Class对象,如果你想在类被加载到JVM中时就被链接(Link),那么可以接着调用另办一个 resolveClass方法,当然你也可以选择让JVM来解决什么时候才链接这个类。
loadClass 如果你不想重新定义加载类的规则,也没有复杂的处理逻辑,只想在运行时能够加载自己指定的一个类,那么你可以用 this.getClass().getClassLoader().loadClass("class")调用ClassLoader的loadClass方法以获取这个类的Class对象,这个loadClass还有重载方法,你同样可以决定在什么时候解析这个类。
双亲委托加载机制
整个JVM平台提供三层ClassLoader。
Bootstrap classLoader:采用native code实现,是JVM的一部分,主要加载JVM自身工作需要的类,如java.lang.*、java.uti.*等; 这些类位于$JAVA_HOME/jre/lib/rt.jar。Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls[i].toExternalForm()); }System.out.println(System.getProperty(
"sun.boot.class.path"
));
ExtClassLoader:扩展的class loader,加载位于$JAVA_HOME/jre/lib/ext目录下的扩展jar。它服务的特定目标在System.getProperty("java.ext.dirs”)目录下。
AppClassLoader:系统class loader,父类是ExtClassLoader,它服务的特定目标在System.getProperty("java.class.path”)目录下,这个目录就是我们经常用到的classpath。它负责加载应用程序主函数类。不管你是直接实现抽象类ClassLoader,还是继承URLClassLoader类,或者其他子类,它的父加载器都是AppClassLoader,因为不管调用哪个父类构造器,创建的对象都必须最终调用getSystemClassLoader()作为父加载器。而getSystemClassLoader()方法获取到的正是 AppClassLoader。
ps:
很多文章在介绍ClassLoader的等级结构时把Bootstrap ClassLoader也列在ExtClassLoader的上一级中,其实Bootstrap ClassLoader并不属于JVM的类等级层次,因为Bootstrap ClassLoader并没有遵守ClassLoader的加载规则。另外Bootstrap ClassLoader并没有子类,ExtClassLoader 的父类也不是 Bootstrap ClassLoader,ExtClassLoader 并没有父类,我们在应用中能提取到的顶层父类是ExtClassLoader。
ExtClassLoader 和AppClassLoader都位于sun.misc.Launcher 类中,它们是Launcher类的内部类,ExtClassLoader 和 AppClassLoader 都继承了 URLClassLoader 类,而 URLClassLoader又实现了抽象类ClassLoader,在创建Launcher对象时首先会创建ExtClassLoader,然后将ExtClassLoader作为父加载器创建 AppClassLoader 对象,而通过 Launcher.getClassLoader()方法获取的ClassLoad就是AppClassLoader对象。所以如果在Java应用中没有定义其他ClassLoader,那么除了 System.getPropeirty("java.ext.dirs")目录下的类是由ExtClassLoader加载外,其他类都由AppClassLoader来加载。
public Launcher() { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { //var1作为appclassload的父类 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } //launcher的加载器设成appclassload Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if(var2 != null) { SecurityManager var3 = null; if(!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch{
....... } } else { var3 = new SecurityManager(); } if(var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } }
ClassLoader加载类的过程
JVM加载class文件到内存的两种方式,其实这两种方式是混合使用的,例如,我们通过自定义的ClassLoader显示加载一个类时,这个类中又引用了其他类,那么这些类就是隐式加载的。
- 隐式加载:不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存,例如:当类中继承或者引用某个类时,JVM在解析当前这个类不在内存中时,就会自动将这些类加载到内存中。
- 显示加载:在代码中通过ClassLoader类来加载一个类,例如调用this.getClass.getClassLoader().loadClass()或者Class.forName()。
加载字节码的步骤:
- 找到.class文件并把这个文件加载到内存中
- 字节码验证,Class类数据结构分析,内存分配和符号表的链接
- 类中静态属性和初始化赋值以及静态代码块的执行
加载字节码到内存
其实在抽象类ClassLoader中并没有定义如何去加载,如何去找到指定类并且把它的字节码加载到内存需要的子类中去实现,也就是要实现findClass()方法。我们看一下子类URLClassLoader 是如何实现 flndClass()的,在 URLClassLoader 中通过一个 URLClassPath 类帮助取得要加载的class文件字节流,而这个URLClassPath定义了到哪里去找这个class文件,如果找到了这个class文件,再读取它的字节流,通过调用defineClass()方法来创建类对象。
看URLClassLoader类的构造函数,我们可以发现必须要指定—个URL数据才能够创建URLClassLoader对象,也就是必须要指定这个ClassLoader默认到哪个目录下去杳找class 文件。这个URL数组也是创建URLClassPath对象的必要条件。从URLClassPath的名字中就可以发现它是通过URL的形式来表示ClassPath路径的。
在创建URLClassPath对象时会根据传过来的URL数组中的路径来判断是文件还是jar包,根据路径的不同分别创建FileLoader或者JarLoader,或者使用默认的加载器。当JVM调用findClass时由这几个加载器来将class文件的字节码加载到内存中。
如何设置每个ClassLoader的搜索路径呢?Bootstrap ClassLoader、ExtClassLoader 和 AppClassLoader 的参数形式。
ClassLoader 类型 | 参数选项 |
说 明 |
Bootstrap ClassLoader | -Xbootclasspath: -Xbootclasspath/a: -Xbootclasspath/p: |
设置Bootstrap ClassLoader的搜索路径 把路径添加到己存在Bootstrap ClassLoader搜索路径的后面 把路径添加到已存在Bootstrap ClassLoader搜索路径的前面 |
ExtClassLoader | -Djava.ext.dirs | 设置ExtClassLoader的搜索路径 |
AppClassLoader | -Djava.class.path= -cp -classpath |
设置AppClassLoader的搜索路径 |
在上面的参数设置中,最常用到的就是设置classpath的环境变量,因为通常都是让Java运行指定的程序。如果在通过命令行执行一个类时出现NoClassDefFoundError错误,那么很可能是没有指定classpath所致,或者指定了 classpath但是没有指明包名。
验证与解析
- 字节码验证,类装入器对于类的字节码要做许多检测,以确保格式正确、行为正确。
- 类准备,在这个阶段准备代表每个类中定义的字段、方法和实现接口所必需的数据结构。
- 解析,在这个阶段类装入器装类所用的其他所有类。可以用许多方式引用类,如超类、接口、字段、方法签名、方法、方法中使用的本地变量。
初始化Class对象
常见加载器类错误分析
ClassNotFoundException
这个异常通常发生在显式加载类的时候,显式加载一个类通常有如下方式:
- 通过类Class中的forName()方法。
- 通过类 ClassLoader 中的 loadClass()方法。
- 通过类 ClassLoader 中的 findSystemClass()方法。
出现这类错误就是当JVM要加载指定文件的字节码到内存时,并没有找到这个文件对应的字节码,也就是这个文件并不存在。解决的办法就是检査在当前的classpath目录下有没有指定的文件存在。如果不知道当前的classpath路径,就可以通过如下命令来获取:this .getClass().getClassLoader().getResource ("").toString()
NoClassDefFoundError
这个异常通常发生在隐式加载类的时候,在JVM的规范中描述了出现NoClassDefFoundError可能的情况就是使用new关键字、属性引用某个类、继承个接口或类,以及方法的某个参数中引用了某个类,这时会触发JVM隐式加载这些类时发现这些类不存在的异常。解决这个错误的办法就是确保每个类引用的类都在当前的classpath路径下面。
UnsatisfiedLinkError
这个异常倒不是很常见,但是出错的话,通常是在JVM启动的时候,如果一不小心将在JVM中的某个lib删除了,可能就会报这个错误了。这个错误通常是在解析native标识的方法时JVM找不到对应的本机库文件时出现。
public class NoLibException { public native void nativeMethod(); static { System.loadLibrary("NoLib"); } public static void main(String[] args) { new NoLibException().nativeMethod(); } }
ExceptionInInitializerError
这个错误在JVM规范中是这样定义的:
- 如果Java虚拟机试图创建类ExceptionlnInitializerError的新实例,但是因为出现Out-Of-Memory-Error而无法创建新实倒,那么就抛出OutOfMemoryError对象作为替代。
- 如果初始化器抛出一些Exception,而且Exception类不是Error或者它的某个子类,那么就会创建ExceptionlnlnitializeiError类的一个新实例,并用Exception作为参数,用这个实例代替Exception。
Tomcat的ClassLoad分析
创建一个简单的Web应用,里面有一个HelloWorldServlet,然后在这个Servlet中打印加载它的ClassLoader,代码如下:
public class HelloWorldServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ClassLoader loader = this.getClass().getClassLoader(); while(loader != null) { out.write(loader.getClass().getName()+""); loader = loader.getParent(); } } }
将这个应用通过<context/>方式配置在server.xml中,代码如下:
注意,不要将这个应用直接放在Tomcat的webapps目录下,因为直接放在webapps目录下,Tomcat使用的ClassLoader会不一样。
上面这段代码打印出来的结果如下:
Tomcat容器类的加载
下面看看Tomcat是在什么时候创建这些ClassLoader的,首先看看StandardClassLoader的创建过程。
StandardClassLoader 是在 Bootstrap 类的 initClassLoaders 方法中创建的,Bootstrap 调用 ClassLoaderFactory 的 createClassLoader()方法创建 StandardClassLoader 对象。如果没有指定 StandardClassLoader 类的父 ClassLoader,就默认设置 getSystemClassLoader()方法返回的ClassLoader 作为其父类,getSystemClassLoader()返回的 ClassLoader 通常就是 AppClassLoader。
如果StandardClassLoader创建成功,将设置到Bootstrap的catalinaLoader属性作为整个 Tomcat 的根 ClassLoader。接下来 Tomcat 将以StandardClassLoader 来加载 org.apache.catalina.startup.Catalina 类并创建对象,最终也将 StandardClassLoader 设置到 Catalina 的parentClassLoader属性中。后面整个Tomcat容器的加载 ClassLoader 都将是StandardClassLoader 。
ps:我们前面说Tomcat容器的加载ClassLoader是StandardClassLoader,但是如果你调用Tomcat中任何一个类,如StandardContext类,通过 getClass().getClassLoader()方法返回的 ClassLoader 并不是 StandardClassLoader,而是AppClassLoader,为什么呢?原因是StandardClassLoader虽然是加载 StandardContext的类,但是可以看StandardClassLoader的实现方法可以发现StandardClassLoader只是一个代理类,并没有覆盖ClassLoader的loadClass()方法,StandardClassLoader仍然沿用委托加载器,它首先会让父加载器来加载,所以真正加载类仍然是是通过其父类AppclassLoader来完成的,加载Tomcat容器本身仍然是AppclassLoader。但是如果Tomcat的ClassPath没有被设置,那么AppClassLoader就将加载不到Tomcat容器的类,这时就要通过StandardClassLoader来加载了。其实不管是StandardClassLosder还是AppClassLoader加载,都没有任何影响;因为它们的加载规则一模—样,唯一不同的就是加载的路径不同。
Tomcat应用类的加载
其实我们真正关心的不是Tomcat容器本身是谁加载的,而是我们的应用是怎么加载的,也就是一个Web应用需要Tomcat执行时,这应用中的类是通过什么规则加载起来的?
我们知道,一个应用在Tomcat中由一个StandardContext表示.由 StandardContext来解释Web应用的web.xml配置文件实例化所有的Servlet。Servlet的calss是由<servlet-class>来指定的,所以可想而知,每个Servlet类的加载肯定是通过显式加载方法加载到Tomcat容器中的。那么Servlet是如何被加载的呢?先看看StandardContext类的startInternal()方法,在StandardContext初始化时將会检査loader属性是否存在,不存在就将创建它。看如下代码:
if (getLoader() == null)[ WebappLaader webappLoader = new WebappLaader (getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }
这段代码清楚地表示将创建WebappLoader对象,而WebappLoader对象将创建WebappClassLoader 作为其 ClassLoader。再翻阅 StandardWrapper 类的 loadServlet()方法可以发现,所有的Servlet都是InstanceManager实例化的,那么InstanceManager类使用的ClassLoader 是不是 WebappClassLoader 呢?
InstanceManager 对象的 ClassLoader也获取 StandardContext 的 Loader 中的ClassLoader,也就是前面设置的 WebappClassLoader,所以 Servlet 的 ClassLoader 是WebappClassLoader.WebappClassLoader 不像 StandardClassLoader 那么简单,它覆盖了父类的 loadClass 方法,使用自己的加载机制,这个加载机制有点复杂,大体分为以下几个步骤:
- 首先检查在WebappClassLoader中是否已经加载过了,如果请求的类以前是被WebappClassLoader 加载的,那么肯定在 WebappClassLoader的缓存容器 resourceEntries 中。
- 如果不在WebappClassLoader的resourceEntries中,则继续检查在JVM虚拟机中是否已经加载过,也就是调用ClassLoader的findLoadedClass方法。
- 如果在前两个缓存中都没有,则先调用SystemClassLoader加载请求的类,SystemClassLoader在这里是AppClassLoader,也就是在当前的JVM的ClassPath路径下査找请求的类。
- 检查请求的类是否在packageTriggers定义的包名下,如果在这个设置的包目录下(即webapps目录下),则将通过StandardClassLoader类来加载。
- 如果仍然没有找到,将由WebappClassLoader来加栽,WebappClassLoader将会在这个应用的WEB-INF/classes目录下查找请求的类文件的字节码。找到后将创建一个ResourceEntry对象保存这个类的元信息,并把它保存在WebappClassLoader的resourceEntries容器中便于下次查找。接着将调用defineClass方法生成请求类的Class对象并返回给InstanceManager来创建实例。
从上面的分析来看,Tomcat仍然沿用了 JVM的类加载规范,也就是委托式加载,保证核心类通过AppClassLoader来加载。但是Tomcat会优先检查WebappClassLoader己经加载的缓存,而不是JVM的findLoadedClass缓存,这一点需要注意。这也说明了如果你将一个Web应用直接放到webapp目录下,那么Tomcat就通过StandardClassLoader 直接加载,而不是通过 WebappClassLoader 来加载。
实现类的热部署
什么是类的热部署
所谓热部署,就是在应用正在运行的时候升级软件,不需要重新启用应用。
对于Java应用程序来说,热部署就是运行时更新Java类文件。在基于Java的应用服务器实现热部署的过程中,类装入器扮演着重要的角色。大多数基于Java的应用服务器,包括EJB服务器和Servlet容器,都支持热部署。
如何实现热部署
我们知道,JVM在加载类之前会检査请求的类是否已经被加载过来,也就是要调用flndLoadedClass()方法查看是否能够返回类实例。如果类已经加载过来,再调用loadClass()将会导致类冲突。
但是JVM判断一个类是否是同一个类会有两个条件。一是看这个类的完整类名是否一样,这个类名包括类所在的包名。 二是看加载这个类的ClassLoader是否是同一个(既是是同一个ClassLoader类的两个实例,加载同一个类也会不一样)。
所以要实现类的热部署可以创建不同的ClassLoader的实例对象,然后通过这个不同的实例对象来加载同名的类,
ps:
类装入器不能重新装入一个已经装入的类,但只要使用一个新的类装入器实例,就可以将类再次装入一个正在运行的应用程序。
重复加载一个类会抛出java.lang.LinkageError,即用同一个ClassLoader的实例对象加载两次同一个类名。
使用不同的Classloader实例加载同一个类,会不会导致JVM的PermGen区无限增大?
答案是否定的,因为我们的Classloader对象也和其他对象一样,当没有对象再引用它以后,也会被JVM回收,但是需要注意的一点是,被这个Classloader加载的类的字节码会保存在JVM的PermGen区,这个数据一般只是在执行Full GC时才会被回收的,所以如果在你的应用中都是大量的动态类加载,FullGC又不是太频繁,也要注意PermGen区的大小,防止内存溢出。
应不应该动态加载类
我想大家都知道用Java有一个痛处,就是修改一个类,必须要重启一遍,很费时,于是就想能不能来个动态类的加载而不需要重启JVM,如果你了解JVM的工作机制,就应该放弃这样的念头。
Java的优势正是基于共享对象的机制,达到信息的髙度共享,也就是通过保存并持有对象的状态而省去类信息的重复创建和回收。我们知道对象一旦被创建,这个对象就可以被人持有和利用。
假如,我只是说假如,如我们能够动态加载一个对象进入JVM,但是如何做到JVM中对象的平滑过渡?几乎不可能!虽然在JVM中对象只有一份,在理论上可以直接替换这个对象,然后更新Java栈中所有对原对象的引用关系。看起来好像对象可以被替换了,但是这仍然不可行,因为它违反了 JVM的设计原则,对象的引用关系只有对象的创建者持有和使用,JVM不可以干预对象的引用关系,因为JVM并不知道对象是怎么被使用的,这就涉及JVM并不知道对象的运行时类型而只知道编译时类型。
假如一个对象的属性结构被修改,但是在运行时其他对象可能仍然引用该属性。虽然完全的无障碍的替换是不现实的,但是如果你非要那样做,也还是有一些“旁门左道”的。前面的分析造成不能动态提供类对象的关键是,对象的状态被保存了,并且被其他对象引用了,一个简单的解决办法就是不保存对象的状态,对象被创建使用后就被释放掉,下次修改后,对象也就是新的了。
这种方式是不是就很好呢?这就是JSP,它难道不是可以动态加载类吗?修改了jsp不用重启。