JDK进阶(1):类加载器 ClassLoader

类加载器

类加载过程

  1. 加载:类加载过程的一个阶段。通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象
  2. 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
  3. 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
  4. 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析
  5. 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)
    类加载的5个过程
  6. 这便是类加载的5个过程,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的 java.lang.Class 对象实例,在虚拟机提供了3种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)

默认类加载器

启动(Bootstrap)类加载器
  1. Bootstrap class loader:最基本的类加载器,本地语言编写,不需要被加载,直接被嵌套在虚拟机中(是虚拟机自身的一部分)
  2. 该类加载负责加载 ${JAVA_HOME}/lib/ 路径下核心类库(rt.jar)或者 -Xbootclasspath 参数指定的路径下的jar包加载到内存中
  3. 拿不到该类加载器的名字,打印出来一般为null
扩展(Extension)类加载器
  1. Extension class loader:扩展类加载器,是指Sun实现的 sun.misc.Launcher$ExtClassLoader 类,由Java实现,是Launcher的静态内部类。
  2. 该类加载器负责加载 ${JAVA_HOME}/lib/ext/目录下或者由系统变量 -Djava.ext.dir 指定位路径中的类库
  3. 名称为:ExtClassLoader
系统(System)类加载器
  1. System class loader:系统(System)类加载器或者应用程序加载器。 指 Sun公司实现的 sun.misc.Launcher$AppClassLoader
  2. 负责加载系统类路径java -classpath-D java.class.path 指定路径下的类库,就是我们经常用到的classpath路径。一般情况下该类加载是程序中默认的类加载器,通过ClassLoader.getSystemClassLoader()就能获取。
  3. 名称:AppClassLoader

类加载器树图

  1. Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它是一种任务委派模式。

  2. JVM 中的所有类装载器采用父子关系的树形结构进行组织。

  • 在实例化每个类装载器对象时,需要对其指定一个父级类装载器对象
  • 不指定的化,默认采用系统类装载器为其父类级加载

类加载器

简单代码

  1. 获取类加载器的名称
//1. bootstrap class loader: null,拿不到 BootStrap 的名字
System.out.println(String.class.getClassLoader());

//2. extesion class loader 负责扩展包中的类(jre/lib/ext下面的类) sun.misc.Launcher$ExtClassLoader
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());

//3. application class loader 负责自己写的类,也称为系统 classLoader。sun.misc.Launcher$AppClassLoader
System.out.println(JdkClassLoader.class.getClassLoader().getClass().getName());

// sun.misc.Launcher$AppClassLoader@14dad5dc
System.out.println(ClassLoader.getSystemClassLoader());
// sun.misc.Launcher$AppClassLoader@19821f
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());

// sun.misc.Launcher$AppClassLoader@14dad5dc
System.out.println(JdkClassLoader.class.getClassLoader());
// sun.misc.Launcher$AppClassLoader
System.out.println(JdkClassLoader.class.getClassLoader().getClass().getName());


输出:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$AppClassLoader
  1. 默认类加载器的关系:AppClassLoader -> ExtClassLoader -> BootStrap
ClassLoader loader = JdkClassLoader.class.getClassLoader();
while (loader != null) {
    System.out.println("=======" + loader.getClass().getName());
    loader = loader.getParent();
}
System.out.println("-------" + loader);

输出:
=======sun.misc.Launcher$AppClassLoader
=======sun.misc.Launcher$ExtClassLoader
-------null

类加载器的委托机制

类的加载

  1. 当前线程的类加载器去加载线程中的第一个类,如果该类(如A)中引用(继承等)另一个类(B),JVM 将使用加载该类(A)的加载器来加载其引用的类(B)
  2. 直接调用ClassLoader.loadClass() 方法来指定某个类加载器加载某个类

委托机制

原理
  1. 类加载器在加载类的时候,优先委托给上级类加载器,其委托方向如为 AppClassLoader -> ExtClassLoader -> BootStrap
  2. 其工作原理为:
  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
  1. 一层层委托后,当所有父加载器都没有加载到类,然后回到发起者类加载器,如果还加不到,就会抛出ClassNotFoundException
  2. 不会去找发起者的子加载器,原因在于,没有getChild()方法,即使有,那么多子加载器也不知道具体找哪一个
graph BT
A[ExtClassLoader]-->C[URLClassLoader]
B[AppClassLoader]-->C[URLClassLoader]
C-->D[SecureClassLoader]
D-->E[ClassLoader]
ClassLoader
  1. loadClass(String):该方法加载指定名称(包括包名)的二进制类型,不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现
  • 当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载器的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载。
  • loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoaderloadClass方法获取到class对象。
protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该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
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  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. findClass(String):自定义类加载时,建议把自定义的类加载逻辑写在findClass()方法中。
  • findClass()方法是在loadClass()方法中被调用的,ClassLoader 类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常
  • 同时应该知道的是findClass方法通常是和defineClass方法一起使用的
  1. defineClass(byte[] b, int off, int len):用来将byte字节流解析成JVM能够识别的Class对象。
  • defineClass()方法通常与findClass()方法一起使用
  • 在自定义类加载器时,会直接覆盖ClassLoaderfindClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。见后面的定义类加载器
  1. resolveClass(Class≺?≻ c):使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用

示例

  1. 简单代码:
  • 默认打印出来是 sun.misc.Launcher$AppClassLoader
  • 现在我们把类 JdkClassLoader.java打包,放JAVA_HOME\lib\ext\ 目录。重新运行:则打印出 sun.misc.Launcher$ExtClassLoader
  • 原因在于:
    • 当前线程的类加载发起者是 AppClassLoader,其先会委托给ExtClassLoader,继续委托给顶层 BootStrap,开始
    • BootStrap先找,没有找到就轮到 ExtClassLoader 找,找不到就轮到 AppClassLoader找,找不到就抛异常
    • 当我们修改JdkClassLoader.java的路径后,ExtClassLoader可以获取
