目录[-]
做Java开发,对于ClassLoader的机制是必须要熟悉的基础知识,本文针对Java ClassLoader的机制做一个简要的总结。因为不同的JVM的实现不同,本文所描述的内容均只限于Hotspot Jvm.
本文将会从JDK默认的提供的ClassLoader,双亲委托模型,如何自定义ClassLoader以及Java中打破双亲委托机制的场景四个方面入手去讨论和总结一下。
JDK默认ClassLoader
JDK 默认提供了如下几种ClassLoader
-
Bootstrp loader
Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib
,-Xbootclasspath
参数指定的路径以及%JAVA_HOME%/jre/classes
中的类。
-
ExtClassLoader
Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader.ExtClassLoader是用Java写的,具体来说就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext
,此路径下的所有classes目录以及java.ext.dirs
系统变量指定的路径中类库。 -
AppClassLoader
Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader
方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。
双亲委托模型
Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:
-
当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
-
当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
-
当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
说到这里大家可能会想,Java为什么要采用这样的委托机制?理解这个问题,我们引入另外一个关于Classloader的概念“命名空间”, 它是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。也就是说即使两个类的全限定名是相同的,但是因为不同的 ClassLoader加载了此类,那么在JVM中它是不同的类。明白了命名空间以后,我们再来看看委托模型。采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面说的,我们JDK本生提供的类库,比如hashmap,linkedlist等等,这些类由bootstrp 类加载器加载了以后,无论你程序中有多少个类加载器,那么这些类其实都是可以共享的,这样就避免了不同的类加载器加载了同样名字的不同类以后造成混乱。
如何自定义ClassLoader
Java除了上面所说的默认提供的classloader以外,它还容许应用程序可以自定义classloader,那么要想自定义classloader我们需要通过继承java.lang.ClassLoader
来实现,接下来我们就来看看再自定义Classloader的时候,我们需要注意的几个重要的方法:
1.loadClass 方法
loadClass method declare
| public Class<?> loadClass(String name) throws ClassNotFoundException |
上面是loadClass方法的原型声明,上面所说的双亲委托机制的实现其实就实在此方法中实现的。下面我们就来看看此方法的代码来看看它到底如何实现双亲委托的。
loadClass method implement
| public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } |
从上面可以看出loadClass方法调用了loadcClass(name,false)方法,那么接下来我们再来看看另外一个loadClass方法的实现。
Class loadClass(String name, boolean resolve)
|
|
上面的代码,我加了注释通过注释可以清晰看出loadClass的双亲委托机制是如何工作的。 这里我们需要注意一点就是public Class<?> loadClass(String name) throws ClassNotFoundException
没有被标记为final,也就意味着我们是可以override这个方法的,也就是说双亲委托机制是可以打破的。另外上面注意到有个findClass方法,接下来我们就来说说这个方法到底是搞末子的。
2.findClass
我们查看java.lang.ClassLoader
的源代码,我们发现findClass的实现如下:
| protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } |
我们可以看出此方法默认的实现是直接抛出异常,其实这个方法就是留给我们应用程序来override的。那么具体的实现就看你的实现逻辑了,你可以从磁盘读取,也可以从网络上获取class文件的字节流,获取class二进制了以后就可以交给defineClass
来实现进一步的加载。defineClass我们再下面再来描述。 ok,通过上面的分析,我们可以得出如下结论:
我们在写自己的ClassLoader的时候,如果想遵循双亲委托机制,则只需要override
findClass
.
3.defineClass
我们首先还是来看看defineClass的源码:
defineClass
| protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null); } |
从上面的代码我们看出此方法被定义为了final,这也就意味着此方法不能被Override,其实这也是jvm留给我们的唯一的入口,通过这个唯 一的入口,jvm保证了类文件必须符合Java虚拟机规范规定的类的定义。此方法最后会调用native的方法来实现真正的类的加载工作。
Ok,通过上面的描述,我们来思考下面一个问题:
假如我们自己写了一个java.lang.String
的类,我们是否可以替换调JDK本身的类?答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的
java.lang.String
类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。
不遵循“双亲委托机制”的场景
上面说了双亲委托机制主要是为了实现不同的ClassLoader之间加载的类的交互问题,被大家公用的类就交由父加载器去加载,但是Java中确实也存在父类加载器加载的类需要用到子加载器加载的类的情况。下面我们就来说说这种情况的发生。
Java中有一个SPI(Service Provider Interface)标准,使用了SPI的库,比如JDBC,JNDI等,我们都知道JDBC需要第三方提供的驱动才可以,而驱动的jar包是放在我们应 用程序本身的classpath的,而jdbc 本身的api是jdk提供的一部分,它已经被bootstrp加载了,那第三方厂商提供的实现类怎么加载呢?这里面JAVA引入了线程上下文类加载的概 念,线程类加载器默认会从父线程继承,如果没有指定的话,默认就是系统类加载器(AppClassLoader),这样的话当加载第三方驱动的时候,就可 以通过线程的上下文类加载器来加载。
另外为了实现更灵活的类加载器OSGI以及一些Java app server也打破了双亲委托机制。
- /**
- * Java 类加载器
- *
- * @author 张明学
- *
- */
- public class ClassLoaderStudy {
- public static void main(String[] args) throws Exception {
- // 获取:加载ClassLoaderStudy的类加载器
- System.out.println(ClassLoaderStudy.class.getClassLoader().getClass().getName());// AppClassLoader
- // 获取:加载ClassLoaderStudy所遍历的类加载器
- ClassLoader classLoader = ClassLoaderStudy.class.getClassLoader();
- while (null != classLoader) {
- System.out.println(classLoader.getClass().getName());
- classLoader = classLoader.getParent();
- }
- /**
- * Java 提供了三个类加载器
- * BootStrap ------> getParent() == null;负责加载JRE/lib/rt.jar中的class
- * ExtClassLoader ------> getParent() == BootStrap; 负责加载JRE/lib/ext/*.jar中的class
- * AppClassLoader ------> getParent() == ExtClassLoader; 负责加载CLASSPATH指定的所有jar目录
- * 每个一个加载器(除BootStrap外)包括自己定义的类加载器,加载一个class时首先交给它的父加载器,父加载器再交给它的父加载器
- * 直到BootStrap类加载器,当父加载器没有找到时再交给它的子加载器,一层层的找,若都没有找到就报:java.lang.ClassNotFoundException
- *
- * 当需要加载一个类默认首选使用当前线程的类加载器去加载,若A类中引用了B类则B也是由该加载器去加载
- */
- // 用自己的类加载去加载自己加过密的class文件
- // 必须要把原来目录中的(即:bin\com\mengya\classLoader\)下面的myClass删除,因为
- // 加载myClass时MyClassLoader首先会交给父加载器,若父加载器在原目录中找到了myClass的class文件
- // 那么就是父加载器去加载了,也就轮不到自己定义的MyClassLoader加载了。
- Class clazz = new MyClassLoader().loadClass("com.mengya.classLoader.myClass");
- // 因为自己编写的myClass是加过密的,故编译器不能正确编译myClass文件,即不能出现MyClass。
- // 故让MyClass继承另一个类,用的时候让它的父类(接口等)去引用加过密的myClass实例
- MyClassInter test = (MyClassInter) clazz.newInstance();
- // MyClass myClass = null;编译器报错
- System.out.println(test.toString());
- }
- }
- /**
- * 自定义的类加载器(继承ClassLoader重写findClass方法)
- *
- * @author 张明学
- *
- */
- public class MyClassLoader extends ClassLoader {
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- String destPath = "F:\\MyEclipseWorkspace\\JavaBase\\myClassLib";
- String className = name.substring(name.lastIndexOf('.') + 1);
- String classFileName = destPath + "\\" + className + ".class";
- InputStream fileInStream = null;
- ByteArrayOutputStream byteArrayOutStream = null;
- try {
- fileInStream = new FileInputStream(classFileName);
- byteArrayOutStream = new ByteArrayOutputStream();
- encrypt(fileInStream, byteArrayOutStream);
- byte[] classByte = byteArrayOutStream.toByteArray();
- return defineClass(classByte, 0, classByte.length);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- byteArrayOutStream.close();
- fileInStream.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return super.findClass(name);
- }
- /**
- * 用来加密class文件
- *
- * @param args
- */
- public static void main(String[] args) {
- // 原class文件
- String srcPath = "F:\\MyEclipseWorkspace\\JavaBase\\bin\\com\\mengya\\classLoader\\MyClass.class";
- // 加密class文件存放目录
- String destPath = "F:\\MyEclipseWorkspace\\JavaBase\\myClassLib";
- InputStream fileInStream = null;
- OutputStream fileOutStream = null;
- try {
- fileInStream = new FileInputStream(srcPath);
- String fileName = srcPath.substring(srcPath.lastIndexOf('\\') + 1);
- String destFilePath = destPath + "\\" + fileName;
- fileOutStream = new FileOutputStream(destFilePath);
- encrypt(fileInStream, fileOutStream);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- fileOutStream.close();
- fileInStream.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- /**
- * 对class文件加(解)密
- *
- * @param inStream
- * class文件输入流
- * @param outStream
- * class文件输出流
- */
- public static void encrypt(InputStream inStream, OutputStream outStream)
- throws Exception {
- int b = -1;
- while ((b = inStream.read()) != -1) {
- outStream.write(b ^ 0xff);
- }
- }
- }
- /**
- * 自己定义的类加载加载的测试类
- *
- * @author 张明学
- *
- */
- public class MyClass implements MyClassInter {
- @Override
- public String toString() {
- return "Hello !";
- }
- }