JVM自定义类加载器加载指定classPath下的全部class及jar

1、JVM中的类加载器类型

  从Java虚拟机的角度讲,只有两种不一样的类加载器:启动类加载器和其余类加载器。
  1.启动类加载器(Boostrap ClassLoader):这个是由c++实现的,主要负责JAVA_HOME/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工做。
  2.其余类加载器:由java实现,能够在方法区找到其Class对象。这里又细分为几个加载器
    a).扩展类加载器(Extension ClassLoader):负责用于加载JAVA_HOME/lib/ext目录中的,或者被-Djava.ext.dirs系统变量指定所指定的路径中全部类库(jar),开发者能够直接使用扩展类加载器。java.ext.dirs系统变量所指定的路径的能够经过System.getProperty("java.ext.dirs")来查看。
    b).应用程序类加载器(Application ClassLoader):负责java -classpath或-Djava.class.path所指的目录下的类与jar包装入工做。开发者能够直接使用这个类加载器。在没有指定自定义类加载器的状况下,这就是程序的默认加载器。
    c).自定义类加载器(User ClassLoader):在程序运行期间, 经过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。html


  这四个类加载器的层级关系,以下图所示。java

      

 

2、为何要自定义类加载器

  1. 区分同名的类:假定在tomcat 应用服务器,上面部署着许多独立的应用,同时他们拥有许多同名却不一样版本的类。要区分不一样版本的类固然是须要每一个应用都拥有本身独立的类加载器了,不然没法区分使用的具体是哪个。
  2. 类库共享:每一个web应用在tomcat中均可以使用本身版本的jar。但存在如Servlet-api.jar,java原生的包和自定义添加的Java类库能够相互共享。
  3. 增强类:类加载器能够在 loadClass 时对 class 进行重写和覆盖,在此期间就能够对类进行功能性的加强。好比使用javassist对class进行功能添加和修改,或者添加面向切面编程时用到的动态代理,以及 debug 等原理。
  4. 热替换:在应用正在运行的时候升级软件,不须要从新启动应用。好比toccat服务器中JSP更新替换。

 

3、自定义类加载器

  3.1 ClassLoader实现自定义类加载器相关方法说明

    要实现自定义类加载器须要先继承ClassLoader,ClassLoader类是一个抽象类,负责加载classes的对象。自定义ClassLoader中至少须要了解其中的三个的方法: loadClass,findClass,defineClass。
   c++

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

    loadClass:JVM在加载类的时候,都是经过ClassLoader的loadClass()方法来加载class的,loadClass使用双亲委派模式。若是要改变双亲委派模式,能够修改loadClass来改变class的加载方式。双亲委派模式这里就不赘述了。
    findClass:ClassLoader经过findClass()方法来加载类。自定义类加载器实现这个方法来加载须要的类,好比指定路径下的文件,字节流等。
    definedClass:definedClass在findClass中使用,经过调用传进去一个Class文件的字节数组,就能够方法区生成一个Class对象,也就是findClass实现了类加载的功能了。git

    贴上一段ClassLoader中loadClass源码,见见真面目...
      github

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 {
                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
                // 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;
    }
}

    源码说明...web

/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/

   翻译过来大概是:使用指定的二进制名称来加载类,这个方法的默认实现按照如下顺序查找类: 调用findLoadedClass(String)方法检查这个类是否被加载过 使用父加载器调用loadClass(String)方法,若是父加载器为Null,类加载器装载虚拟机内置的加载器调用findClass(String)方法装载类, 若是,按照以上的步骤成功的找到对应的类,而且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。 ClassLoader的子类最好覆盖findClass(String)而不是这个方法。 除非被重写,这个方法默认在整个装载过程当中都是同步的(线程安全的)。编程

   resolveClass:Class载入必须连接(link),连接指的是把单一的Class加入到有继承关系的类树中。这个方法给Classloader用来连接一个类,若是这个类已经被连接过了,那么这个方法只作一个简单的返回。不然,这个类将被按照 Java™规范中的Execution描述进行连接。segmentfault

 3.2 自定义类加载器实现

    按照3.1的说明,继承ClassLoader后重写了findClass方法加载指定路径上的class。先贴上自定义类加载器。api

