目录
- 一、类加载器(ClassLoader)
- 二、四种类加载器之间的关系
- 三、双亲委派机制
- 3.1、为什么要设计双亲委派机制
- 3.2、类加载器loadClass(String name) 源码解析
- 3.3、打破双亲委派机制
一、类加载器(ClassLoader)
JVM的类加载机制是按需加载的模式运行的,也就是代表着:所有类并不会在程序启动时全部加载,而是当需要用到某个类发现它未加载时,才会去触发加载的过程。
-
引导/启动类加载器: Bootstrap ClassLoader,负责加载存放在jdk\jre\lib(jdk代表jdk的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类,比如java.lang.Integer均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
注意:因为JVM是通过全限定名加载类库的,所以,如果你的文件名不被虚拟机识别,就算你把jar包丢入到lib目录下,引导类加载器也并不会加载它。出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类文件。
-
扩展类加载器: Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
这个类加载器是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$ExtClassLoader位置。它主要负责加载<JAVA_HOME>\lib\ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。它可以直接被开发者使用。
-
系统/应用程序类加载器: Application ClassLoader,也被称为应用程序类加载器,也是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$AppClassLoader位置。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是经常用到的classpath路径。应用程序类加载器也可以直接被开发者使用。
一般情况下,该类加载器是程序的默认类加载器,我们可以通过ClassLoader.getSystemClassLoader()方法可以直接获取到它。
-
自定义类加载器: Application ClassLoader,也被称为应用程序类加载器,也是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$AppClassLoader位置。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是经常用到的classpath路径。应用程序类加载器也可以直接被开发者使用。
二、四种类加载器之间的关系
如上分析的类加载器关系链如下:
Bootstrap引导类加载器 → Extension拓展类加载器 → Application系统类加载器 → User自定义类加载器
Bootstrap类加载器是在JVM启动时初始化的,它会负责加载ExtClassLoader,并将其父加载器设置为BootstrapClassLoader。BootstrapClassLoader加载完ExtClassLoader后会接着加载AppClassLoader系统类加载器,并将其父加载器设置为ExtClassLoader拓展类加载器。而自己定义的类加载器会由系统类加载器加载,加载完成后,AppClassLoader会成为它们的父加载器。
要注意的是:类加载器之间并不存在相互继承或包含关系,从上至下仅存在父加载器的层级引用关系。
例:
// 继承ClassLoader类,JDKClassLoaderDemo 相当于一个自定义类加载器
public class JDKClassLoaderDemo extends ClassLoader{
public static void main(String[] args) {
JDKClassLoaderDemo classLoader = new JDKClassLoaderDemo();
System.out.println("自定义加载器:" + classLoader);
System.out.println("自定义加载器的父类加载器:" + classLoader.getParent());
System.out.println("Java程序系统默认的加载器:" + ClassLoader.getSystemClassLoader());
System.out.println("系统类加载器的父加载器:" + ClassLoader.getSystemClassLoader().getParent());
System.out.println("拓展类加载器的父加载器:" + ClassLoader.getSystemClassLoader().getParent().getParent());
System.out.println();
System.out.println("bootstrapLoader加载以下文件:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i]);
}
System.out.println();
System.out.println("extClassloader加载以下文件:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println();
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
// 输出结果
/*
自定义加载器:com.kerwin.jvm.classloader.JDKClassLoaderDemo@74a14482
自定义加载器的父类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
Java程序系统默认的加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
系统类加载器的父加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
拓展类加载器的父加载器:null
bootstrapLoader加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/classes
extClassloader加载以下文件:
C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
appClassLoader加载以下文件:
项目目录\target\classes;
...
*/
三、双亲委派机制
类加载器在加载 class 文件的时候,遵从双亲委派原则,意思是加载依次由父加载器先执行加载动作,只有当父加载器没有加载到 class 文件时才由子类加载器进行加载。这种机制很好的保证了 Java API 的安全性,使得 JDK 的代码不会被篡改。
3.1、为什么要设计双亲委派机制
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
自定义String类运行测试:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("**************My String Class**************");
}
}
// 运行结果:
/*
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
*/
3.2、类加载器loadClass(String name) 源码解析
//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 检查当前类加载器是否已经加载了该类,如果没有加载则委托父加载器加载
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); //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
// 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;
}
}
3.3、打破双亲委派机制
尽管双亲委派机制有着诸多好处,但有时我们也需要打破它,比如在某些场景下需要加载自定义的类库,或者需要实现热部署等功能。这时,我们就需要自定义类加载器,并在其中打破双亲委派机制。
以Java代码为例,我们可以通过继承ClassLoader类并重写loadClass方法来打破双亲委派机制。在loadClass方法中,我们可以根据需要自行加载类文件,并通过defineClass方法将其转换为Class对象,但是有一点需要注意,包名不能以java.
开头(如:java.lang.String),在java.lang.ClassLoader.preDefineClass
方法中会判断如果包名以java.
开头会抛出java.lang.SecurityException: Prohibited package name: java
。
注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。
- 自定义一个User
package com.kerwin.jvm.classloader;
public class User {
public void printClassLoad(){
System.out.println("当前User类的类加载器为:"+this.getClass().getClassLoader());
}
}
- 打破双亲委派机制实现
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t = System.nanoTime();
// 这里规定只有指定目录的类不走双亲委派加载,非自定义的类还是走双亲委派加载
if (!name.startsWith("com.kerwin.jvm.classloader")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:\\classloader-example\\target\\classes");
Class clazz = classLoader.loadClass("com.kerwin.jvm.classloader.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("printClassLoad", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader());
System.out.println();
MyClassLoader classLoader1 = new MyClassLoader("D:\\classloader-example\\target\\classes");
Class clazz1 = classLoader1.loadClass("com.kerwin.jvm.classloader.User");
Object obj1 = clazz1.newInstance();
Method method1 = clazz1.getDeclaredMethod("printClassLoad", null);
method1.invoke(obj1, null);
System.out.println(clazz1.getClassLoader());
}
}
// 输出结果
/*
当前User类的类加载器为:com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@7a7b0070
com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@7a7b0070
当前User类的类加载器为:com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@6ed3ef1
com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@6ed3ef1
*/
可以看到两个User所属类加载器内存地址不同,代表是两个不同的类加载器,那么这个User类信息也在JVM中存在2份,两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个,如果存在双亲委派那么类加载器应该是同一个,类加载器的内存地址应该也是相同的,这里不同则代表已经打破双亲委派。