作者某人Valar
如需转载请保留原文链接
本文涉及到的Java源码均为 Java8
版本
部分图片来自百度,如有侵权请联系删除
目录:
前言:我们刚刚接触Java时,在IDE(集成开发环境) 或者文本编辑器中所写的都是.java文件,在编译后会生成.class文件,又称字节码文件。
javac HelloWorld.java ---> HelloWorld.class 复制代码
对于.class文件来说,需要被加载到虚拟机中才能使用,这个加载的过程就成为 类加载 。如果想要知道类加载的方式,就需要知道 类加载器 和 双亲委托机制 的概念。也就是我们本篇所要介绍的内容。
Java中的类加载器可以分为两种:
而系统类加载器又有3个:
Bootstrap ClassLoader用来加载 JVM(Java虚拟机)
运行时所需要的系统类,其使用 c++
实现。
从以下路径来加载类:
%JAVA_HOME%/jre/lib
Java虚拟机的启动就是通过 Bootstrap ClassLoader创建一个初始类来完成的。 可以通过如下代码来得出Bootstrap ClassLoader所加载的目录:
public class ClassLoaderTest { public static void main(String[]args) { System.out.println(System.getProperty("sun.boot.class.path")); } } 复制代码
打印结果为:
C:/Program Files/Java/jdk1.8.0_102/jre/lib/resources.jar; C:/Program Files/Java/jdk1.8.0_102/jre/lib/rt.jar; C:/Program Files/Java/jdk1.8.0_102/jre/lib/sunrsasign.jar; C:/Program Files/Java/jdk1.8.0_102/jre/lib/jsse.jar; C:/Program Files/Java/jdk1.8.0_102/jre/lib/jce.jar; C:/Program Files/Java/jdk1.8.0_102/jre/lib/charsets.jar; C:/Program Files/Java/jdk1.8.0_102/jre/lib/jfr.jar; C:/Program Files/Java/jdk1.8.0_102/jre/classes 复制代码
可以发现几乎都是 $JAVA_HOME/jre/lib
目录中的jar包,包括rt.jar、resources.jar和charsets.jar等等。
Extensions ClassLoader(扩展类加载器)具体是由 ExtClassLoader
类实现的, ExtClassLoader
类位于 sun.misc.Launcher
类中,是其的一个静态内部类。对于 Launcher
类,可以先看成是Java虚拟机的一个入口。
ExtClassLoader
的部分代码如下:
Extensions ClassLoader负责将 JAVA_HOME/jre/lib/ext
或者由系统变量 -Djava.ext.dir
指定位置中的类库加载到内存中。
通过以下代码可以得到Extensions ClassLoader加载目录:
System.out.println(System.getProperty("java.ext.dirs")); 复制代码
打印结果为:
C:/Program Files/Java/jdk1.8.0_102/jre/lib/ext; C:/Windows/Sun/Java/lib/ext 复制代码
也称为SystemAppClass(系统类加载器),具体是由 AppClassLoader
类实现的, AppClassLoader
类也位于 sun.misc.Launcher
类中。
部分代码如下:
通过以下代码可以得到App ClassLoader加载目录:
System.out.println(System.getProperty("java.class.path")); 复制代码
打印结果为:
C:/workspace/Demo/bin 复制代码
这个路径其实就是当前Java工程目录bin,里面存放的是编译生成的class文件。
在Java中,除了上述的3种系统提供的类加载器,还可以自定义一个类加载器。
为了可以从指定的目录下加载jar包或者class文件,我们可以用继承java.lang.ClassLoader类的方式来实现一个自己的类加载器。
在自定义类加载器时,我们一般复写 findClass
方法,并在 findClass
方法中调用 defineClass
方法。
接下来会先介绍下ClassLoader类相关的具体内容,之后看一个自定义类加载器demo。
从上面关于ExtClassLoader、AppClassLoader源码图中我们可以看到,他们都继承自URLClassLoader,那这个URLClassLoader是什么,其背后又有什么呢?
先来一张 很重要的继承关系 图:
关系:
还有2个结论:
我们准备一个简单的demo 自建的一个Test.java文件。
public class Test{} 复制代码
public class Main { public static void main(String[] args) { ClassLoader cl = Test.class.getClassLoader(); System.out.println("ClassLoader is:"+cl.toString()); } } 复制代码
这样就可以获取到Test.class文件的类加载器,然后打印出来。结果是:
sun.misc.Launcher$AppClassLoader@75b83e92 复制代码
也就是说明Test.class文件是由AppClassLoader加载的。
那AppClassLoader是谁加载的呢? 其实AppClassLoader也有一个父加载器,我们可以通过以下代码获取
public class Test { public static void main(String[] args) { ClassLoader loader = Test.class.getClassLoader(); while (loader != null) { System.out.println(loader); loader = loader.getParent(); } } } 复制代码
上述代码结果如下:
sun.misc.Launcher$AppClassLoader@7565783b sun.misc.Launcher$ExtClassLoader@1b586d23 复制代码
至于为何没有打印出ExtClassLoader的父加载器Bootstrap ClassLoader,这是因为Bootstrap ClassLoader是由C++编写的,并不是一个Java类,因此我们无法在Java代码中获取它的引用。
上一节我们看到了ClassLoader的 getParent
方法, getParent
获取到的其实就是其父加载器。这一节将通过源码,来介绍ClassLoader中的一些重要方法。
ClassLoader类 --------- public final ClassLoader getParent() { if (parent == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(parent, Reflection.getCallerClass()); } return parent; } 复制代码
我们可以看到,其返回值有两种可能,为 空
或者是 parent
变量。
从源码中还可以发现其是一个final修饰的方法,我们知道被final修饰的说明这个方法提供的功能已经满足当前要求,是不可以重写的, 所以其各个子类所调用的 getParent()
方法最终都会由ClassLoader来处理。
parent变量又是什么呢?我们在查看源码时可以发现parent的赋值是在构造方法中。
ClassLoader类 --------- private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; ... //省略了无关代码 } 复制代码
而此构造方法又是私有的,不能被外部调用,所以其调用者还是在内部。于是接着查找到了另外两个构造方法。
ClassLoader类 --------- protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); } 复制代码
所以:
getSystemClassLoader()
接着看上面代码中的getSystemClassLoader的源码:
ClassLoader类 --------- public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; } 复制代码
其返回的是一个scl。在 initSystemClassLoader()
方法中发现了对scl变量的赋值。
ClassLoader类 --------- private static synchronized void initSystemClassLoader() { if (!sclSet) { if (scl != null) throw new IllegalStateException("recursive invocation"); sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); //1 if (l != null) { Throwable oops = null; scl = l.getClassLoader(); ...//省略代码 } sclSet = true; } } 复制代码
重点来了,注释1处其获取到的是 Launcher
类的对象,然后调用了 Launcher
类的 getClassLoader()
方法。
Launcher类 --------- public ClassLoader getClassLoader() { return this.loader; } 复制代码
那这个this.loader是什么呢?在 Launcher
类中发现,其赋值操作在 Launcher
的构造方法中,其值正是 Launcher
类中的 AppClassLoader :
Launcher类 --------- 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); } ... } 复制代码
到这里谜团全部解开了:
在创建ClassLoder时,
getSystemClassLoader()
方法的返回值(也就是 Launcher
类中的 AppClassLoader )作为其parent。 能将class二进制内容转换成Class对象,如果不符合要求的会抛出异常,例如 ClassFormatError
、 NoClassDefFoundError
。
在自定义ClassLoader时,我们通常会先将特定的文件读取成byte[]对象,再使用此方法,将其转为class对象。
ClassLoader类 --------- /** * String name:表示预期的二进制文件名称,不知道的话,可以填null。 * byte[] b:此class文件的二进制数据 * int off:class二进制数据开始的位置 * int len:class二进制数据的总长度 */ protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null); } protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; } 复制代码
findClass()
方法一般被 loadClass()
方法调用去加载指定名称类。
ClassLoader类 --------- /** * String name:class文件的名称 */ protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } 复制代码
通过源码看到ClassLoader类中并没有具体的逻辑,而是等待着其子类去实现,通过上面的分析我们知道两个系统类加载器 ExtClassLoader
和 AppClassLoader
都继承自 URLClassLoader
,那就来看一下 URLClassLoader
中的具体代码。
URLClassLoader类 --------- protected Class<?> findClass(final String name) throws ClassNotFoundException { final Class<?> result; try { result = AccessController.doPrivileged( new PrivilegedExceptionAction<Class<?>>() { public Class<?> run() throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); Resource res = ucp.getResource(path, false); if (res != null) { try { return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException(name, e); } ... return result; } private Class<?> defineClass(String name, Resource res) throws IOException { ... URL url = res.getCodeSourceURL(); ... java.nio.ByteBuffer bb = res.getByteBuffer(); if (bb != null) { ... return defineClass(name, bb, cs); } else { byte[] b = res.getBytes(); ... return defineClass(name, b, 0, b.length, cs); } } 复制代码
可以看到其对传入的name进行处理后,就调用了 defineClass(name, res)
;在这个方法里主要是通过res资源和url,加载出相应格式的文件,最终还是通过ClassLoader的 defineClass
方法加载出具体的类。
上节说到 findClass()
一般是在 loadClass()
中调用,那 loadClass()
是什么呢? 其实 loadClass()
就是 双亲委托机制 的具体实现,所以在我们先介绍下双亲委托机制后,再来分析 loadClass()
。
先简单介绍下双亲委托机制: 类加载器查找Class(也就是在loadClass时)所采用的是双亲委托模式,所谓双亲委托模式就是
(图片来自http://liuwangshu.cn/application/classloader/1-java-classloader-.html)
Bootstrap ClassLoader
。 Bootstrap ClassLoader
可以从 %JAVA_HOME%/jre/lib
目录或者-Xbootclasspath指定目录查找到,就直接返回该对象,否则就让 ExtClassLoader
去查找。 ExtClassLoader
就会从 JAVA_HOME/jre/lib/ext
或者 -Djava.ext.dir
指定位置中查找,找不到时就交给 AppClassLoader
, AppClassLoader
就从当前工程的bin目录下查找 CustomClassLoader
查找,具体查找的结果,就要看我们怎么实现自定义ClassLoader的 findClass
方法了。 接下来我们看看双亲委托机制在源码中是如何体现的。 先看loadClass的源码:
ClassLoader类 --------- protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //首先,根据name检查类是否已经加载,若已加载,会直接返回 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //若当前类加载器有父加载器,则调用其父加载器的loadClass() c = parent.loadClass(name, false); } else { //若当前类加载器的parent为空,则调用findBootstrapClassOrNull() c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { // 1.如果到这里c依然为空的话,表示一直到最顶层的父加载器也没有找到已加载的c,那就会调用findClass进行查找 // 2.在findClass的过程中,如果指定目录下没有,就会抛出异常ClassNotFoundException // 3.抛出异常后,此层调用结束,接着其子加载器继续进行findClass操作 long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } 复制代码
findBootstrapClassOrNull()方法:可以看到其对name进行校验后,最终调用了一个 native
方法 findBootstrapClass()
。在 findBootstrapClass()
方法中最终会用Bootstrap Classloader来查找类。
ClassLoader类 --------- private Class<?> findBootstrapClassOrNull(String name) { if (!checkName(name)) return null; return findBootstrapClass(name); } private native Class<?> findBootstrapClass(String name); 复制代码
不会。 在Java中,我们用包名+类名作为一个类的标识。 但在JVM中,一个类用其 包名+类 名和 一个ClassLoader的实例 作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.
通过一个demo来看,
public class Main { public static void main(String[] args) { ClassLoaderTest myClassLoader = new ClassLoaderTest("F://"); ClassLoaderTest myClassLoader2 = new ClassLoaderTest("F://"); try { Class c = myClassLoader.loadClass("com.example.Hello"); Class c2 = myClassLoader.loadClass("com.example.Hello"); Class c3 = myClassLoader2.loadClass("com.example.Hello"); System.out.println(c.equals(c2)); //true System.out.println(c.equals(c3)); //flase } } 复制代码
输出结果:
true false 复制代码
只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类。
上面demo中用到的自定义ClassLoader:
自定义的类加载器 注意点: 1.覆写findClass方法 2.让其可以根据name从我们指定的path中加载文件,也就是将文件正确转为byte[]格式 3.使用defineClass方法将byte[]数据转为Class对象 ------------- public class ClassLoaderTest extends ClassLoader{ private String path; public ClassLoaderTest(String path) { this.path = path; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = null; byte[] classData = classToBytes(name); if (classData == null) { throw new ClassNotFoundException(); } else { clazz= defineClass(name, classData, 0, classData.length); } return clazz; } private byte[] classToBytes(String name) { String fileName = getFileName(name); File file = new File(path,fileName); InputStream in=null; ByteArrayOutputStream out=null; try { in = new FileInputStream(file); out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length=0; while ((length = in.read(buffer)) != -1) { out.write(buffer, 0, length); } return out.toByteArray(); } catch (IOException e) { e.printStackTrace(); }finally { try { if(in!=null) { in.close(); } } catch (IOException e) { e.printStackTrace(); } try{ if(out!=null) { out.close(); } }catch (IOException e){ e.printStackTrace(); } } return null; } private String getFileName(String name) { int index = name.lastIndexOf('.'); if(index == -1){ return name+".class"; }else{ return name.substring(index+1)+".class"; } } } 复制代码
到此Java的类加载器以及双亲委托机制都讲了个大概,如果文中有错误的地方、或者有其他关于类加载器比较重要的内容又没有介绍到的,欢迎在评论区里留言,一起交流学习。
下一篇会说道 Java new一个对象的过程 ,其中会涉及到类的 加载、验证 ,以及对象创建过程中的 堆内存分配 等内容。
参考: liuwangshu.cn/application…
blog.csdn.net/briblue/art…
blog.csdn.net/justloveyou…