java自带三个重要的类加载器,其组织结构如下。
1.三个类加载器的创建
是rt.jar包里面sun.misc.Launcher类:
public Launcher()
{
ExtClassLoader localExtClassLoader;
try
{ //首先创建了ExtClassLoader
localExtClassLoader = ExtClassLoader.getExtClassLoader();
}
catch (IOException localIOException1)
{
throw new InternalError("Could not create extension class loader");
}
try
{ //以ExtClassloader作为父加载器创建了AppClassLoader
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
}
catch (IOException localIOException2)
{
throw new InternalError("Could not create application class loader");
} //默认下线程上下文加载器是appclassloader,这就是原因。
Thread.currentThread().setContextClassLoader(this.loader);
................
}
1.1 bootstrapClassLoader
是 jvm通过c编写的,不是java代码。
这里只看ExtClassLoader 和AppClassLoader 两个cl的创建过程.
1.2 ExtClassLoader创建过程
public static ExtClassLoader getExtClassLoader()
throws IOException
{ //可以知道ExtClassLoader类加载路径为java.ext.dirs
File[] arrayOfFile = getExtDirs();
try
{
(ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction()
{
public Launcher.ExtClassLoader run()
throws IOException
{
int i = this.val$dirs.length;
for (int j = 0; j < i; j++) {
MetaIndex.registerDirectory(this.val$dirs[j]);
}
return new Launcher.ExtClassLoader(this.val$dirs);
}
});
}
catch (PrivilegedActionException localPrivilegedActionException)
{
throw ((IOException)localPrivilegedActionException.getException());
}
}
private static File[] getExtDirs()
{
String str = System.getProperty("java.ext.dirs");
File[] arrayOfFile;
if (str != null)
{
StringTokenizer localStringTokenizer = new StringTokenizer(str, File.pathSeparator);
int i = localStringTokenizer.countTokens();
arrayOfFile = new File[i];
for (int j = 0; j < i; j++) {
arrayOfFile[j] = new File(localStringTokenizer.nextToken());
}
}
else
{
arrayOfFile = new File[0];
}
return arrayOfFile;
}
1.3 AppClassLoader 创建过程
AppClassLoader.getAppClassLoader的代码
public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader)
throws IOException
{ //可知AppClassLoader类加载路径为java.class.path
String str = System.getProperty("java.class.path");
final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str);
(ClassLoader)AccessController.doPrivileged(new PrivilegedAction()
{
public Launcher.AppClassLoader run()
{
URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile);
return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader);
}
});
}
可变的SystemClassLoader
通过ClassLoader#getSystemClassLoader()来获取
查看ClassLoader#initSystemClassLoader方法源码
ClassLoader#getSystemClassLoader()默认情况下,返回的是 AppClassLoader,但可通过系统属性“java.system.class.loader ” 来重新指定一个自定义类加载器(此加载器必须有一个参数为ClassLoader的构造函数)作为SystemClassLoader。我们自定义的类加载的会被AppClassLoader加载;实例化时以AppClassLoader作为构造参数,即自定义的SystemClassLoader的父加载器是AppClassLoader。
且将SystemClassLoader 设置为线程上下文加载器(无论SystemCloassLoader是AppClassLoader还是自定义ClassLoader,都将被设置为线程上下文类加载器)
1.4 UrlClassLoader指定类资源
这些ClassLoader都是UrlClassLoader的子类,构造的时候各自的资源路径不同,即参数URL[] urls不同,
public URLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}
URL的构造
- 通过文件系统构造Url
new URL[]{new URL(new File())}
资源的检索
通过ClassLoader#getResources(
ClassLoader cl = Thread.currentThread().getContextClassLoader();
String resourceName = "com/rock/Test1.class";
Enumeration urls = cl .getResources(resourceName);
while(urls.hasMoreElements()){
Url url = urls.nextElement();
}
getResources的解释
查找具有给定名称的资源。 资源是某些数据(图像,音频,文本等),可由类代码以与代码位置无关的方式进行访问。
资源的名称是用" /"分隔的路径名,用于标识资源。
此方法将首先在父类加载器中搜索资源; 如果父级为null,则搜索虚拟机内置的类加载器的路径。 失败的话,此方法将调用findResource(String)来查找资源
1.5 各自的类资源路径
- BootstrapClassLoader
启动类加载器,由 C 语言实现;是特定于平台的机器指令。负责开启整个加载过程。
加载 ExtClassLoader 和AppClassLoader (Launcher类里创建了exp和app cl,Launcher类的cl 是启动类加载器,根据cl传递机制)
加载%JAVA_HOME%/lib目录中或-Xbootclasspath中参数指定的路径中的类,这个路径下的类是JVM 启动时所需要的核心类。
//查看加载的资源
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
或者
System.getProperty("sun.boot.class.path")
- ExtClassLoader
由启动类加载器所加载
是扩展类加载器,实现为sun.misc.Launcher$ExtClassLoader
加载%JRE_HOME%/lib/ext目录中或-Djava.ext.dirs中参数指定的路径中的jar包;
扩展类加载器只能加载jar包(jar cvf test.jar 类全限定名(.修改为/);
//查看加载的资源
System.getProperty("java.ext.dirs")
- AppClassLoader
由启动类加载器所加载
是系统类加载器,实现为sun.misc.Launcher$AppClassLoader
加载 classpath 下的类,应用程序默认用它来加载类。
//查看加载的资源
System.getProperty("java.class.path")
通过idea 查看此信息,与通过java -jar 方式运行结果不同;
java -jar 方式 只显示.
idea 是通过操作 java.class.path 对应的property,添加了其它类路径信息。
- 自定义类加载器
用来加载自定义路径下的类
1.6 ClassLoader的获取路径
- 类的ClassLoader
clazz.getClassLoader(); //每个Class对象都会有一个属性指向将其加载的ClassLoader - 线程上下文的ClassLoader
Thread.currentThread.getContextClassLoader(); - 系统类加载器
ClassLoader.getSystemClassLoader(); - 调用者的ClassLoader
DriverManager.getCallerClassLoader();
总结
Java应用启动过程是首先BootstarpClassloader加载rt.jar包里面的sun.misc.Launcher类,而该类内部使用BootstarpClassloader加载器构建和初始化Java中三种类加载和线程上下文类加载器,然后在根据不同场景去使用这些类加载器去自己的类查找路径去加载类。
2 Parents委托实现
类加载器的组织形式,通过parent属性的指向,构成了树形结构
标准的委托逻辑,加载是指,检索类资源并将其加载。
- 父加载器能加载 父加载器来加载
- 自己在寻找加载资源之前,将让父类加载器去寻找加载。父类再找父类,直到bootstrap ClassLoader,bc没有父类加载器。;
- 保证了等级越高,加载的优先权越高
- 父加载器不加载 我就来加载(findClass);我加载不了,子加载器来加载。
- 若父类没有加载成功,才逐级下放这个加载权。
-
子类加载器不能加载父类加载器能加载的类,如java.lang.String,即使用户自己造一个这个类型,启动类加载器优先将java.lang.String加载成功后,应用类加载器不会再加载用户自己造的这个。
2.1 委托实现关键 在ClassLoader#loadClass方法
loadClass(类的全路径名);
一般我们自定义类加载器,是基于文件系统找到这个类资源文件,进行加载。
public abstract class ClassLoader {
// 每个类加载器都有个父加载器
private final ClassLoader parent;
public Class> loadClass(String name) {
// 查找一下这个类是不是已经加载过了
Class> c = findLoadedClass(name);
// 如果没有加载过
if( c == null ){
// 先委托给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找 Bootstrap 加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的 findClass 去加载
if (c == null) {
c = findClass(name);
}
return c;
}
protected Class> findClass(String name){
//1. 根据传入的类名 name,到在特定目录下去寻找类文件,把.class 文件读入内存
...
//2. 调用 defineClass 将字节数组转成 Class 对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成一个 Class 对象,用 native 方法实现,
//Class在使用之前,必须先被解析。
//从class文件解析出的这个,这个byte[]数组可以是加密,解密处理后,再传给defineClass方法
protected final Class> defineClass(byte[] b, int off, int len){
...
}
}
findLoadedClass
Returns the class with the given binary name if this loader has been recorded by the Java virtual machine as an initiating loader of a class with that binary name. Otherwise null is returned.
如果Java虚拟机将此加载程序记录为具有该二进制名称的类的initiating loader(初始类加载器),则返回具有给定二进制名称的类。 否则返回null。
通过初始类加载器的namespace(命名空间,即通过委派过程的缓存,返回 这个流程,存储到了初始类加载器的命名空间(叫类名空间更合适)了)来判断是否已经加载过了,无需在让定义类加载器来判断。
这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。通过双亲委托机制,子类加载器无法加载到 父类加载器管辖范围的同名的类。也就是说我们自己写的与核心类同名的类,委托模型就加载不到;如此来保证核心类不会被覆盖。
类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。同样的道理,如果你要自定义类加载器,不去继承 AppClassLoader,而是继承 ClassLoader 抽象类,再重写 findClass 和 loadClass 方法即可,Tomcat 就是通过自定义类加载器来实现自己的类加载逻辑。如果你要打破双亲委托机制,就需要重写 loadClass 方法,因为 loadClass 的默认实现就是双亲委托机制。
2.2 自定义子类加载器
自定义类加载器是为了扩展JVM动态加载类的能力。
若不打破委派机制的话, 自定义的子类加载器,只需要重写findClass方法,扩展自己类资源的检索范围。
2.3 打破双亲委托机制
要打破双亲委托机制,需要继承 ClassLoader 抽象类,并且需要重写它的 loadClass 方法,因为 ClassLoader 的默认实现就是双亲委托。
参考链接:
阿里加多
深入拆解Tomcat & Jetty