System.out.println(JdkClassLoader.class.getClassLoader().getClass().getName()); 

类与类加载器

  1. 在JVM中表示两个class对象是否为同一个类对象存在两个必要条件
  • 类的完整类名必须一致,包括包名
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
  • 在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
  1. class文件的显示加载与隐式加载(JVM 加载 class 文件到内存的方式)
  • 显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)this.getClass().getClassLoader().loadClass()加载class对象
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

自定义类加载器

自定义方法

  1. 继承抽象类 ClassLoader
  2. 重写loadClass() 或者 findClass()
  • loadClass():打破委托机制。loadClass()内部会去找父类委托,然后再找自己,调用findClass()方法。不建议。
  • findClass():继承委托机制。先让父加载器运行,然后自己运行
  1. definClass():实现把得到的 class 文件转换成字节码

示例

  1. 被加载的类
public class CustomClassLoaderAttachment extends Date {


    private String name;

    public CustomClassLoaderAttachment(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("CustomClassLoaderAttachment{");
        sb.append("name='").append(name).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

  1. 自定义类加载
public class CustomClassLoader extends ClassLoader {

    /**
     * 类的加载路径
     */
    private String classPath;
    /**
     * 类加载器的名字
     */
    private String name;

    public CustomClassLoader() {
    }

    public CustomClassLoader(String classPath, String name) {
        this.classPath = classPath;
        this.name = name;
    }

    /**
     * 重写 loadClass() 方法,打破委托机制
     *
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // loadClass() 方法会去 父类委托,然后再找自己,调用findClass()方法
        return super.loadClass(name);
    }

    /**
     * 重写 findClass() 方法,先让父类树处理,然后自己处理,继承了委托机制
     *
     * @param name
     * @return
     */
    @Override
    protected Class<?> findClass(String name) {
        String classFileName = classPath + name + ".class";
        try {
            FileInputStream fis = new FileInputStream(classFileName);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            cypher(fis, bos);
            fis.close();
            byte[] bytes = bos.toByteArray();
            return defineClass(null, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     *
     *
     * @param ips
     * @param ops
     */
    private static void cypher(InputStream ips, OutputStream ops) {
        int b;
        try {
            while ((b = ips.read()) != -1) {
                ops.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    public static void main(String[] args) throws Exception {

        CustomClassLoader classLoader = new CustomClassLoader("/WorkSpace/Sam/Learning/Learning-JavaOptimize/Java-JDK/target/classes/com/learning/optimize/jdk/classloader/", "CustomClassLoader");
        Class<?> clazz = classLoader.loadClass("CustomClassLoaderAttachment");
        System.out.println(clazz.getClassLoader().getClass().getName());

        Constructor<?> constructor = clazz.getConstructor(String.class);
        Date attachment = (Date) constructor.newInstance("名字");
        System.out.println(attachment);
    }

    /*
    * 输出:
        com.learning.optimize.jdk.classloader.CustomClassLoader
        CustomClassLoaderAttachment{name='名字'}
    */
}

Tomcat 的类加载器(Tomcat8.5)

  1. 自定义的类加载器有:ParallelWebappClassLoader
  2. URLClassLoader 是JDK实现的自定义类加载器
@WebServlet(name = "ServletClassLoader", value = {"/servletClassLoader"})
public class ServletClassLoader extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter out = resp.getWriter();

        //系统加载器
        out.println("----" + ClassLoader.getSystemClassLoader().getClass().getName());


        //循环遍历加载器
        ClassLoader loader = this.getClass().getClassLoader();

        while (loader != null) {
            out.println(loader.getClass().getName() + "<br/>");
            loader = loader.getParent();
        }

        out.println(loader);


        out.flush();
        out.close();

    }

    /*
        sun.misc.Launcher$AppClassLoader org.apache.catalina.loader.ParallelWebappClassLoader
        java.net.URLClassLoader
        sun.misc.Launcher$AppClassLoader
        sun.misc.Launcher$ExtClassLoader
        null
    */
}

输出:
sun.misc.Launcher$AppClassLoader org.apache.catalina.loader.ParallelWebappClassLoader
java.net.URLClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null

使用类加载获取资源文件

  1. 使用 ClassLoader 类加载加载
  • path:路径不能以 ‘/’ 开头
  • 默认从 ClassPath 根路径下获取
// 默认则是从 ClassPath 根下获取,path不能以 '/' 开头,最终是由 ClassLoader 获取资源
InputStream ips2 = ClassLoaderLoadResource.class.getClassLoader().getResourceAsStream("config2.properties");
  1. 使用 Class 类加载
  • path 不以 ‘/’ 开头:针对当前的类,从此类所在的包下取资源
  • path 以 ‘/’ 开头:从 ClassPath下获取资源,与 ClassLoader 类加载加载一样
// path 不以 '/' 开头,就是针对当前的类。是从此类所在的包下取资源
// path 以 '/' 开头,则是从 ClassPath(Src根目录) 下获取资源
InputStream ips3 = ClassLoaderLoadResource.class.getResourceAsStream("config2.properties");
  1. Spring 的 ClassPathResource 就是根据类加载器与 Class类的资源获取功能实现的

参考

  1. 源码地址:Java 进阶
  2. 深入理解Java类加载器(ClassLoader)

Fork me on Gitee

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值