【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();
网络类加载器子类必须定义方法findClass
和loadClassData
,以便从网络加载类。一旦下载了组成类的字节,它就应该使用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
③ 类加载器执行顺序
执行顺序为:
- Bootstrap CLassloder
- Extention ClassLoader
- 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 {//...}
源码有精简,我们可以得到相关的信息。
- Launcher初始化了ExtClassLoader和AppClassLoader。
- 使用ExtClassLoader创建AppClassLoader。
- 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。