Java魔法堂:类加载器入了个门

简介:

一、前言                            

  《Java魔法堂:类加载机制入了个门》中提及整个类加载流程中只有加载阶段作为码农的我们可以入手干预,其余均由JVM处理。本文将记录加载阶段的核心组件——类加载器的相关信息,以便日后查阅。若有纰漏请大家指正,谢谢。

  注意:以下内容基于JDK7和HotSpot VM。

 

二、类加载器种类及其关系                    

从上图可知Java主要有4种类加载器

1. Bootstrap ClassLoader(引导类加载器):作为JVM的一部分无法在应用程序中直接引用,由C/C++实现(其他JVM可能通过Java来实现)。负责加载<JAVA>/jre/lib目录-Xbootclasspath参数所指定的目录统属性sun.boot.class.path指定的目录 中特定名称的jar包。在JVM启动时将通过Bootstrap ClassLoader加载rt.jar,并初始化sun.misc.Launcher从而创建Extension ClassLoader和System ClassLoader实例,和将System ClassLoader实例设置为主线程的默认Context ClassLoader(线程上下文加载器)。

  注意:Bootstrap ClassLoader只会加载特定名称的类库,如rt.jar等。假如我们自己定义一个jar类库丢进<JAVA_HOME>/jre/lib目录下也不会被加载的!

  下面我们看看Bootstrap ClassLoader到底加载了哪些jar包吧!

import java.net.*;
import sun.misc.*;

class Main{
  public static void main(String[] args){
    URL[] urls = Launcher.getBootstrapClassPath().getURLs();
    for (URL url : urls)
      System.out.println(url.toExternalForm());
  }
}
/* vim:!javac % & java Main 后输出
 * lib/resources.jar
 * lib/rt.jar
 * lib/sunrsasign.jar
 * lib/jsse.jar
 * lib/jce.jar
 * lib/charsets.jar
 * lib/jfr.jar
 * lib/classe
 */

2. Extension ClassLoader(扩展类加载器):仅含一个实例,由 sun.misc.Launcher$ExtClassLoader 实现,负责加载<JAVA_HOME>/jre/lib/ext目录系统属性java.ext.dirs所指定的目录 中的所有类库。

3. App/System ClassLoader(系统类加载器):仅含一个实例,由 sun.misc.Launcher$AppClassLoader 实现,可通过 java.lang.ClassLoader.getSystemClassLoader 获取。负责加载 ①系统环境变量ClassPath 或 ②-cp系统属性java.class.path 所指定的目录下的类库。

4. Custom ClassLoader(用户自定义类加载器):可同时存在多个用户自定义的类加载器,具体如何定义请参考后文。

 

除了上面的4种类加载器外,JDK1.2开始引入了另一个类加载器——Context ClassLoader(线程上下文加载器)

5. Context ClassLoader(线程上下文加载器):默认 为System ClassLoader,可通过Thread.currentThread().setContextClassLoader(ClassLoader) 来设置,可通过ClassLoader Thread.currentThread().getContextClassLoader()来获取。每个线 程均将Context ClassLoader预先设置为父线程的Context ClassLoader。该类加载器主要用于打破双亲委派模型,容许父类加载器通过子类加载器加载所需的类库。

 

三、双亲委派模型                        

  在介绍双亲委派模型前先看看以下示例:

/*
 * Main.java文件
 */
import java.net.*;
import java.lang.reflect.*;

class Main{
  public static void main(String[] args) 
      throws ClassNotFoundException, MalformedURLException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException{
    ClassLoader pClassLoader = ClassLoader.getSystemClassLoader(); // 以System ClassLoader作为父类加载器
    URL[] baseUrls = {new URL("file:/d:/testLib/")}; // 搜索类库的目录
    final String binaryName = "com.fsjohnhuang.HelloWorld"; // 需要加载的类的二进制名称

    ClassLoader userClassLoader1 = new URLClassLoader(baseUrls, pClassLoader);
    ClassLoader userClassLoader2 = new URLClassLoader(baseUrls, pClassLoader);
    Class clazz1 = userClassLoader1.loadClass(binaryName);
    Class clazz2 = userClassLoader2.loadClass(binaryName);
    Object instance1 = clazz1.newInstance();
    Object instance2 = clazz2.newInstance();
    // 调用say方法
    clazz1.getMethod("say").invoke(instance1);
    clazz2.getMethod("say").invoke(instance2);
    // 输出类的二进制名称
    System.out.println(clazz1.toString());
    System.out.println(clazz2.toString());

    // 比较两个类的地址是否相同
    System.out.println(clazz1 == clazz2);
    // 比较两个类是否相同或是否为继承关系
    System.out.println(clazz1.isAssignableFrom(clazz2));
    // 查看类型转换是否成功
    boolean ret = true;
    try{
        clazz2.cast(instance1);
    }
    catch(ClassCastException e){
        ret = false;
    }
    System.out.println(ret);
  } 
}
   结果:
Hello World!
Hello World!
class com.fsjohnhuang.HelloWorld
class com.fsjohnhuang.HelloWorld
false
false
false

  奇了个怪了,为什么两个类的Class实例不一样呢?这是因为 对于任意一个类,都需要由加载它的类加载器和该类本身一同确立其在JVM中的唯一性。也就是说对于同一个类文件,通过不同的类加载器加载那么在JVM中就生成了不同的类。

  那现在问题来了,我们知道由java.lang.*(打包到rt.jar中)是由Bootstrap ClassLoader加载的,现在我闲着蛋疼自定义一个类加载器来加载java.lang.String,按照上面的定义那JVM中就有两个 java.lang.String类了,然后出现下列问题:



if (myString.newInstance() instanceof String){
  System.out.println("1"); // 绝对不会执行这一句
}
else{
   System.out.println("2");
}

  注意:由于类会通过自身对应的类加载器加载其引用的其他类。若myString中还引用了其他类,那么将会通过我自定的类加载器来加载一次哦!

  假如会发生上述情况,真实项目中发生的问题就更大了。(注意:上述代码在真实环境绝对无法成立,自定义的类加载器本身就被限制为无法加载java.*的类哦!)

  双亲委派模型就是用于解决上述问题,越基础的类由越上层的类加载器进行加载,如Java API类库则有Bootstrap ClassLoader加载。具体如下:

  当一个类加载器收到类加载的请求,首先会将请求委派给父类加载器,这样一层一层委派到Bootstrap ClassLoader。然后加载器根据请求尝试搜索和加载类,若搜索失败则向子类加载器反馈信息(抛出 ClassNotFoundException),然后子类加载器才尝试自己去加载。JAVA中采用组合的方式实现双亲委派模型,而不是继承的方式。

  不难发现Bootstrap、Extension和System三种类加载器默认的加载类的目录路径均是不同的,也可以说 类的来源地与类加载器应该是一一对应。位于同一来源地的类应该由相同的类加载器加载,而不是由其他类加载来加载,或者通过双亲委派模型将加载请求传递给相 应的类加载器。因最基础的类库通过Bootstrap加载,其次则由Extension加载,应用程序的则由System来加载,应用程序动态依赖的功能 模块则通过用户自定义类加载器加载。

 

四、非双亲委派模型                        

  双亲委派模型解决了类重复加载的乱象。但现在问题又来了,双亲委派模型仅限于子类加载器将加载请求转发到父类加载器,请求是单向流动的,那如果通过父类加载器加载一个在子类加载器管辖类来源的类,那怎么办呢?再说真的有这样的场景吗?

  首先我们将 “通过父类加载器加载一个在子类加载器管辖类来源的类” 具体化为 “在一个由Bootstrap ClassLoader加载的类中动态加载其他目录路径下的类库”,这样我们就轻松地找到JNDI、JAXP等SPI(Service Provider Interface)均符合这种应用场景。以下就以JAXP来介绍吧!

  JAXP(Java API for XML Processing),用于处理XML文档的API,接口和默认实现位于rt.jar中,但增强型的具体实现则由各个厂家提供且以第三方jar包的形式部署在项目的CLASSPATH下。其中抽象类 javax.xml.parsers.DocumentBuilderFactory的类方法newInstance(String factoryClassName, ClassLoader classLoader) 可根据二进制名称获取由各厂家具体实现的DocumentBuilderFactory实例。现在以 javax.xml.parsers.DocumentBuilderFactory.newInstance(" org.apache.xerces.jaxp.DocumentBuilderFactoryImpl", null) 的调用形式来深入下去。

  首先假设newInstance内部是以以下方式加载类的