package com.chenerzhu.learning.classloader;

import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @author chenerzhu
 * @create 2018-10-04 10:47
 **/
public class MyClassLoader extends ClassLoader {
    private String path;

    public MyClassLoader(String path) {
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String name) {
        try {
            return Files.readAllBytes(Paths.get(path));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

    

    以上就是自定义的类加载器了,实现的功能是加载指定路径的class。再看看如何使用。 数组

package com.chenerzhu.learning.classloader;

import org.junit.Test;

/**
 * Created by chenerzhu on 2018/10/4.
 */
public class MyClassLoaderTest {
    @Test
    public void testClassLoader() throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("src/test/resources/bean/Hello.class");
        Class clazz = myClassLoader.loadClass("com.chenerzhu.learning.classloader.bean.Hello");
        Object obj = clazz.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}

    首先经过构造方法建立MyClassLoader对象myClassLoader,指定加载src/test/resources/bean/Hello.class路径的Hello.class(固然这里只是个例子,直接指定一个class的路径了)。而后经过myClassLoader方法loadClass加载Hello的Class对象,最后实例化对象。如下是输出结果,看得出来实例化成功了,而且类加载器使用的是MyClassLoader。

com.chenerzhu.learning.classloader.bean.Hello@2b2948e2
com.chenerzhu.learning.classloader.MyClassLoader@335eadca

 

4、类Class卸载

  JVM中class和Meta信息存放在PermGen space区域(JDK1.8以后存放在MateSpace中)。若是加载的class文件不少,那么可能致使元数据空间溢出。引发java.lang.OutOfMemory异常。对于有些Class咱们可能只须要使用一次,就再也不须要了,也可能咱们修改了class文件,咱们须要从新加载 newclass,那么oldclass就再也不须要了。因此须要在JVM中卸载(unload)类Class。
  JVM中的Class只有知足如下三个条件,才能被GC回收,也就是该Class被卸载(unload):

  1. 该类全部的实例都已经被GC。
  2. 该类的java.lang.Class对象没有在任何地方被引用。
  3. 加载该类的ClassLoader实例已经被GC。

  很容易理解,就是要被卸载的类的ClassLoader实例已经被GC而且自己不存在任何相关的引用就能够被卸载了,也就是JVM清除了类在方法区内的二进制数据。
  JVM自带的类加载器所加载的类,在虚拟机的生命周期中,会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象。所以这些Class对象始终是可触及的,不会被卸载。而用户自定义的类加载器加载的类是能够被卸载的。虽然知足以上三个条件Class能够被卸载,可是GC的时机咱们是不可控的,那么一样的咱们对于Class的卸载也是不可控的。

 

5、JVM自定义类加载器加载指定classPath下的全部class及jar

  通过以上几个点的说明,如今能够实现JVM自定义类加载器加载指定classPath下的全部class及jar了。这里没有限制class和jar的位置,只要是classPath路径下的都会被加载进JVM,而一些web应用服务器加载是有限定的,好比tomcat加载的是每一个应用classPath+“/classes”加载class,classPath+“/lib”加载jar。如下就是代码啦...

  

package com.chenerzhu.learning.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author chenerzhu
 * @create 2018-10-04 12:24
 **/
public class ClassPathClassLoader extends ClassLoader{

    private static Map<String, byte[]> classMap = new ConcurrentHashMap<>();
    private String classPath;

    public ClassPathClassLoader() {
    }

    public ClassPathClassLoader(String classPath) {
        if (classPath.endsWith(File.separator)) {
            this.classPath = classPath;
        } else {
            this.classPath = classPath + File.separator;
        }
        preReadClassFile();
        preReadJarFile();
    }

    public static boolean addClass(String className, byte[] byteCode) {
        if (!classMap.containsKey(className)) {
            classMap.put(className, byteCode);
            return true;
        }
        return false;
    }

    /**
     * 这里仅仅卸载了myclassLoader的classMap中的class,虚拟机中的
     * Class的卸载是不可控的
     * 自定义类的卸载须要MyClassLoader不存在引用等条件
     * @param className
     * @return
     */
    public static boolean unloadClass(String className) {
        if (classMap.containsKey(className)) {
            classMap.remove(className);
            return true;
        }
        return false;
    }

    /**
     * 遵照双亲委托规则
     */
    @Override
    protected Class<?> findClass(String name) {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String className) {
        if (classMap.containsKey(className)) {
            return classMap.get(className);
        } else {
            return null;
        }
    }

    private void preReadClassFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanClassFile(file);
            }
        }
    }

