从Java虚拟机的角度讲,只有两种不同的类加载器:启动类加载器和其他类加载器。

1.启动类加载器(Boostrap ClassLoader):这个是由c++实现的,紧张卖力JAVA_HOME/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入事情。

2.其他类加载器:由java实现,可以在方法区找到其Class工具。
这里又细分为几个加载器

jsponbeforeunloadJVM自界说类加载器加载指定classPath下的所有class及jar RESTful API

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动态实时类装入特性。

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

二、为什么要自定义类加载器

区分同名的类:假定在tomcat 运用做事器,上面支配着许多独立的运用,同时他们拥有许多同名却不同版本的类。
要区分不同版本的类当然是须要每个运用都拥有自己独立的类加载器了,否则无法区分利用的详细是哪一个。
类库共享:每个web运用在tomcat中都可以利用自己版本的jar。
但存在如Servlet-api.jar,java原生的包和自定义添加的Java类库可以相互共享。
加强类:类加载器可以在 loadClass 时对 class 进行重写和覆盖,在此期间就可以对类进行功能性的增强。
比如利用javassist对class进行功能添加和修正,或者添加面向切面编程时用到的动态代理,以及 debug 等事理。
热更换:在运用正在运行的时候升级软件,不须要重新启动运用。
比如toccat做事器中JSP更新更换。

三、自定义类加载器

3.1 ClassLoader实现自定义类加载器干系方法解释

要实现自定义类加载器须要先继续ClassLoader,ClassLoader类是一个抽象类,卖力加载classes的工具。
自定义ClassLoader中至少须要理解个中的三个的方法: loadClass,findClass,defineClass。

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实现了类加载的功能了。

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

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

源码解释...

/ 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描述进行链接。

3.2 自定义类加载器实现

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

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。

在此我向大家推举一个Java学习互换群。
互换学习群号:874811168 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码剖析,高并发、高性能、分布式、微做事架构的事理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。
还能领取免费的学习资源,一起学习,一起进步,目前受益良多。

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

四、类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):

该类所有的实例都已经被GC。
该类的java.lang.Class工具没有在任何地方被引用。
加载该类的ClassLoader实例已经被GC。

很随意马虎理解,便是要被卸载的类的ClassLoader实例已经被GC并且本身不存在任何干系的引用就可以被卸载了,也便是JVM打消了类在方法区内的二进制数据。

JVM自带的类加载器所加载的类,在虚拟机的生命周期中,会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class工具。
因此这些Class工具始终是可触及的,不会被卸载。
而用户自定义的类加载器加载的类是可以被卸载的。
虽然知足以上三个条件Class可以被卸载,但是GC的机遇我们是不可控的,那么同样的我们对付Class的卸载也是不可控的。

五、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(); 在此我向大家推举一个Java学习互换群。
互换学习群号:874811168 可免费领取java资料。
} } 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中取,如果取不到便是加载缺点了。

六、末了

至此,JVM自定义类加载器加载指定classPath下的所有class及jar已经完成了。
这篇博文花了两天才写完,在写的过程中故意识地去理解了许多代码的细节,收成大概多。
本来最近仅仅是想实现Quartz掌握台页面任务添加支持动态class,结果不知不觉跑到类加载器的坑了,在此也趁这个机会总结一遍。
当然以上内容并不能担保精确,以是希望大家看到缺点能够指出,帮助我更正已有的认知,共同进步。

出处:https://www.cnblogs.com/chenerzhu/p/9741883.html