一:ClassLoader
从JVM结构图中可以看到,类加载器的作用是将Java类文件加载到Java虚拟机。
HotSpot JVM结构,图片来自Java Garbage Collection Basics
只有当类被加载进虚拟机内存,才能使用对应的类。
在Java中,类加载过程大概分为以下几步:
通过全限类名获取类文件字节数组。可来自本地文件、jar包、网络等。
在方法区/元空间保存类的描述信息、静态属性。
在JVM堆中生成一个对应的java.lang.Class对象。
理解Java的类加载机制,对理解JVM有很大帮助。
二:Java默认的类加载器
Java默认提供三个类加载器,分别为:
Bootstrap ClassLoader
Extension ClassLoader
App ClassLoader
Bootstrap ClassLoader 负责加载Java基础类,主要是 %JRE_HOME%/lib/ 目录下的rt.jar、resources.jar、charsets.jar等。
Extension ClassLoader 负责加载Java扩展类,主要是 %JRE_HOME%/lib/ext 目录下的jar。
App ClassLoader 负责加载当前应用的ClassPath中的所有类。
三个ClassLoader所负责加载的类,可以通过以下方式进行查看。
public class ClassPath { public static void main(String[] args) { System.out.println("Bootstrap ClassLoader path: "); System.out.println(System.getProperty("sun.boot.class.path")); System.out.println("----------------------------"); System.out.println("Extension ClassLoader path: "); System.out.println(System.getProperty("java.ext.dirs")); System.out.println("----------------------------"); System.out.println("App ClassLoader path: "); System.out.println(System.getProperty("java.class.path")); System.out.println("----------------------------"); } }
具体原因,在源码分析章节说明。
其中Bootstrap ClassLoader是JVM级别的,由C++撰写。
Extension ClassLoader和App ClassLoader都是Java类。
JVM启动Bootstrap ClassLoader,然后初始化sun.misc.Launcher。
接着,Launcher初始化Extension ClassLoader和App ClassLoader。
三:源码分析
sun.misc.Launcher类是Java程序的入口。
其构造器如下:
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); } Thread.currentThread().setContextClassLoader(this.loader); …… }
其中有两行比较重要的代码:
Launcher.ExtClassLoader.getExtClassLoader();this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
第一行初始化了ExtClassLoader,但没有指定其parent。
一些文章表示ExtClassLoader的父加载器是Bootstrap ClassLoader,这个说法其实并不完全准确。
第二行初始化了AppClassLoader,指定ExtClassLoader作为其父加载器。并将AppClassLoader作为系统类加载器。
AppClassLoader将会成为自定义ClassLoader的默认父加载器。
具体逻辑可按照以下顺序查看源代码:
Launcher类的getClassLoader()方法。
ClassLoader类的initSystemClassLoader()方法。
ClassLoader类的getSystemClassLoader()方法。
ClassLoader类的ClassLoader()方法。
其中getSystemClassLoader()方法的注释为:
/**
* Returns the system class loader for delegation. This is the default
* delegation parent for new <tt>ClassLoader</tt> instances, and is
* typically the class loader used to start the application.
**/
ExtClassLoader和AppClassLoader都继承了URLClassLoader类。
URLClassLoader支持从文件目录和jar包加载class。
ExtClassLoader和AppClassLoader都调用了父类的构造函数。
public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)
URLClassLoader类中有个属性为ucp,表示该ClassLoader负责搜索的路径。
ExtClassLoader和AppClassLoader最大的不同,即它们负责的路径不同。
/* The search path for classes and resources */private final URLClassPath ucp;
查看源码可得:
ExtClassLoader负责搜索的路径为:
String var0 = System.getProperty("java.ext.dirs");
AppClassLoader负责搜索的路径为:
String var1 = System.getProperty("java.class.path");
所以,上一节可以通过这两个方法获取不同ClassLoader所负责加载的目录。
此外,Bootstrap ClassLoader负责搜索的路径为:
String bootClassPath = System.getProperty("sun.boot.class.path");
ClassLoader源码
ClassLoader是一个抽象类,几个主要的方法如下:
defineClass(String name, byte[] b, int off, int len)把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例。
findClass(String name)查找名称为name的类,返回的结果是java.lang.Class类的实例。
loadClass(String name)加载名称为name的类,返回的结果是java.lang.Class类的实例。
resolveClass(Class<?> c)链接指定的Java 类。
其中,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; } }
该方法主要的步骤如下:
指定全限类名进行加载,首先调用findLoadedClass(name)判断当前类加载器是否已经加载该类。
如果没有被加载。则判断当前ClassLoader的父加载器是否为null。如果不为null,则委托其父加载器进行加载。如果为null,则使用Bootstrap ClassLoader进行加载。
如果父加载器或Bootstrap ClassLoader都无法加载,则调用findClass(name)方法寻找需要加载的类。
此外,loadClass方法还涉及加锁的过程,使用ConcurrentHashMap对不同的全限类名进行加锁。
具体可查看getClassLoadingLock方法。
四:双亲委托模式
Java类加载机制使用双亲委托模式。
一个ClassLoader加载一个类时,首先需要将任务委托给其父加载器,直到Bootstrap ClassLoader。
如果父加载器未加载该类,则逐层返回给委托发起者即当前ClassLoader进行加载。
在正常应用中,用户不自定义类加载器。
类加载工作首先由App ClassLoader发起,然后委托给Extension ClassLoader,最后委托给Bootstrap ClassLoader。
首先,通过一个例子了解三个ClassLoader所负责加载的类和双亲委托模式。
新建一个jar包,名为acai-cl.jar,包中有个简单的Person类。
写一个简单的程序输出person对象所对应的ClassLoader。
import com.acai.Person;public class TestClassLoader { public static void main(String[] args) { Person person = new Person(); System.out.println(person.getClass().getClassLoader()); } }
测试一:将jar包引入项目
jar包引入项目
对应输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
可以看到,位于ClassPath的类,是由App ClassLoader负责加载。
测试二:将jar包复制到%JRE_HOME%/lib/ext目录
复制到%JRE_HOME%/lib/ext
对应输出:
sun.misc.Launcher$ExtClassLoader@4cc77c2e
可以得出,Extension ClassLoader负责加载%JRE_HOME%/lib/ext目录下的类。
加载Person类时,会首先尝试使用App ClassLoader进行加载。
由于双亲委托模式,最终委托到Extension ClassLoader,而其负责的目录%JRE_HOME%/lib/ext下存在Person类,则进行了类加载操作。
测试三:将jar包追加到Bootstrap ClassLoader加载路径上
追加到Bootstrap ClassLoader加载路径
使用参数:-Xbootclasspath/a:d:\acai-cl.jar,将jar包追加到Bootstrap ClassLoader加载路径。
对应输出:
null
可以看出,Person类的加载工作,最终被委托到了Bootstrap ClassLoader。
注:Bootstrap ClassLoader由C++撰写。由Bootstrap ClassLoader负责加载的类,其getClassLoader()方法输出为null。
可以尝试输出String类的类加载器。
System.out.println(String.class.getClassLoader());
接下来,再通过debug来验证双亲委托模式。
还是原来那个简单的demo。
import com.acai.Person;public class Test { public static void main(String[] args) { Person person = new Person(); System.out.println(person.getClass().getClassLoader()); } }
在ClassLoader类的loadClass方法上打断点。
App ClassLoader尝试加载
Extension ClassLoader尝试加载
Bootstrap ClassLoader尝试加载
可以看出,类的加载过程符合从下到上委托,最终会被委托到Bootstrap ClassLoader。
同时符合从上到下加载,每一层ClassLoader都会尝试进行加载。最终由App ClassLoader加载了Person类。
接着,尝试加载一个特殊的类:Splash.class。
Splash类位于jfxrt.jar,这个jar包在%JRE_HOME%/lib/ext目录下。
import com.sun.javafx.applet.Splash;public class ExtTest { public static void main(String[] args) { Splash splash = new Splash(null); System.out.println(splash.getClass().getClassLoader()); } }
对应输出:
sun.misc.Launcher$ExtClassLoader@330bedb4
毫无疑问,Splash类应该由Extension ClassLoader进行加载。
但其加载过程,仍然会从默认的系统类加载器App ClassLoader开始。
可以通过debug进行查看。
App ClassLoader尝试加载
Splash类加载的过程会被委托到Bootstrap ClassLoader,但Bootstrap ClassLoader并不负责加载%JRE_HOME%/lib/ext目录下的类。最终由Extension ClassLoader进行加载。
Bootstrap ClassLoader尝试加载未成功
最终由Extension ClassLoader加载
很多文章在阐述三个ClassLoader之间的关系时候,会给出一个getParent操作的demo。
并且认为Bootstrap ClassLoader是Extension ClassLoader的父加载器。
Extension ClassLoader是App ClassLoader的父加载器。
App ClassLoader是自定义类加载器的父加载器。
这样的解释基本正确,但Bootstrap ClassLoader和Extension ClassLoader之间的关系需要额外解释。
双亲委托机制,图片来自参考7
由于Bootstrap ClassLoader并不是使用Java编写,故无法指定Extension ClassLoader的parent为Bootstrap ClassLoader。
这一层关系在ClassLoader的loadClass方法中做了弥补。
在加载类时,会判断当前ClassLoader的父加载器是否为null,为null则使用Bootstrap ClassLoader进行加载。
在Java提供的三个默认类加载器中,父加载器为null的只有Extension ClassLoader。
该过程可参考ClassLoader的loadClass方法。
为什么使用双亲委托模式?
网上很多例子是关于String类。假设自己写一个java.lang.String类,使用双亲委托模式可以防止这个问题。
但其实双亲委托模式可以被打破,而真正阻止自定义java.lang.String的是“安全机制”。
这里尝试自定义java.lang.String类,并使用自定义ClassLoader进行加载。
package java.lang;public class String { }
import java.io.IOException;import java.nio.file.Files;import java.nio.file.Paths;public class StringClassLoader extends ClassLoader { @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if ("java.lang.String".equals(name)) { return findClass(name); } else { return super.loadClass(name); } } @Override public Class<?> findClass(String s) throws ClassNotFoundException { try { byte[] classBytes = Files.readAllBytes(Paths.get("d:/String.class")); return defineClass(s, classBytes, 0, classBytes.length); } catch (IOException e) { throw new ClassNotFoundException(s); } } public static void main(String[] args) throws ClassNotFoundException { StringClassLoader stringClassLoader = new StringClassLoader(); Class clazz = stringClassLoader.loadClass("java.lang.String", false); System.out.println(clazz.getClassLoader()); } }
该自定义类加载器破坏了双亲委托机制,具体方式将在下个章节说明。
输出结果为:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
可以看到,在被findClass方法调用的defineClass中有这么一段:
if ((name != null) && name.startsWith("java.")) { throw new SecurityException ("Prohibited package name: " + name.substring(0, name.lastIndexOf('.'))); }
它会检查当前加载类的全限类名是否以java.开头,这也是一种安全机制。
如果按照网上的说法,java.lang.String被Bootstrap ClassLoader加载,demo中自定义的类加载器会被略过,不会输出异常。
所以说,双亲委托模式的作用只是防止类重复加载。
五:自定义ClassLoader
多数情况下,Java默认的三个类加载器已经可以满足需求。
自定义类加载器则可以实现额外的需求,例如:
从网络文件加载类。
从任意目录加载类。
对字节码文件做加密处理,由自定义类加载器做解密。
实现自定义类加载器的主要步骤为:
继承ClassLoader类。如果只是从目录或者jar包加载类,也可以选择继承URLClassLoader类。
重写findClass方法。
在重写的findClass方法中,无论用何种方法,获取类文件对应的字节数组,然后调用defineClass方法转换成类实例。
自定义类加载器真正好玩的是打破双亲委托机制,也是很多面试官会问到的问题。
上文提到类加载双亲委托模式实现位于ClassLoader的loadClass方法,想要破坏这个机制,则需要重写该方法。
打破双亲委托模式的确有一定的实用价值。
比如有两个class文件,或者两个jar包。
其中两个类的全限类名都一样,如果需要同时使用这两个类,则需要打破双亲委托模式。
有两个Person类,它们的全限类名均为com.acai.Person,唯一的区别是sayHello()方法输出的内容略有不同。
package com.acai;import lombok.Data;@Datapublic class Person { private String name; private Integer age; public void sayHello() { System.out.println("Hello, this is Person in acai-cl"); } }
package com.acai;import lombok.Data;@Datapublic class Person { private String name; private Integer age; public void sayHello() { System.out.println("Hello, this is Person in acai-cl2"); } }
将两个Person所在的项目打成jar包。
两个jar包
常规操作是,把两个jar包都引进项目。
写一个小小的demo。
import com.acai.Person;public class Main { public static void main(String[] args) throws Exception { Person person = new Person(); System.out.println(person.getClass().getClassLoader()); person.sayHello(); } }
对应输出为:
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl
可以看到,demo中默认使用了acai-cl.jar中的Person类。
如果想要使用acai-cl2.jar中的Person类,则想到新建一个ClassLoader。
需要从jar包加载类,则优先想到URLClassLoader。
import com.acai.Person;import java.io.File;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;public class Main { public static void main(String[] args) throws Exception { Person person = new Person(); System.out.println(person.getClass().getClassLoader()); person.sayHello(); URL url = new File("d:/acai-cl2.jar").toURI().toURL(); URLClassLoader loader = new URLClassLoader(new URL[]{url}); Thread.currentThread().setContextClassLoader(loader); Class<?> clazz = loader.loadClass("com.acai.Person"); System.out.println(clazz.getClassLoader()); Method method = clazz.getDeclaredMethod("sayHello"); method.invoke(clazz.newInstance()); } }
作者:阿菜的博客
链接:https://www.jianshu.com/p/d98324f5ad23
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章