    private void scanClassFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".class")) {
                try {
                    byte[] byteCode = Files.readAllBytes(Paths.get(file.getAbsolutePath()));
                    String className = file.getAbsolutePath().replace(classPath, "")
                            .replace(File.separator, ".")
                            .replace(".class", "");
                    addClass(className, byteCode);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanClassFile(f);
                }
            }
        }
    }

    private void preReadJarFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanJarFile(file);
            }
        }
    }

    private void readJAR(JarFile jar) throws IOException {
        Enumeration<JarEntry> en = jar.entries();
        while (en.hasMoreElements()) {
            JarEntry je = en.nextElement();
            je.getName();
            String name = je.getName();
            if (name.endsWith(".class")) {
                //String className = name.replace(File.separator, ".").replace(".class", "");
                String className = name.replace("\\", ".")
                        .replace("/", ".")
                        .replace(".class", "");
                InputStream input = null;
                ByteArrayOutputStream baos = null;
                try {
                    input = jar.getInputStream(je);
                    baos = new ByteArrayOutputStream();
                    int bufferSize = 1024;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    addClass(className, baos.toByteArray());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (baos != null) {
                        baos.close();
                    }
                    if (input != null) {
                        input.close();
                    }
                }
            }
        }
    }

    private void scanJarFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".jar")) {
                try {
                    readJAR(new JarFile(file));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanJarFile(f);
                }
            }
        }
    }


    public void addJar(String jarPath) throws IOException {
        File file = new File(jarPath);
        if (file.exists()) {
            JarFile jar = new JarFile(file);
            readJAR(jar);
        }
    }
}

  

  如何使用的代码就不贴了,和3.2节自定义类加载器的使用方式同样。只是构造方法的参数变成classPath了,篇末有代码。当建立MyClassLoader对象时,会自动添加指定classPath下面的全部class和jar里面的class到classMap中,classMap维护className和classCode字节码的关系,只是个缓冲做用,避免每次都从文件中读取。自定义类加载器每次loadClass都会首先在JVM中找是否已经加载className的类,若是不存在就会到classMap中取,若是取不到就是加载错误了。

 

6、最后

  至此,JVM自定义类加载器加载指定classPath下的全部class及jar已经完成了。这篇博文花了两天才写完,在写的过程当中有意识地去了解了许多代码的细节,收获也不少。原本最近仅仅是想实现Quartz控制台页面任务添加支持动态class,结果不知不觉跑到类加载器的坑了,在此也趁这个机会总结一遍。固然以上内容并不能保证正确,因此但愿你们看到错误可以指出,帮助我更正已有的认知,共同进步。。。

 

本文的代码已经上传github:https://github.com/chenerzhu/learning/tree/master/classloader  欢迎下载和指正。

SpringBoot实现可视化动态操做Quartz定时任务:https://github.com/chenerzhu/quartz-console

参考文章:

  深度分析Java的ClassLoader机制(源码级别)

  自定义类加载器-从.class和.jar中读取

  Class热替换与卸载

相关文章
相关标签/搜索