细究Java类加载机制

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/J080624/article/details/84835493

【1】JVM类加载机制概述与分类

在这里插入图片描述

类加载器主要分为两类,一类是 JDK 默认提供的,一类是用户自定义的。

① JDK 默认提供三种类加载器

  • Bootstrap ClassLoader 启动类加载器:每次执行 java 命令时都会使用该加载器为虚拟机加载核心类。该加载器是由 native code 实现,而不是 Java 代码,加载类的路径为 <JAVA_HOME>/jre/lib。特别的 <JAVA_HOME>/jre/lib/rt.jar 中包含了 sun.misc.Launcher类, 而sun.misc.Launcher$ExtClassLoader 和 sun.misc.Launcher$AppClassLoader 都是 sun.misc.Launcher的内部类,所以拓展类加载器和系统类加载器都是由启动类加载器加载的。
    在这里插入图片描述

这个加载器很特殊,它不是Java类,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。

Java字节码文件是经过编译器预处理过的一种文件,是JAVA的执行文件存在形式,它本身是二进制文件,但是不可以被系统直接执行,而是需要虚拟机解释执行。

  • Extension ClassLoader, 拓展类加载器:用于加载拓展库中的类。拓展库路径为 <JAVA_HOME>/jre/lib/ext/。实现类为 sun.misc.Launcher$ExtClassLoader。还可以加载-D java.ext.dirs选项指定的目录。
    在这里插入图片描述

我们先前的内容有说过,可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。这里我们通过可以编写测试代码。

System.out.println(System.getProperty("java.ext.dirs"));

结果如下:

C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
  • System ClassLoader 系统类加载器:用于加载 CLASSPATH 中的类。实现类为 sun.misc.Launcher$AppClassLoader。一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
    在这里插入图片描述

AppClassLoader加载的就是java.class.path下的路径。

System.out.println(System.getProperty("java.class.path"));

Custom ClassLoader, 一般都是 java.lang.ClassLoder 的子类。正统的类加载机制是基于双亲委派的,也就是当调用类加载器加载类时,首先将加载任务委派给双亲,若双亲无法加载成功时,自己才进行类加载。在实例化一个新的类加载器时,我们可以为其指定一个 parent,即双亲,若未显式指定,则 System ClassLoader–AppClassLoader 就作为默认双亲。

java.net.URLClassLoader:该类加载器用来加载 URL 指定的 JAR 文件或目录中的类和资源,以/结尾的 URL 认为是目录,否则认为是 JAR 文件。

// 尝试通过 URLClassLoader 来加载桌面下的 Test 类。
public static void main(String[] args) {
     try {
           URL[] urls = new URL[1];
           URLStreamHandler streamHandler = null;
           File classPath = new File("C:\\Users\\Administrator\\Desktop\\");
           String repository = new URL("file", null,  classPath.getCanonicalPath() + File.separator).toString();
           urls[0] = new URL(null, repository, streamHandler);	 
           ClassLoader loader = new URLClassLoader(urls);
           Class testClass = loader.loadClass("Test");
           //output:  class Test
           System.out.println(testClass);
           // output:  java.net.URLClassLoader@7f31245a
           System.out.println(testClass.getClassLoader());
       } catch (MalformedURLException e) {
           e.printStackTrace();
       } catch (IOException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       }
   }

② java.lang.ClassLoader

ClassLoader 是一个抽象类,负责加载类,给定类的二进制名称,类加载器应该尝试定位或生成构成类的定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。即通过类名从文件系统中找到对应的*.class文件并读取。

每一个类的Class对象都包含一个getClassLoader() 方法引用定义该类的ClassLoader。但是数组并非由类加载器创建,而是由JVM在需要的时候创建。调用数组对象的getClassLoader()实际上返回的时数组元素对象的类加载器。如果数组元素是基本类型,那么就没有关联的类加载器。

应用程序实现了ClassLoader的子类,以扩展Java虚拟机动态加载类的方式。类加载程序通常可以由安全管理器使用来指示安全域。

ClassLoader使用一个委派模型来搜索类和资源。 ClassLoader 的每个实例都有一个关联的父类加载器。当请求查找类或资源时,ClassLoader实例将在自身试图查找类或资源本身之前,将对类或资源的搜索委托给其父类加载器。虚拟机的内置类加载器("bootstrap class loader")本身没有父类,但是可以作为ClassLoader实例的父类。

支持类并发加载的类加载器被称为具有并行能力类加载器,需要在类初始化时通过调用ClassLoader.registerAsllelCapable方法注册自己。需要注意的是,这是默认行为。但是其子类如果是具有并行能力的,仍然需要注册自己。

在委托模型不是严格分层的环境中,类加载器需要具有并行能力,否则类加载可能导致死锁,因为加载器锁在类加载过程的持续时间内被保持(参见loadClass方法)。

