Java类加载器

本文和大家聊聊Java类加载器这档子事。

什么是类加载器?

咱们先来给他下一个通俗点的定义:

将java字节码(.class文件)转换成类对象(java.lang.Class),加载到JVM内存。

Java中有哪几种类加载器?

类加载器可分为四种:
Bootstrap ClassLoader:由C++代码实现,加载sun.boot.class.path所指定的目录,或\lib目录中文件。

Ext ClassLoader:加载系统变量 java.ext.dirs所指定的目录,或\lib\ext 目录中的文件。

App ClassLoader:加载用户类路径下的文件。

Custom ClassLoader:开发者自己折腾的类加载器,按需进行类文件加载。(例如:网络类加载器,通过网络加载.class文件)

类加载器如何工作?

第一步,类加载器向JVM查询目标对象(请求加载的对象)是否已经加载过,若已经加载过,则直接返回此对象;否则,进行第二步。

第二步,获得当前类加载器的父类加载器,递归执行这一步骤,由下往上,直至到达顶级父类加载器(即Bootstrap ClassLoader),进行第三步。

第三步,从顶级父类加载器,由上而下,在自己的搜索范围内检索目标对象的类文件(.class),若检索到,则读取该文件的字节码转换成Class对象到JVM,返回目标对象;否则,抛出ClassNotFoundException。

有读者老爷说了,听你白扯了半天,对类加载器的认知还是不清楚,有没有实质性的东西?下面咱们来点实的。

类加载器的委派模式

从上文中类加载器的工作步骤,可以看出,递归获取父类加载器,由下往上,直达顶级父类加载器,在从顶级父类加载器,自上而下,逐级在自己的搜索范围内检索目标对象类文件,转换成Class对象(若检索到)的这种行为,称为类加载器的委派模式

描述递归获取父类加载器,由下往上,直达顶级父类加载器的代码如下:

//代码段截取自ClassLoader.loadClass(String name, boolean resolve)
 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
           }

描述从顶级父类加载器,自上而下,逐级在自己的搜索范围内检索目标对象类文件,转换成Class对象的代码如下:

 //代码段截取自ClassLoader.loadClass(String name, boolean resolve)
   if (c == null) {
      // If still not found, then invoke findClass in order
      // to find the class.
      long t1 = System.nanoTime();
      c = findClass(name);//在自己的搜索范围内进行检索,若找到,转换成Class对象
       .....................................
       .....................................
      }
      if (resolve) {
            resolveClass(c);
       }
       return c;

真正实现类加载的是defineClass方法,代码如下:

//代码段截取自URLClassLoader.findClass(final String name)
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 {
       return null;
}

回顾类加载器的委派模式,我们发现,发起类加载请求的类加载器,不一定会最终执行类的加载操作,可能其父类加载器来执行类加载。

发起类加载请求的类加载器称为初始类加载器;完成类加载操作的类加载器称为定义类加载器。(为啥叫定义类加载?因名而得呗-defineClass)

有读者老爷发问了,为什么要设计这种委派模式

委派模式的作用

个人理解,设计这种委派模式的用意有两点:
第一,避免对象的重复加载
第二,保护Java基础类型的行为

关于第一点,自然不必多说。
对于第二点,咱们设想一下,在没有委派模式的场景中,用户自己编写了一个java.lang.String类,并将其放置在程式的类路径(ClassPath)中,那系统中就存在了多个不同的String类,这使得Java的基础类型的行为无法得到保证,程式也会变得很混乱。

细心的读者可能会从上面的例子中发现,同一个类被两个不同的类加载器加载会出现问题,产生这种问题的原因是什么?在后面的类加载器的隔离性中会进行解释

类加载器的层级结构是怎样的?

如下是类加载器层级示意图:

Java类加载器_第1张图片

口说无凭,咱们来验证一下类加载器的层级结构,执行如下代码:

 package com.hys;

 public class Main {

     public static void main(String[] args) {
 
         ClassLoader c1 = User.class.getClassLoader();
         System.out.println(c1);
 
          ClassLoader c2 = c1.getParent();
          System.out.println(c2);         

          ClassLoader c3 = c2.getParent(); 
          System.out.println(c3); 
    }
}

执行结果:

sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@140e19d
null 

Process finished with exit code 0

执行结果符合咱们的预期。

有读者老爷发问了,怎么就符合预期了?结果中有个值为null,是个啥?Bootstrap ClassLoader呢?
原因在于顶级类加载器是Bootstrap ClassLoader,此加载器是C++代码编写,所以,使用Java获取的时候,返回了null

类加载器的隔离性

JVM如何判断一个类的唯一性?
JVM通过加载该类的类加载器和类名称来确定一个类的唯一性。

这意味着我们使用两个不同的类加载器加载同一个类,而在JVM层面上却认为这是两个完全不同的类,从另一个层面看,相当于类被类加载器所包裹与另个类加载器中的类进行了隔离

如何证明这种隔离的存在?执行如下代码:

package com.hys;

public class Main {
    public static void main(String[] args) {

        try{
           User u1 = new User();
           User u2 = new User();
           // u1和u2都是同一个类加载器加载的
           System.out.println("The same classloader: u1.isInstance(u2)" + u1.getClass().isInstance(u2));

            MyClassLoader c1 = new MyClassLoader();
            Class clazz = c1.loadClass("com.hys.User");
           // clazz是MyClassLoader类加载器加载的
            System.out.println("The different classloader: clazz.isInstance(u1)" + clazz.isInstance(u1));
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
}

执行结果:

The same classloader: u1.isInstance(u2)true
The different classloader: clazz.isInstance(u1)false

Process finished with exit code 0

自定义类加载器

自定义类加载器,一般是继承ClassLoader类,覆盖findClass(String name)方法,在此方法中做查询与读取.class文件的操作,如下代码:

package com.hys;

import java.io.*;

public class MyClassLoader extends ClassLoader {

    private String mRootDir;

    public MyClassLoader() {

    }

    public MyClassLoader(String rootDir) {
        mRootDir = rootDir;
    }

    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] data = loadClassData(name);
        if (data == null) {
            throw new ClassNotFoundException();
        }else{
            return defineClass(name, data, 0, data.length);
        }
    }

    private byte[]loadClassData(String className) {
        String path = classNameToPath(className);
        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;
    }

    private String classNameToPath(String className) {

        String classPath;
        if(mRootDir == null || mRootDir.isEmpty())
            classPath = className.replace('.', File.separatorChar) + ".class";
        else
            classPath = mRootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";

        return classPath;
    }
}

我是青岚之峰,如果读完后觉的有所收获,欢迎点赞加关注

你可能感兴趣的:(Java类加载器)