1.类加载器概述
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(ClassLoader)
以 JDK 8 为例:
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader (扩展类加载器) | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader(应用程序类加载器) | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
每个类加载器各司其职。
类加载器都有层级关系,比如应用程序类加载器它去加载类的时候,它首先会问一问,类是否由上一级加载过了,它首先会问扩展类加载器,有没有加载过这个类,如果没有,它会问扩展类加载器的上一级启动类加载器有没有加载过这个类,如果它们都没有加载,才能轮到应用程序类加载器加载这个类。
比如我们想加载String,字符串类通过应用程序加载器调用它的loadClass方法加载String,然后它就问它的上级还有上上级,而我们的启动类加载器已经加载过了,因为String类属于核心类目录lib下的类,所以它已经由启动类加载器加载过了。这样扩展类和应用程序加载器都无需关心。而我们的自定义Student类启动类和扩展类都没加载过,应用程序自己来进行加载。这种方式我们称之为双亲委派的加载模式
bootstrap类加载器是C++代码写的,所以java代码无法直接访问。
2.启动类加载器
加载的类是JAVA_HOME/jre/lib目录下的这些类,我们也可以通过虚拟机提供的一些参数把我们自己比编写的类交给启动类加载器来进行加载。
可通过在控制台输入指令,使得类被启动类加器加载
用bootstrap类加载加载类:
public class F {
static {
System.out.println("bootstrap F init");
}
}
执行
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader()); // AppClassLoader ExtClassLoader
}
}
通过class.forName完成类的加载,也可以顺便做类的链接以及初始化操作。最终我们可以通过类名.getClassLoader()方法获得类的类加载器信息。
输出
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null
-Xbootclasspath 表示设置 bootclasspath
其中 /a:. 表示将当前目录追加至 bootclasspath 之后
可以用这个办法替换核心类
- java -Xbootclasspath:
- java -Xbootclasspath/a:<追加路径>
- java -Xbootclasspath/p:<追加路径>
如果类加载器是应用程序加载器,会打印Appclassloader,如果是扩展类加载器,会打印ExtensionClassLoader。如果是启动类加载器,会打印bootstrapLoader.
3.扩展类加载器
如果classpath和JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用扩展类加载器加载。当应用程序类加载器发现扩展类加载器已将该同名类加载过了,则不会再次加载
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("classpath G init");
}
}
执行
/**
* 演示 扩展类加载器
* 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
* 里面也有一个 G 的类,观察到底是哪个类被加载了
*/
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
输出
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2
因为F是在classpath下,所以打印的肯定是应用程序类加载器。结果中确实用应用程序类加载器将其加载并初始化了。因为它不可能在启动类和扩展类的路径下找到这类。
如果我们在扩展类的类路径下和应用程序的类路径下都放一个同名类。此时的结果如何。
写一个同名的类
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("ext G init");
}
}
打个 jar 包
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)
扩展类加载器下的类必须是以jar包的方式存在的。打jar包的命令用jar -cvf 路径
将 jar 包拷贝到 JAVA_HOME/jre/lib/ext
重新执行 Load5_2
输出
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44
可以看到加载了扩展类加载器,如果解释上述现象。
双亲委派模式:当我们应用程序类加载器想去加载这个类的时候,需要先问一下上级,如果上级扩展类加载器已经找到了一个同名的类文件,扩展类加载器就把这个类给加载了,应用程序类加载器就没机会再加载了。
4.双亲委派模式
双亲委派模式,即调用类加载器ClassLoader 的 loadClass 方法时,查找类的规则
即委派双亲优先做类的加载,上级没有,再由本级的类加载器完成加载。
注意:这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系。
loadClass源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1.检查该类是否已经加载(首先查找该类是否已经被该类加载器加载过了)
Class<?> c = findLoadedClass(name);
//如果没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//看是否被它的上级加载器加载过了 Extension的上级是Bootstarp,但它显示为null
if (parent != null) {
//2.有上级的话,委派上级loadClass
c = parent.loadClass(name, false);
} else {
//3.如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//捕获异常,但不做任何处理
}
if (c == null) {
//如果还是没有找到,先让拓展类加载器调用findClass方法去找到该类,如果还是没找到,就抛出异常
//然后让应用类加载器去找classpath下找该类
long t1 = System.nanoTime();
//4.每一层找不到,调用findClass方法(每个类加载器自己扩展)来加载,比如扩展类加载器,就到ext路径下去找这个类,如应用程序类加载器可以到classpath中找,对于每一类类加载器,可以自定义这样的规则。
c = findClass(name);
// 5.记录时间
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
例如:
package cn.itcast.jvm.t3.load;
import java.util.ServiceLoader;
public class Load5_3 {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(Load5_3.class.getClassLoader());
Class<?> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
-
sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
-
sun.misc.Launcher $ AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()
-
sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { -
每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载 c = findClass(name);
-
BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
-
sun.misc.Launcher E x t C l a s s L o a d e r / / 4 处 , 调 用 自 己 的 fi n d C l a s s 方 法 , 是 在 J A V A H O M E / j r e / l i b / e x t 下 找 H 这 个 类 , 显 然 没 有 , 回 到 s u n . m i s c . L a u n c h e r ExtClassLoader // 4 处,调用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher ExtClassLoader//4处,调用自己的findClass方法,是在JAVAHOME/jre/lib/ext下找H这个类,显然没有,回到sun.misc.LauncherAppClassLoader 的 // 2 处
-
继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了
5.线程上下文类加载器
我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?
让我们追踪一下源码:
public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
先不看别的,看看 DriverManager 的类加载器:
System.out.println(DriverManager.class.getClassLoader());
打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
继续看 loadInitialDrivers() 方法:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1) 使用ServiceLoader机制加载驱动,即SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2) 使用jdbc.drivers定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
//这里的ClassLoader.getSystemClassLoader()就是应用程序类加载器
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
它里面打破了双亲委派的规则,理应用启动类加载器来完成所有与之关联的类的加载,但是这里违反了这样的规定,而是使用了应用程序类加载器来下载我们的驱动类。
先看 2)发现它后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此 可以顺利完成类加载
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称
这样就可以使用
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next(); }
来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:
- JDBC
- Servlet 初始化器
- Spring 容器
- Dubbo(对 SPI 进行了扩展)
接着看 ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
6.自定义类加载器
问问自己,什么时候需要自定义类加载器
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器(即一个类可能有多个不同的版本,我们希望新旧版本同时工作,虽然这些类的包名类名都是同样的,但是它里面的字节码有新旧之分,还是希望它同时工作)
步骤
继承ClassLoader父类
要遵从双亲委派机制,重写 findClass 方法
- 不是重写loadClass方法,否则不会走双亲委派机制
读取类文件的字节码(一般是byte数组)
调用父类的 defineClass 方法来加载类
使用者调用该类加载器的 loadClass 方法
package cn.itcast.jvm.t3.load;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Load7 {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> c1 = classLoader.loadClass("MapImpl1");
Class<?> c2 = classLoader.loadClass("MapImpl1");
System.out.println(c1 == c2);
MyClassLoader classLoader2 = new MyClassLoader();
Class<?> c3 = classLoader2.loadClass("MapImpl1");
System.out.println(c1 == c3);
c1.newInstance();
}
}
class MyClassLoader extends ClassLoader {
@Override // name 就是类名称
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "e:\\myclasspath\\" + name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}
破坏双亲委派模式
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK1.2面世以前的“远古”时代
- 建议用户重写findClass()方法,在类加载器中的loadClass()方法中也会调用该方法
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的
- 如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的
- 这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等