java之ClassLoader的原理以及应用

一、背景

1.ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。

2.理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。

3.但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习ClassLoader这种加载机制。

二、概念

1.先来看下java程序是怎么工作的,我们都知道Java是跨平台的,是因为不同平台下的JVM能将字节码文件解释为本地机器指令,JVM是怎么加载字节码文件的?答案就是ClassLoader,接下来我们来画个图来看下关系。

三、代码

1.获取类加载器的代码测试

public class ClassLoaderDemo1 {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        //获取加载到JVM的类加载器的方法
        ClassLoader classLoader = ClassLoaderDemo1.class.getClassLoader();
        while (classLoader != null){
            System.out.println(classLoader);
            //得到当前加载器的父类加载器
            classLoader = classLoader.getParent();
        }
    }
}

2.结果

null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4f023edb

3.分析

3.1.要理解这个输出,我们就得说一下双亲委派模式,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。

3.2.采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。检查和加载过程以及系统提供的ClassLoader的作用如下图。如果在你项目中建一个java.lang.String的类,那系统中用的String类是你定义的String类,还是原生api中的String类,用双亲加载来解释就很容易理解用的是原生api中的String类

类加载器的关系如下:

  1. 启动类加载器,由C++实现,没有父类。

  2. 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null。

  3. 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader系统类加载器(AppClassLoader)。

  4. 自定义类加载器,父类加载器肯定为AppClassLoader。自定义类加载器,父类加载器肯定为AppClassLoader。

四、源码分析

1.一般只需要理解ClassLoader 这3个方法即可
   loaderClass:实现双亲委派
   findClass:用来复写加载
   defineClass:本地方法,最终加载类只能通过defineClass

2.源码

2.1.源码在jdk的java.lang

// 从这方法开始加载
public Class<?> loadClass(String name) throws ClassNotFoundException {
 return loadClass(name, false);
}

2.2.源码

// 实现双亲委派
protected Class<?> loadClass(String name, boolean resolve)
 throws ClassNotFoundException
{
 synchronized (getClassLoadingLock(name)) {
   // First, check if the class has already been loaded
   // 先从缓存查找该class对象,找到就不用重新加载
   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
       // 这里体现了自顶向下尝试加载类,当父类加载加载不到时
       // 会抛出ClassNotFoundException
       // 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();
       // 如果都没有找到,通过自己的实现的findClass去加载
       // findClass方法没有找到会抛出ClassNotFoundException
       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;
 }
}

2.3.源码

// 复写加载
protected Class<?> findClass(String name) throws ClassNotFoundException {
 throw new ClassNotFoundException(name);
}

复写加载,如果都加载不了。这个时候我们得通过自己的实现的findClass去加载。接下来我们来讲下自定义加载器。

五、自定义加载器

1.为什么要编写自己的类加载器?

1.1.当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。

1.2.当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。

1.3.当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。

六、代码例子

1.当继承ClassLoader时,只要重写findClass方法即可,还可以继承URLClassLoader,代码更简洁,这里不再赘述,下面写一个从指定文件中加载class文件的FileClassLoader。

public class DemoObj {
    public String toString(){
        return " I am DemoObj";
    }
}

2.编译出来的DemoObj.class文件

package com.tydic.encryptiondemo.controller;

public class DemoObj {
    public DemoObj() {
    }

    public String toString() {
        return " I am DemoObj";
    }
}

3.把DemoObj.class文件放到指定目录,然后由FileClassLoader去加载。

public class FileClassLoader extends ClassLoader {

    // class文件的目录
    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = rootDir +"/" + className.replace('.', '/') + ".class";
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        String rootDir = "E:\\lib\\com\\test";
        FileClassLoader loader = new FileClassLoader(rootDir);
        try {
            // 传入class文件的全限定名
            Class<?> clazz = loader.loadClass("com.example.classloader.st.DemoObj");
            System.out.println(clazz.getClassLoader());
            // I am DemoObj
            System.out.println(clazz.newInstance().toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.结果

com.example.classloader.st.FileClassLoader@5674cd4d
I am DemoObj

七、结束

1.这样就算讲完啦!!!晚安!

发布了122 篇原创文章 · 获赞 64 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/chenmingxu438521/article/details/102579214