Class.forName("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
// 或
this.getClass().getClassLoader.loadClass("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");

 由于DocumentBuilderFactory是由Boostrap ClassLoader加载的,因此上述操作结果是通过Bootstrap ClassLoader来加载第三方类库,结果必须是ClassNotFoundException的。也就是说我们需要获取System ClassLoader或它的子类加载器才能成功加载这个类。

  首先想到的是通过ClassLoader.getSystemClassLoader()方法来获取System ClassLoader。然而JDK1.2又引入了另一个更灵活的方式,那就是Context ClassLoader(线程上下文类加载器,默认为System ClassLoader),通过Context ClassLoader我们可以获取System ClassLoader或它的子类加载器,从而可以加载CLASSPATH和其他路径下的类库。

  newInstance(String, ClassLoader)的实际实现是调用FactoryFinder.newInstance方法,而该方法则调用getProviderClass方 法来获取Class实例,getProviderClass方法中则通过SecuritySupport的实例方法 getContextClassLoader()来获取类加载器,代码片段如下:

ClassLoader getContextClassLoader()
    throws SecurityException
  {
    return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction()
    {
      public Object run() {
        ClassLoader cl = null;

        cl = Thread.currentThread().getContextClassLoader();

        if (cl == null) {
          cl = ClassLoader.getSystemClassLoader();
        }
        return cl;
      }
    });
  }

 注意:Context ClassLoader可是要慎用哦!因为可以通过setContextClassLoader方法动态设置线程上下文类加载器,也就是有可能每次调用时的类加载器均不相同(所管辖的目录路径也不相同),在并发环境下就更容易出问题了。

 

五、从源码理解                          

  首先我们看看ExtClassLoader和AppClassLoader是如何创建的,目光移到sun/misc/Launcher.java文件中, 而ExtClassLoader和AppClassLoader则以Luancher的内部类的形式实现。在Launcher类进入初始化阶段时会创建一 个Launcher实例,其构造函数中会实例化ExtClassLoader,然后以ExtClassLoader实例作为父类加载器来实例化 AppClassLoader,并将AppClassLoader实例设置为主线程默认的Context ClassLoader。

public Launcher()
  {
    ExtClassLoader localExtClassLoader;
    try
    {
      // 实例化ExtClassLoader
      localExtClassLoader = ExtClassLoader.getExtClassLoader();
    } catch (IOException localIOException1) {
      throw new InternalError("Could not create extension class loader");
    }

    try
    {
      // 实例化AppClassLoader
      this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
    } catch (IOException localIOException2) {
      throw new InternalError("Could not create application class loader");
    }
    // 主线程的默认Context ClassLoader
    Thread.currentThread().setContextClassLoader(this.loader);

    String str = System.getProperty("java.security.manager");
    if (str != null) {
      SecurityManager localSecurityManager = null;
      if (("".equals(str)) || ("default".equals(str)))
        localSecurityManager = new SecurityManager();
      else
        try {
          localSecurityManager = (SecurityManager)this.loader.loadClass(str).newInstance();
        } catch (IllegalAccessException localIllegalAccessException) {
        } catch (InstantiationException localInstantiationException) {
        } catch (ClassNotFoundException localClassNotFoundException) {
        }
        catch (ClassCastException localClassCastException) {
        }
      if (localSecurityManager != null)
        System.setSecurityManager(localSecurityManager);
      else
        throw new InternalError("Could not create SecurityManager: " + str);
    }
  }
   ExtClassLoader和AppClassLoader均继承了java.net.URLClassLoader,并且仅对类的加载、搜索目录路径作修改而已。如AppClassLoader的getAppClassLoader方法:
static class AppClassLoader extends URLClassLoader
  {
    public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader)
      throws IOException
    {
      // 获取搜索、加载类的目录路径
      String str = System.getProperty("java.class.path");
      final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str);

      return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction()
      {
        public Launcher.AppClassLoader run() {
          URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile);
          // 设置类加载器的搜索、加载类的目录路径,并创建一个类加载器实例
          return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader);
        }
      });
    }

  在研究URLClassLoader之前我们先看看java.lang.ClassLoader,除Bootstrap ClassLoader外所有类加载器必须继承ClassLoader。还记得  ClassLoader.getSystemClassLoader().loadClass("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl") 吧,现在我们就从loadClass出发,看看整个类加载机制吧!