通常,Java虚拟机以平台依赖的方式从本地文件系统加载类。例如,在UNIX系统中,虚拟机从CLASSPATH环境变量定义的目录加载类。然而,有些类可能不源自文件,它们可能源自其他途径,例如网络,或者它们可以由应用程序构造。方法defineClass(String,byte[],int,int)将字节数组转换为类Class的实例。这个新定义的类的实例可以使用Class.newInstance创建。

类加载器创建的对象的方法和构造函数可能引用其他类。为了确定所引用的类,Java虚拟机调用最初创建引用类的类加载器的loadClass方法。

例如,应用程序可以创建一个网络类加载器来从服务器下载类文件。示例代码如下:

ClassLoader loader= new NetworkClassLoader(host,port);
Object main= loader.loadClass("Main", true).newInstance();

网络类加载器子类必须定义方法findClassloadClassData,以便从网络加载类。一旦下载了组成类的字节,它就应该使用defineClass方法来创建类实例。示例实现是:

class NetworkClassLoader extends ClassLoader {
		 String host;
		 int port;
		 public Class findClass(String name) {
			 byte[] b = loadClassData(name);
			 return defineClass(name, b, 0, b.length);
		 }
		 
		 private byte[] loadClassData(String name) {
              // load the class data from the connection
		        //...      		          
	 	}
}

需要注意的是,ClassLoader类中方法的参数(类名)必须是符合Java语言规范定义的二进制名称The Java™ Language Specification。实例如下:

java.lang.String
javax.swing.JSpinner$DefaultEditor
java.security.KeyStore$Builder$FileBuilder$1
java.net.URLClassLoader$3$1

③ 类加载器执行顺序

执行顺序为:

  1. Bootstrap CLassloder
  2. Extention ClassLoader
  3. AppClassLoader

看sun.misc.Launcher,它是一个java虚拟机的入口应用:

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    //这里获取bootClassPath -BootStarp ClassLoader加载路径
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
         // Create the extension class loader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
         // Now create the class loader to use to launch the application
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
 //设置AppClassLoader为线程上下文类加载器
        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);
        }

    }
 /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return this.loader;
    }

   /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {//...}

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {//...}

源码有精简,我们可以得到相关的信息。

  1. Launcher初始化了ExtClassLoader和AppClassLoader。
  2. 使用ExtClassLoader创建AppClassLoader。
  3. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty(“sun.boot.class.path”)得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。

我们可以先代码测试一下sun.boot.class.path是什么内容。

System.out.println(System.getProperty("sun.boot.class.path"));

得到的结果是:

C:\Program Files\Java\jdk1.8.0_101\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\classes

可以看到,这些全是JRE目录下的jar包或者是class文件。自此我们已经知道了BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.path、java.ext.dirs和java.class.path来加载资源文件的。

JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件。JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。


④ 每个类加载器实例都有一个父加载器

AppClassLoader的父加载器是ExtClassLoader,而ExtClassLoader并没有显示指定parent–null,但是Bootstrap CLassLoader是其父加载器。

如上面贴过得Launcher类源码所示:

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
        //获取ExtClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
        //使用ExtClassLoader获取AppClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

private final ClassLoader parent;定义在ClassLoader中,有关parent的赋值在其构造方法中:

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

需要注意的是父加载器和父类不同,如下所示AppClassLoader和ExtClassLoader均继承于URLClassLoader:

static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}

⑤ 双亲委派模型

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

步骤如下:

  • 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
  • 递归,重复第1部的操作。
  • 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
  • Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
  • ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。

如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

在这里插入图片描述


⑥ ClassLoader.loadClass()方法

该方法即为类加载的过程。如下所示:

  • 通过 findLoadedClass() 检查该类是否已经被加载。该方法为 native code 实现,若该类已加载则返回。
  • 若未加载则委派给双亲,parent.loadClass(),若成功则返回。
  • 若未成功,则调用 findClass() 方法加载类。java.lang.ClassLoader 中该方法只是简单的抛出一个 ClassNotFoundException 所以,自定义的 ClassLoader 都需要 Override findClass() 方法。
  • 如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。

loadClass方法源码实例如下(通过指定的全限定类名加载class,它通过同名的loadClass(String,boolean)方法):

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	//如果父类不为null则使用父类加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    	//Returns a class loaded by the bootstrap class loader;
                    	// or return null if not found.
                    	//findBootstrapClass同样是一个native 方法
                        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
                    // to find the class.
                    long t1 = System.nanoTime();
                    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;
        }
    }

ClassLoader.findClass方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

【2】Tomcat 8.5.15类加载机制

在这里插入图片描述

Tomcat 使用正统的类加载机制(双亲委派),但部分地方做了改动。

Bootstrap classLoader 和 Extension classLoader 的作用不变。

