类加载是什么
把磁盘中的 java 文件加载到内存中的过程叫做类加载
当咱们用 java 命令运行某个类的 main 函数启动程序时,首先须要通过类加载器把主类加载到 JVM. 有如下 User 类
package dc.dccmmtop;
public Class User {public static void main(String[] args) {System.out.println("hello");
}
}
运行 java dc.dccmmtop.User
时,先要找到 User.Class 文件,查找全门路就是 Class_PATH + {{package name}},对于 User 类来说,就是 {$Class_APTH}/dc/dccmmtop.User.Class
如果 User.java
在F:\code
, 并且不在 Class_PATH 下,能够通过 java -Classpath "F:\code"
长期指定。
加载类之后还有后续的步骤:
- 验证
- 筹备
- 解析
- 初始化
- 应用
- 卸载
这篇文章次要来讲讲类加载
类加载器
不理解类加载机制的,可能就认为,只需找到 java 文件所在的磁盘地位,而后进行一次读文件的操作不就实现了加载嘛,其实远非如此。
总有一个加载类的工具,这个工具叫做类加载器,在 java 代码中能够通过如下形式获取以后类的类加载器是什么
package dccmmtop;
public Class User {public static void main(String[] args) {System.out.println("hello");
System.out.println(User.Class.getClassLoader());
}
}
如图能够看到类加载器的名字叫做 AppClassLoader
咱们全局搜寻一下这个类,会发现在 sun.misc.Launcher.java
文件中找到。
那么这个AppClassLoader
自身也是一个 java 文件,它又是什么时候被加载并初始化的呢?
咱们滚动到文件顶部,看到 Launcher 类的构造方法局部:
标记 1 和标记 2 实现了一个单例模式,在 5 处获取到了 AppClassLoader
实例。也就是说在某一个中央通过调用 Launcher 类中的 getLauncher()
办法,会失去 AppClassLoader
实例,那么 getLauncher()
办法又是在哪里调用的呢?追踪到这里曾经无奈在 java 代码中找到上一步了,其实这个办法是 jvm (c++ 实现)调用的,如下图:
以上就是类加载的次要步骤了。上面看一下双亲委派机制
双亲委派机制
咱们持续看AppClassLoader
实例化的过程:
在 5 处,实例化了一个 AppClassLoader
的对象,同时传进去了一个参数 var1
, 这个 var1 是另外一个类加载器ExtClassLoader
, 咱们在进入 getAppClassLoader
办法看一看是怎么实现的:
先看一下 几个 ClassLoad 的继承关系:
有下面的继承关系图能够看进去,AppClassLoader
和 ExtClassLoader
都是从 ClassLoader
继承来的。
在 Launcher()
中可知,调用 AppClassLoader.getAppClassLoader()
办法时,把 ExtClassLoader
的实例作为参数传递进来,最终到 4 这一步,作为 var2 参数,调用父类的构造方法,持续追踪父类的构造方法直到 ClassLoader
:
在 ClassLoader
构造方法中,保护了一个 parent 变量,到此咱们晓得了 AppClassLoader
中 parent 变量保留的是 ExtClassLoader
的实例, 如下图示意
持续看 Launcher 构造方法:
loadClass()
办法将 Class 文件加载到 jvm 中,咱们跟踪一下这个办法,会发现最初会调到 根类ClassLoader
中:
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;
}
}
下面代码块中的弟 6 行,findLoadedClass()
, 先从已加载到的类汇合中查找有没有这个类,如果有的话,间接返回,没有再进行下一步, findLoadedClass
办法源码如下
到 native finnal Class<?> findLoadedClass0(String name);
这里曾经无奈在向后追踪了,看到 naive
, 要明确 应用 native 关键字阐明这个办法是原生函数,也就是这个办法是用 C /C++ 语言实现的,并且被编译成了 DLL,由 java 去调用.
此时 User.Class 是第一次加载,AppClassLoader
中必定无奈在已加载的汇合中找到,所以持续向下走到第 10,11 行. 下面曾经剖析过,AppClassLoader
中的 parent 是 ExtClassLoader
, 所以在 11 行由 ExtClassLoader
的实例执行 laodClass
办法。ExtClassLoader
没有覆写根类ClassLoader
的loaderClass
办法,所以也会到这里,只不过 ExtClassLoader
的 parent 是 NUll,会走到 13 行,调用findBootstrapClassOrNull()
办法,再看一下这个办法的实现:
会发现这个办法也是 C ++ 实现的,尽管咱们无奈看到源码,然而依据正文能够晓得,这个是保留了启动类加载器加载过的类。
到此为止,咱们曾经见识过 3 中不同的类加载器了:
- AppClassLoader
- ExtClassLoader
- BootStrapClassLoader
咱们先不论这个前面两个类加载器是什么,假设他们也找不到 User.Class. 持续向下看:
执行到第 21 行 findClas()
这里,再看源码
在 A -2 这一步,ucp
其实保留的就是以后 ClassLoader 的类加载门路,就不再开展。要记住此时的 ClassLoader 是 ExtClassLoader
, 如果依然找不到 User.Class 会执行到 A-3. 而后返回到 loadClass 办法中,此时 c 是空,继续执行到 33 行,返回到 AppClassLoader
调用 parent.getAppClassLoader
处,在 AppClassLoader
实例的范畴下持续向后执行,而后再持续调用 findClass
办法,如果在 AppClassLoader
的类加载门路中找到 User.Class 文件,就会 执行 defindClass(name,res)
办法去加载类文件了。
整个过程用文字描述起来比较复杂,来张图就很分明了,为什么叫做双亲委派:
把 loadedClassList 汇合称作缓存
- 先在 AppClassLoader 中缓存中找,如果找不到向 ExtClassLoader 找,如果能找到,间接返回
- 在 ExtClassLoader 中缓存找,如果找不到向 BootStrapClassLoader 找,如果能找到,间接返回
- 在 BootStrapClassLoader 找,如果找不到,在 ExtClassLoader 类门路汇合中找,
- 如果在 ExtClassLoader 类门路汇合找不到,在 AppClassLoader 类门路汇合找
- 如果在 AppClassLoader 类门路汇合中能找到,加载该类,并放入缓存。找不到则报错
双亲指的是 ExtClassLoader
和 BootStrapClassLoader
,AppClassLoader 先不加载,而是向上让其“父”加载,父加载不到时,本人再加载。这里的父不是父类,而是调用层级的关系。
是时候介绍一下 这三个类加载器
BootStrapClassLoader
疏导类加载器
负责加载撑持 JVM 运行的位于 JRE 的 lib 目录下的外围类库,比方 rt.jar、charsets.jar 等
ExtClassLoader
扩大类加载器
负责加载撑持 JVM 运行的位于 JRE 的 lib 目录下的 ext 扩大目录中的 JAR 类包
AppClassLoader
应用程序加载器
负责加载 ClassPath 门路下的类包,次要就是加载你本人写的那些类
咱们能够写代码验证一下:
package dccmmtop;
import sun.misc.Launcher;
import java.net.URL;
public Class User {public static void main(String[] args) {System.out.println(String.Class.getClassLoader()); // null
System.out.println(com.sun.crypto.provider.DESKeyFactory.Class.getClassLoader().getClass().getName()); //sun.misc.Launcher$ExtClassLoader
System.out.println(User.Class.getClassLoader().getClass().getName()); // sun.misc.Launcher$AppClassLoader
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"));
}
}
输出如下:
null // 因为调用了 c++ 实现。无奈获取到 java 对象
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
bootstrapLoader 加载以下文件:file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/Classes
extClassloader 加载以下文件:C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
appClassLoader 加载以下文件:C:\Program Files\Java\jdk1.8.0_261\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\jaccess.jar;... 省略
为什么应用双亲委派机制
- 沙箱平安机制:
本人写的 java.lang.String.Class 类不会被加载,这样便能够避免外围 API 库被随便篡改 - 防止类的反复加载:当父亲曾经加载了该类时,就没有必要子 ClassLoader 再加载一 次,保障被加载类的唯一性
全盘负责委托机制
“全盘负责”是指当一个 ClassLoder 装载一个类时,除非显示的应用另外一个 ClassLoder,该类
所依赖及援用的类也由这个 ClassLoder 载入
自定义类加载器
从上述源码的形容可知,类加载器的外围办法是 findClass , 和 defineClass。
defindClass 将 class 文件从磁盘加载文件到内存,defineClass 开始解析 class 文件:
所以自定义类加载器只需继承 ClassLoader,而后从写 findClass 文件就行了:
目录如下:
App.java:
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class App {
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);
// defineClass 将一个字节数组转为 Class 对象,这个字节数组是 class 文件读取后最终的字节 数组。return defieClass(name, data, 0, data.length);
} catch (Exception e) {e.printStackTrace();
throw new ClassNotFoundException();}
}
}
public static void main(String args[]) throws Exception {
// 初始化自定义类加载器,会先初始化父类 ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器 AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D 盘创立
// 创立 io/dc 几级目录,将 User 类的复制类 User.class 丢入该目录
Class clazz = classLoader.loadClass("io.dc.User");
Object obj = clazz.newInstance();
// 应用反射调用 User 类的 sout 办法
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
突破双亲委派机制
通过下面的源码剖析发现,次要是 ClassLoader
类中的laodClass
办法来实现的双亲委派机制,本人不加载而是先让其父加载。
所以间接复写 loadClass 办法即可,不再指定父级加载,以后类间接加载,如下:
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class App {
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);
// defineClass 将一个字节数组转为 Class 对象,这个字节数组是 class 文件读取后最终的字节 数组。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)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
if (name.startsWith("io.dc")) {
// 间接查找, 限定包名
c = findClass(name);
} else {
// 其余包中的类还是应用双亲委派机制
// 否则会报找不到 Object 类
c = this.getParent().loadClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {resolveClass(c);
}
return c;
}
}
}
public static void main(String args[]) throws Exception {
// 初始化自定义类加载器,会先初始化父类 ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器 AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D 盘创立
// 创立 io/dc 几级目录,将 User 类的复制类 User.class 丢入该目录
Class clazz = classLoader.loadClass("io.dc.User");
Object obj = clazz.newInstance();
// 应用反射调用 User 类的 sout 办法
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}