protected Class<?> loadClass(String paramString, boolean paramBoolean)
    throws ClassNotFoundException
  {
    synchronized (getClassLoadingLock(paramString))
    {
      // 检查该类是否已加载过,若已加载过则返回缓存中的Class实例
      Class localClass = findLoadedClass(paramString);
      // 下面是双亲委派模型的具体实现
      if (localClass == null) {
        long l1 = System.nanoTime();
        try {
          // 若有父类加载器则将加载请求传递到父类加载器
          // 若parent变量为null则表示父类加载器是Bootstrap ClassLoader,同样将加载请求传递到父类加载器
          if (this.parent != null)
            localClass = this.parent.loadClass(paramString, false);
          else {
            localClass = findBootstrapClassOrNull(paramString);
          }
        }
        catch (ClassNotFoundException localClassNotFoundException)
        {
            // 父类加载器无法加载给类时则抛出异常
        }

        if (localClass == null)
        {
          long l2 = System.nanoTime();
          // 开始加载类了!
          localClass = findClass(paramString);

          PerfCounter.getParentDelegationTime().addTime(l2 - l1);
          PerfCounter.getFindClassTime().addElapsedTimeFrom(l2);
          PerfCounter.getFindClasses().increment();
        }
      }
      // 对类执行解析操作
      if (paramBoolean) {
        resolveClass(localClass);
      }
      return localClass;
    }
  }

   可以看到loadClass方法内部主要为双亲委派模型的实现,实际的类加载操作是在findClass方法中实现的。另外由于不允许同一个类加载器重复 加载同一个类,因此当对同一个类重复进行加载操作时,则通过findLoadedClass方法来返回已有的Class实例。

  ClassLoader中指提供findClass的定义,具体实现由子类提供。而URLClassLoader的findClass则是通过 URLClassPath实例来获取类的二进制数据,然后调用defineClass对二进制数据进行初步验证,然后在由ClassLoader的 defineClass进行其余的验证后生成Class实例返回。

protected Class<?> findClass(final String paramString)
    throws ClassNotFoundException
  {
    try
    {
      return (Class)AccessController.doPrivileged(new PrivilegedExceptionAction()
      {
        public Class run() throws ClassNotFoundException {
          // ucp为URLClassPath实例
          // 通过URLClassPath实例获取类的二进制数据
          String str = paramString.replace('.', '/').concat(".class");
          Resource localResource = URLClassLoader.this.ucp.getResource(str, false);
          if (localResource != null) {
            try {
              // 调用URLClassLoader的defineClass方法验证类的二进制数据并返回Class实例
              return URLClassLoader.this.defineClass(paramString, localResource);
            } catch (IOException localIOException) {
              throw new ClassNotFoundException(paramString, localIOException);
            }
          }
          throw new ClassNotFoundException(paramString);
        }
      }
      , this.acc);
    }
    catch (PrivilegedActionException localPrivilegedActionException)
    {
      throw ((ClassNotFoundException)localPrivilegedActionException.getException());
    }
  }

  总结一下, 类加载过程为loadClass -> findClass -> defineClass。loadClass为双亲委派的实现,defineClass为类数据验证和生成Class实例,findClass为获取类的二进制数据。

  那么我们自定义类加载器时只需重写findClass就可以加载不同路径下的类库了!

 

六、手动加载类吧,骚年!                   

  手动加载类的形式是多样的,具体如下:

  1. 利用现有的类加载器


// 通过当前类的类加载器加载(会执行初始化)
Class.forName("二进制名称");
Class.forName("二进制名称", true, this.getClass().getClassLoader());

// 通过当前类的类加载器加载(不会执行初始化)
Class.forName("二进制名称", false, this.getClass().getClassLoader());
this.getClass().loadClass("二进制名称");

// 通过系统类加载器加载(不会执行初始化)
ClassLoader.getSystemClassLoader().loadClass("二进制名称");

// 通过线程上下文类加载器加载(不会执行初始化)
Thread.currentThread().getContextClassLoader().loadClass("二进制名称");

  2. 利用URLClassLoader



URL[] baseUrls = {new URL("file:/d:/testLib/")};
URLClassLoader loader = new URLClassLoader(baseUrl, ClassLoader.getContextClassLoader());
Class clazz = loader.loadClass("com.fsjohnhuang.HelloWorld");

  3. 继承ClassLoader自定义类加载器



public class MyClassLoader extends ClassLoader{
  private String dir;

  public MyClassLoader(String dir, ClassLoader parent){
    super(parent);
    this.dir = dir;
  }
  
  @Override
  protect Class<?> findClass(String binaryName) throws ClassNotFoundException{
    String pathSegmentSeperator = System.getProperty("file.separator");
    String path = binaryName.replace(".", pathSegmentSeperator ).concat(".class");

    FileInputStream fis = new FileInputStream(dir + pathSegmentSeperator  + path);
    byte[] b = new byte[fis.available()];
    fis.read(b, 0, b.length);
    fis.close();
    return defineClass(binaryName, b, 0, b.length);
  }
}

七、如何卸载类?                        

类卸载实质上就是GC对方法区(HotSpot中可称为永久代)的类数据进行垃圾回收。

虚拟机规范:
  A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result , system classes may never be unloaded.
  只有当加载该类型的类加载器实例( 非类加载器类型) 为unreachable 状态时,当前被加载的类型才被卸载. 启动类加载器实例永远为reachable 状态,由启动类加载器加载的类型可能永远不会被卸载。