System classLoader 正常情况下加载的是 CLASSPATH 下的类,但是 Tomcat 的启动脚本并未使用该变量,而是从以下仓库下加载类:

  • $CATALINA_HOME/bin/bootstrap.jar 包含了 Tomcat 的启动类。在该启动类中创建了 Common classLoader、Catalina classLoader、shared classLoader。因为 $CATALINA_BASE/conf/catalina.properties 中只对 common.loader 属性做了定义,server.loader 和 shared.loader 属性为空,所以默认情况下,这三个 classLoader 都是 CommonLoader。具体的代码逻辑可以查阅 org.apache.catalina.startup.Bootstrap 类的 initClassLoaders() 方法和 createClassLoader() 方法。
  • $CATALINA_BASE/bin/tomcat-juli.jar 包含了 Tomcat 日志模块所需要的实现类。
  • $CATALINA_HOME/bin/commons-daemon.jar。

Common classLoader 是位于 Tomcat 应用服务器顶层的公用类加载器。由其加载的类可以由 Tomcat 自身类和所有应用程序使用。扫描路径由 $CATALINA_BASE/conf/catalina.properties文件中的 common.loader 属性定义。默认是 $CATALINA_HOME/lib。

catalina classLoader 用于加载服务器内部可见类,这些类应用程序不能访问。

shared classLoader 用于加载应用程序共享类,这些类服务器不会依赖。

Webapp classLoader 。每个应用程序都会有一个独一无二的 webapp classloader,他用来加载本应用程序/WEB-INF/classes 和 /WEB-INF/lib 下的类。

Webapp classLoader 的默认行为会与正常的双亲委派模式不同:
* 从 Bootstrap classloader 加载。
* 若没有,从 /WEB-INF/classes 加载。
* 若没有,从 /WEB-INF/lib/*.jar 加载。
* 若没有,则依次从 System、Common、shared 加载(该步骤使用双亲委派)。

当然了,我们也可以通过配置来使 Webapp classLoader 严格按照双亲委派模式加载类:

  • 通过在工程的 META-INF/context.xml(和 WEB-INF/classes 在同一目录下) 配置文件中添加 <Loader delegate="true"/>
  • 因为 Webapp classLoader 的实现类是 org.apache.catalina.loader.WebappLoader,他有一个属性叫 delegate, 用来控制类加载器的加载行为,默认为 false,我们可以使用 set 方法,将其设为 true 来启用严格双亲委派加载模式。

严格双亲委派模式加载步骤:

  • 从 Bootstrap classloader 加载。
  • 若没有,则依次从 System、Common、shared 加载。
  • 若没有,从 /WEB-INF/classes 加载。
  • 若没有,从 /WEB-INF/lib/*.jar 加载。

【3】自定义ClassLoader

步骤如下:

  • 编写一个类继承自ClassLoader抽象类。
  • 复写它的findClass()方法。
  • 在findClass()方法中调用defineClass()。

假设我们需要一个自定义的classloader,默认加载路径为D:\hh下的jar包和资源。

① 编写测试类

如下所示,并将其编译过的class文件放在D:\hh下。

package com.jane.controller;

/**
 * Created by Janus on 2018/12/9.
 */
public class TestClassLoader {
    
    public void say(){
        System.out.println("Hello");
    }
}

② 编写DiskClassLoader

如下所示:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * Created by Janus on 2018/12/9.
 */
public class DiskClassLoader extends ClassLoader {
    private String mLibPath;

    public DiskClassLoader(String path) {
        // TODO Auto-generated constructor stub
        mLibPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // TODO Auto-generated method stub

        String fileName = getFileName(name);

        File file = new File(mLibPath,fileName);
	 System.out.println("DiskClassLoader.findClass "+fileName);
        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);
    }

    //获取要加载 的class文件名--name为类限定全类名
    private String getFileName(String name) {
        // TODO Auto-generated method stub
        int index = name.lastIndexOf('.');
        if(index == -1){
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}

在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。


③ 编写测试代码

测试代码如下:

public class ClassLoaderTest {
    public static void main(String[] args){
        DiskClassLoader diskLoader = new DiskClassLoader("D:\\hh");
        try {
            //加载class文件--参数为完全限定类名
            Class c = diskLoader.loadClass("com.jane.controller.TestClassLoader");

            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //通过反射调用TestClassLoader类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException
                        | NoSuchMethodException
                        | SecurityException |
                        IllegalArgumentException |
                        InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}

运行结果如下(测试的时候将TestClassLoader 从IDE中移除掉,否则AppClassLoader将会从classpath下加载):

DiskClassLoader.findClass TestClassLoader.class
Hello

另外,如下图所示,可以看到DiskClassLoader的parent为AppClassLoader
在这里插入图片描述

参考博文:https://blog.csdn.net/briblue/article/details/54973413。

猜你喜欢

转载自blog.csdn.net/J080624/article/details/84835493