再次认识ClassLoader

通读这篇文章你会知道如何回答以下问题:

  • Java自带的三大加载器加载的jar位置都是在哪里?
  • 三大加载器之间的关系是怎么样的? 在代码中是如何体现的?
  • 双亲委派模型是什? 代码中如何体现这种模式的应用? 这种模式的不足是什么?
  • 上下文加载器存在的作用是什么?应用的场景有哪些?

java三大加载器加载的jar位置

知道每个加载器加载什么位置的jar,这对后面分析委托机制会起到作用。
Java语言自带的有三个类加载器

  1. Bootstrap CLassloder

  2. Extention ClassLoader

  3. AppClassLoader
    以下是输出各个加载器的加载jar的位置,这些路径可以通过虚拟机参数进行修改。

    public static void testClassLoader() {
     System.out.println("BootstrapClassLoader:");
     String property = System.getProperty("sun.boot.class.path");
     Arrays.stream(property.split(";")).forEach(System.out::println);
    
     System.out.println("ExtClassLoader :");
     property = System.getProperty("java.ext.dirs");
     Arrays.stream(property.split(";")).forEach(System.out::println);
    
     System.out.println("AppClassLoader :");
     property = System.getProperty("java.class.path");
     Arrays.stream(property.split(";")).forEach(System.out::println);    }
    
  • Bootstrap CLassloder 加载位置如下:
    JDK\jdk1.8\jre\lib\resources.jar
    JDK\jdk1.8\jre\lib\rt.jar
    JDK\jdk1.8\jre\lib\sunrsasign.jar
    JDK\jdk1.8\jre\lib\jsse.jar
    JDK\jdk1.8\jre\lib\jce.jar
    JDK\jdk1.8\jre\lib\charsets.jar
    JDK\jdk1.8\jre\lib\jfr.jar
    JDK\jdk1.8\jre\classes
  • Extention ClassLoader加载位置如下:
    C:\Program Files\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext;
  • AppClassLoader 加载位置如下:
    就是你项目配置的jar路径以及工程生成的classes的位置,如maven会是 target\classes,或者bin目录;
  • 这些路径的可以从sun.misc.Launcher类得知;

加载器之间的关系

  • 如果类是在boostrapClassLoder下加载,无法获取其加载器
    从报错的情况,因为bootstrapclassloader有加载过rt.jar,这个从前面可以看出来,Integer类是rt.jar里面的类,所以Integer是由bootstrapclassloader进行加载的没错,那为啥是空指针呢?

    1-1
    1-1

    来一张extclassloader的继承图,AppClassLoader也一样的继承关系
    图1
    图1

    这个空指针我是这么理解的:凡是有boostraploader进行加载的类,都是获取不到此类的加载器,因为Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用;

     

  • ExtClassLoadder的父加载器是null
    接下来从源码角度来看下,图2输出的原因(即extClassLoader的父记载器为啥null)

    图2
    图2

    关键从parent方法入手,可以发现在图一的继承关系中找到parent方法是在ClassLoader中如图3,从idea点击过去也可以找到,从方法来看parent是final成员变量,所以找到构造方法,就知道如何它是如何初始化了
    图3
    图3

    从下面的代码可以看出parent初始化有两种方式:
    一种是指定ClassLoader;
    第二种如果没有指定则通过initSystemClassLoader方法进行初始化,这个方法获取classLoader方式是从Launcher类的getClassLoader方法获取的;

     

     protected ClassLoader(ClassLoader parent) {
     this(checkCreateClassLoader(), parent);
     }   
      protected ClassLoader() {
     this(checkCreateClassLoader(), getSystemClassLoader());
     }
     public static ClassLoader getSystemClassLoader() {
     initSystemClassLoader();
     if (scl == null) {
         return null;
     }
     SecurityManager sm = System.getSecurityManager();
     if (sm != null) {
         checkClassLoaderPermission(scl, Reflection.getCallerClass());
     }
            return scl;
     }
     private static synchronized void initSystemClassLoader() {
     if (!sclSet) {
         if (scl != null)
             throw new IllegalStateException("recursive invocation");
         sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
         if (l != null) {
             Throwable oops = null;
             scl = l.getClassLoader();
             try {
                 scl = AccessController.doPrivileged(
                     new SystemClassLoaderAction(scl));
             } catch (PrivilegedActionException pae) {
                 oops = pae.getCause();
                 if (oops instanceof InvocationTargetException) {
                     oops = oops.getCause();
                 }
             }
             if (oops != null) {
                 if (oops instanceof Error) {
                     throw (Error) oops;
                 } else {
                     // wrap the exception
                     throw new Error(oops);
                 }
             }
         }
         sclSet = true;
     }   
      }
    

