类运行全过程
在日常开发过程中,我们都会用到许许多多的类,跟我们开发息息相关,而了解他的加载原理那是重中之重;
前提知识点
前提知识点很重要请仔细阅读,带着知识点跟着下方案例走。
首先大家请看这个段代码
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() { //一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
这是一段非常简单的代码,new Maht
对象,调用compute()
方法。
我们来简单的了解下它的加载流程
是不是很难理解,不着急,往下看
类加载器
我们先来看看几种类加载器:
1、引导类加载器(BootstarpLoader):由C++实现的加载器,负责加载支撑JVM运行位于JRE的lib目录下核心Java类,例如:rt.jar、charsets.jar等;
2、扩展类加载器(ExtClassLoader):负责加载支撑JVM运行位于JRE的ext扩展目录中的Jar类包;
3、应用程序加载器(AppClassLoader):负责加载ClassPath路径下的类包,说白话就是加载你自己写的类;
4、自定义类加载器:负责加载用户自定义路径下的类包
Math
对象加载流程可离不开上述几种加载器。
每种加载器都负责加载不同包路径下的类,有各自的分工,我们先来看看类加载器的核心类Launcher
Launcher
中装载java类加载器ExtClassLoader
与AppClassLoader
,大致结构如下
public class Launcher {
static class AppClassLoader extends URLClassLoader {
}
static class ExtClassLoader extends URLClassLoader {
}
}
可以看出这两个类加载器存放在Launcher中的两个内部类,他们公共继承了
URLClassLoader
,URLClassLoader
最终继承了ClassLoader
而BootstarpLoader
,他是由C++所实现的类,由C++发起调用,我们在jdk源码中是看不到的,感兴趣的可以去看看HotSpot源码。
我们大致了解加载器的基本结构后,再来看看双亲委派机制
什么是双亲委派机制
双亲委派机制是指,加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在委托子类加载器寻找,直至找到该类;
而JDK类加载中就使用了双亲委派机制,比如我们的Math
类,最先会找应用程序类加载器加载(AppClassLoader),应用程序类加载器会先委托扩展类加载器(ExtClassLoader)加载,扩展类加载器再委托引导类加载器(BootstarpLoader),顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。。
我们来看看下面这个案例:
public class TestJDKClassLoader {
public static void main(String[] args) {
//获取String类的加载器
System.out.println(String.class.getClassLoader());
//获取扩展类加载器
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
//获取Target目录下类加载器,也就是自己写的类
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println();
//获取各个加载器的父加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("the bootstrapLoader : " + bootstrapLoader);
System.out.println("the extClassloader : " + extClassloader);
System.out.println("the appClassLoader : " + 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();
// appClassLoader 他会打印所有的 加载类路径 但是 会去读取不会去加载
System.out.println("appClassLoader加载以下文件:");
System.out.println(System.getProperty("java.class.path"));
}
}
// ------------------------输出内容如下----------------------------
//BootstrapLoader 是C++实现 所以输出为null
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@3764951d
the appClassLoader : sun.misc.Launcher$AppClassLoader@14dad5dc
bootstrapLoader加载以下文件:
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/resources.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/rt.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/sunrsasign.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jsse.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jce.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/charsets.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/lib/jfr.jar
file:/D:/dev/Java/jdk1.8.0_45/jre/classes
extClassloader加载以下文件:
D:\dev\Java\jdk1.8.0_45\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
appClassLoader加载以下文件:
D:\dev\Java\jdk1.8.0_45\jre\lib\charsets.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\deploy.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\access-bridge-64.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\cldrdata.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\dnsns.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\jaccess.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\jfxrt.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\localedata.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\nashorn.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunec.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunjce_provider.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunmscapi.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\sunpkcs11.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\ext\zipfs.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\javaws.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jce.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jfr.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jfxswt.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\jsse.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\management-agent.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\plugin.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\resources.jar;D:\dev\Java\jdk1.8.0_45\jre\lib\rt.jar;D:\ideaProjects\project-all\target\classes;C:\Users\zhuge\.m2\repository\org\apache\zookeeper\zookeeper\3.4.12\zookeeper-3.4.12.jar;C:\Users\zhuge\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;C:\Users\zhuge\.m2\repository\org\slf4j\slf4j-log4j12\1.7.25\slf4j-log4j12-1.7.25.jar;C:\Users\zhuge\.m2\repository\log4j\log4j\1.2.17\log4j-1.2.17.jar;C:\Users\zhuge\.m2\repository\jline\jline\0.9.94\jline-0.9.94.jar;C:\Users\zhuge\.m2\repository\org\apache\yetus\audience-annotations\0.5.0\audience-annotations-0.5.0.jar;C:\Users\zhuge\.m2\repository\io\netty\netty\3.10.6.Final\netty-3.10.6.Final.jar;C:\Users\zhuge\.m2\repository\com\google\guava\guava\22.0\guava-22.0.jar;C:\Users\zhuge\.m2\repository\com\google\code\findbugs\jsr305\1.3.9\jsr305-1.3.9.jar;C:\Users\zhuge\.m2\repository\com\google\errorprone\error_prone_annotations\2.0.18\error_prone_annotations-2.0.18.jar;C:\Users\zhuge\.m2\repository\com\google\j2objc\j2objc-annotations\1.1\j2objc-annotations-1.1.jar;C:\Users\zhuge\.m2\repository\org\codehaus\mojo\animal-sniffer-annotations\1.14\animal-sniffer-annotations-1.14.jar;D:\dev\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar
现在大致了解什么是类加载器与双亲委派机制 后我们来进入实战。
操作案例
接下来进入源码
为了方便快速理解,从核心方法,由核心类慢慢扩展理解
请看Launcher
核心类中的loadClass()
方法
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}
首先这个方法有两个参数String var1, boolean var2
,var2这个参数不做考虑,而var1他是我们加载类的包名路径;
我们用Debug打个断点看看,右击给断点设置条件var1.equals("com.example.demo03.demo.Math")
我们已经把Math
类拦截下来了,
前面有多个if语句,都是类加载的初始化过程,他最后调用了父类(ClassLoader)super.loadClass(var1, var2)
方法,我们直接跳进去。
父类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 {
//findBootstrapClassOrNull 为native方法,根据name加载类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果c为null,即加载失败
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//委派自己子类找,最终他会调用URlClassLoader类的findClass方法
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;
}
}
我们可以看到核心逻辑代码都被synchronized
包裹着,加上了getClassLoadingLock(name)
锁,所以他是线程安全的;
我们再往下看,调用findLoadedClass(name)
方法找到此加载类,如果c=null
也就是Math没有加载,那他必定会进入if语句
进入if语句后,可以看到try/catch
代码块
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
}
如果parent
不为空,则使用parent
调用加载类方法;
parent
是什么,怎么来的?
parent
其实就是ClassLoader
中的一个参数,里面装着类加载器它的父加载器
看看他是如何赋值的,看下Launcher.class
类的构造方法
调用了两个类加载器的初始化方法,点进去看看
ExtClassLoader
:
再次进去,注意(classLoader)为null
可以看到他调用父类的方法,点到最后,会进入最终父类ClassLoader
方法
我们可以知道ExtClassLoader 他的parent是null,按照双亲委派他应该为bootstarpLoader
,因为他是由C++实现所以他为null,而AppClassLoder
亦是如此,只不过他的parent放入的是ExtClassLoader
上述方法都是由C++来调用的所以debug断点是拦截不到的
回到loadClass
方法
首先,调用了findLoadedClass(name)
方法,查找该类是否已被加载吗,如果为空,则使用自己父加载器parent.loadClass(name, false)
再次调用此方法,如果parent
为空,则调用findBootstrapClassOrNull(name)
方法加载类。
如果未找到,则会调用findClass(name)
,委派子加载器加载,最终他会调用URLClassLoader findClass(final String name)
URLClassLoader:
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
//拼接类名全路径
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//记载类
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
会调用 defineClass
方法,而此方法是本地方法无法看到,了解大致流程就可以了
总结
1、初始化ExtClassLoader 与 AppClassLoder 给 parent 赋值(实现双亲委派前提);
2、调用核心方法loadclass,使用parent调用自己递归,从而实现双亲委派;
3、最终向下委派,调用findClass加载类;