Unreachable状态的解释:
  1 、A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
  2 、finalizer-reachable: A finalizer-reachable object can be reached from some finalizable object through some chain of references, but not from any live thread. An unreachable object cannot be reached by either means.
也就是说 

1. 加载器的类实例已经被回收。

2. 类的实例已经被回收。

3. 类的Class实例没有被任何地方引用,无法在任何地方通过反射访问该类。

  对于Bootstrap、Ext和Sys类加载器来说正常情况下是不会被回收的,只有用户自定义类加载器才可以。通过 $ java -verbose:class Main 执行以下代码。



import java.net.*;
import java.io.*;

public class Main{
    public  static class MyURLClassLoader  extends  URLClassLoader { 
       public  MyURLClassLoader() { 
          super (getMyURLs()); 
       } 

       private   static  URL[] getMyURLs() { 
        try  { 
           return   new  URL[]{ new  File ("d:/").toURL()}; 
        }  catch  (Exception e) { 
           e.printStackTrace(); 
           return   null ; 
        } 
      } 
    } 

    public static void main(String[] args) throws IOException{
     try  { 
            MyURLClassLoader classLoader =  new  MyURLClassLoader(); 
            Class classLoaded = classLoader.loadClass("RMDIR"); 
            System.out.println(classLoaded.getName()); 
   
            classLoaded =  null ; 
            classLoader =  null ; 
  
           System.out.println(" 开始GC"); 
           System.gc(); 
           System.out.println("GC 完成"); 
       System.in.read();
         }  catch  (Exception e) { 
             e.printStackTrace(); 
         } 
    }
}

八、加载图片、视频等非类资源                   

  ClassLoader除了用于加载类外,还可以用于加载图片、视频等非类资源。同样是采用双亲委派模型将加载资源的请求传递到顶层的Bootstrap ClassLoader,在其管辖的目录下搜索资源,若失败才逐层返回逐层搜索。

相关的实例方法如下:

URL getResource(String name)
InputStream getResourceAsStream(String name)
Enumeration<URL> getResources(String name)

而相关的类方法均是调用系统类加载器的上述方法而已。

 

九、总结                            

  若有纰漏请大家指正,谢谢!

目录
相关文章
|
1月前
|
Java 测试技术 API
滚雪球学Java(10):Java注释
【2月更文挑战第22天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!!
64 2
|
2月前
|
移动开发 Cloud Native Java
java进化史
Java进化史见证了编程语言从诞生到成熟的整个过程,以及其在全球范围内的广泛应用。从1995年Java语言首次发布至今,Java已经走过了二十多年的历程,期间经历了数次重要的版本更新和技术变革。本文将回顾Java的进化历程,分析关键版本的特点与影响,并探讨Java未来的发展趋势。
|
6月前
|
安全 Java 程序员
java 程序员 和 三八女神节有什么神秘的关系,你晓得吗? 用Java 给女神绘制一张贺卡你会吗?
java 程序员 和 三八女神节有什么神秘的关系,你晓得吗? 用Java 给女神绘制一张贺卡你会吗?
56 0
java 程序员 和 三八女神节有什么神秘的关系,你晓得吗? 用Java 给女神绘制一张贺卡你会吗?
|
3月前
|
存储 Java 编译器
揭秘Java类加载机制与双亲委派:知其所以然,舞动代码世界
揭秘Java类加载机制与双亲委派:知其所以然,舞动代码世界
|
9月前
|
Java
【Java】Java基础训练之超市购物程序设计
【Java】Java基础训练之超市购物程序设计
246 0
|
11月前
|
IDE 前端开发 JavaScript
你见过哪些目瞪口呆的 Java 代码技巧? 上
你见过哪些目瞪口呆的 Java 代码技巧? 上
|
11月前
|
设计模式 算法 Java
你见过哪些目瞪口呆的 Java 代码技巧? 下
你见过哪些目瞪口呆的 Java 代码技巧? 下
|
设计模式 IDE 前端开发
你见过哪些目瞪口呆的 Java 代码?
你见过哪些目瞪口呆的 Java 代码?
114 0
|
存储 分布式计算 安全
我与java的第一次相识
我与java的第一次相识
95 0
我与java的第一次相识
|
设计模式 IDE 前端开发
你见过哪些目瞪口呆的 Java 代码技巧?
本文不是一个吹嘘的文章,不会讲很多高深的架构,相反,会讲解很多基础的问题和写法问题,如果读者自认为基础问题和写法问题都是不是问题,那请忽略这篇文章,节省出时间去做一些有意义的事情。