Launcher类的classLoader方法初始化如下:
从代码可知Launcher.getClassLoader就是获取appclassLoader,意味着一个类的父加载器如果没有指定,则是默认就是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 {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    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 (IllegalAccessException var5) {
                ;
            } catch (InstantiationException var6) {
                ;
            } catch (ClassNotFoundException var7) {
                ;
            } catch (ClassCastException var8) {
                ;
            }
        } else {
            var3 = new SecurityManager();
        }
        if(var3 == null) {
            throw new InternalError("Could not create SecurityManager: " + var2);
        }
        System.setSecurityManager(var3);
    }
}
public ClassLoader getClassLoader() {
    return this.loader;
}

但是这个还是无法解释extClassLoader的父加载器是null,请看Laucher类中ExtClassLoader类的构造法方方法,指定的父加载器就是null,所以从parent变量的获取追踪到extClassloader是由于指定null父加载器,所以导致extClassLoader获取父加载器是null,
而AppClassLoader获取父加载器是extClassLoader,是因为指定了extClassLoader为自己的父加载器;

extclassLoader构造方法
extclassLoader构造方法

 

appclassLoader构造方法
appclassLoader构造方法

 

 

  • 从源码角度理解双亲委托机制以及boostrapClassLoader为啥可以作为extClassLoader的父加载器
    双亲委托是什么,看过博主的文章就知道了,一句话概括

委托是从下向上,然后具体查找过程却是自上至下

从ClassLoader的loadClass可以明白双亲委托机制过程,同时知道如果自定义的ClassLoader是覆盖findClass,而不是loadClass,采用这种方式进行加载可以避免java核心api中定义的类型被自定义的加载器加载,从而出现多个

 

ClassLoadere-loadClass
ClassLoadere-loadClass

 

  • 自定义加载器,自己也写个,跟博主一样,然后调试了一遍,对加载过程再熟悉一遍

    public class MyClassLoader extends  ClassLoader{
    private String filePath;
    
    MyClassLoader(String filePath) {
      this.filePath = filePath;
    }
    
    @Override
     public Class<?> findClass(String name) throws ClassNotFoundException {
      int i = name.lastIndexOf(".") +1;
      String s = name.substring(i) + ".class";
      File file = new File(filePath, s);
      try {
          FileInputStream is = new FileInputStream(file);
          ByteArrayOutputStream bos = new ByteArrayOutputStream();
          int len = 0;
          try {
               while ((len = is.read()) != -1) {
                  bos.write(len);
              }
          } catch (IOException e) {
              e.printStackTrace();
          }
          byte[] data = bos.toByteArray();
          is.close();
          bos.close();
          return defineClass(name,data,0,data.length);
      } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
      }
      return super.findClass(name);
    }
    
    public static void main(String[] args) throws Exception{
      MyClassLoader classLoader = new MyClassLoader("D:\\");
      Class<?> aClass = classLoader.loadClass("com.example.Product");
      if (aClass != null) {
          Object o = aClass.newInstance();
          Method ok = aClass.getDeclaredMethod("ok");
          ok.invoke(o);
      }
    }
    }
    
  • 打破双亲委派模型------引出上下文类加载器
    双亲委派模型的加载方式是从底层加载器到顶层加载器进行加载,但是如果顶层加载器要加载来自底层加载器的类时,此时就传统加载模式就无法完成了。
    这种情况会出现在SPI使用中,引用一段话来说明SPI加载的情况

SPI机制简介
SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
SPI具体约定
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

以下面这段代码来说明双亲委派模型的 逆向加载

    String url = "jdbc:mysql://localhost:3306/mysql";
      //通过java库获取数据库连接
    Connection conn = java.sql.DriverManager.getConnection(url, "root", "123456");

首先调用静态方法必将导致调用者会进行初始化,所以DriverMananger类将初始化,如果类中有静态代码块必将执行,下面是DriverManager类的代码:

 

静态代码k
静态代码k

 

loadInitialDrivers方法
loadInitialDrivers方法

因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不是BootrapLoader,那会是什么加载器呢,就是上下文加载,这个可以从load方法中看出:

load
load

所以这边就可以看出要通过顶层加载器去加载底层加载器的类时,通过上下文加载器实现;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值