java ClassLoader 基本原理.docx
《java ClassLoader 基本原理.docx》由会员分享,可在线阅读,更多相关《java ClassLoader 基本原理.docx(21页珍藏版)》请在冰豆网上搜索。
javaClassLoader基本原理
Ⅰ.类加载器基本概念
顾名思义,类加载器(ClassLoader )用来加载Java类到Java虚拟机中。
一般来说,Java虚拟机使用Java类的方式如下:
∙Java源程序(.java文件)在经过Java编译器编译之后就被转换成Java字节代码(.class文件)。
∙类加载器负责读取Java字节代码,并转换成java.lang.Class类的一个实例。
每个这样的实例用来表示一个Java类。
通过此实例的newInstance()方法就可以创建出该类的一个对象。
实际的情况可能更加复杂,比如:
Java字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
Ⅱ.Classloader类图
注意:
这是类关系,非对象关系。
基本上所有的类加载器都是java.lang.ClassLoader类的一个实例。
BootstrapClassLoader是一个单独的java类,其实在这里,不应该叫他是一个java类,因为,它已经完全不用java实现了。
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例。
除此之外,ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等。
不过本文只讨论其加载类的功能。
为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如:
getParent()
返回该类加载器的父类加载器。
loadClass(Stringname)
加载名称为name的类。
是加载器加载类的主要方法,也是实现双亲委派模型的主要方法(稍后会详细讲解)。
返回的结果是java.lang.Class类的实例。
findClass(Stringname)
在自己的查找路径中,查找名称为name的类,并调用defineClass方法,加载该类。
返回的结果是java.lang.Class类的实例。
findLoadedClass(Stringname)
在自己的类加载器中,查找名称为name的已经被自己加载过的类。
返回的结果是java.lang.Class类的实例。
defineClass(Stringname,byte[]b,intoff,intlen)
把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例。
这个方法被声明为final的。
按照《JavaLanguageSpecification》的定义,任何作为String类型参数传递给ClassLoader中方法的类名称都必须是一个二进制名称(上述方法中的"Stringname")。
有效类名称的示例包括:
"java.lang.String"
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
".URLClassLoader$3$1"
需要注意的是内部类的Stringname表示,如com.example.Sample$1和com.example.Sample$Inner等表示方式。
URLClassLoader是ClassLoader的子类,是系统类加载器(appCLassLoader)和扩展类加载器(extClassLoader)的父类(类图关系,非对象关系)。
URLClassLoader非常强大,可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。
实际上,在应用程序中,可以直接使用URLClassLoader来加载类,URLClassLoader提供了如下两个构造器:
∙URLClassLoader(URL[]urls):
使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls指定的系列路径来查询并加载类。
∙URLClassLoader(URL[]urls,ClassLoaderparent):
使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同。
Ⅲ.Classloader对象关系图
Java中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由Java应用开发人员编写的。
通常,会对类加载器对象指定一个或多个“目录或文件”,使类加载器只加载这些指定"目录或文件"下的.class文件。
系统提供的类加载器主要有下面三个:
1).引导类加载器(bootstrapClassLoader):
它用来加载 Java的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader。
例如:
Java的核心库一般是JRE目录下的“lib\rt.jar”等等。
具体路径可以用System.out.println(System.getProperty("sun.boot.class.path"));获取。
2).扩展类加载器(extClassLoader):
它用来加载 Java的扩展库。
Java虚拟机的实现会提供一个扩展库目录。
该类加载器在此目录里面查找并加载Java类。
例如:
Java的扩展库一般是JRE目录的“jre\lib\ext”等等。
具体路径可以用System.out.println(System.getProperty("java.ext.dirs"));获取。
3).系统类加载器(appCLassLoader):
它根据 Java应用的类路径(CLASSPATH)来加载Java类。
一般来说,Java应用的类都是由它来完成加载的。
可以通过ClassLoader.getSystemClassLoader()来获取它。
具体路径可以用System.out.println(System.getProperty("java.class.path"));获取。
除了引导类加载器之外,所有的类加载器都有一个父加载器。
通过getParent()方法可以得到。
对于系统提供的类加载器来说,系统类加载器(appCLassLoader)的父加载器是扩展类加载器(extClassLoader),而扩展类加载器(extClassLoader)的父加载器是引导类加载器(bootstrapClassLoader)。
而且,java中的每个Java类都维护着一个指向加载它的classloader的引用,通过getClassLoader()方法就可以获取到此引用,例如ClassLoadercl=Hello.class.getClassLoader()。
对于开发人员编写的类加载器(例如,CustomClassLoaderA)来说,CustomClassLoaderA在类Person中,实例化一个ClassLoader对象:
?
1
2
3
4
5
public class Person {
public void sayHello(){
ClassLoader customClassLoaderA=new CustomClassLoaderA();
}
}
那么customClassLoaderA的父加载器就是加载类Person的加载器,即Person.getClassLoader()。
一般来说,开发人员编写的类加载器的父类加载器是系统类加载器(appCLassLoader)。
所以,类加载器通过这种方式组织起来,形成树状结构。
如下图:
注意:
这是对象关系,非类关系。
代码示例:
通过递归调用getParent()方法来输出全部的父类加载器:
?
1
2
3
4
5
6
7
8
import .URL;
import .URLClassLoader;
public class CustomClassloaderA extends URLClassLoader {
public CustomClassloaderA(URL[] urls) {
super(urls);
}
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import .URL;
public class TestMain {
public static void main(String[] args) throws ClassNotFoundException {
URL url = new URL("file:
/D:
/workspace/classfinder/src/com/sunchp");
ClassLoader classloader = new CustomClassloaderA(new URL[] { url });
while (classloader !
= null) {
System.out.println(classloader.toString());
classloader = classloader.getParent();
}
}
}
运行结果:
com.sunchp.CustomClassloaderA@a574b2
sun.misc.Launcher$AppClassLoader@417470d0
sun.misc.Launcher$ExtClassLoader@439a8942
∙第一个输出的是自定义的类加载器com.sunchp.CustomClassloaderA@a574b2,它是我们自定义的类加载器CustomClassloaderA类的实例;
∙第二个输出的是系统类加载器(appCLassLoader),它是sun.misc.Launcher$AppClassLoader类的实例;
∙第三个输出的是扩展类加载器(extClassLoader),是sun.misc.Launcher$ExtClassLoader类的实例。
需要注意的是这里并没有输出引导类加载器,这是由于有些JDK的实现对于父类加载器是引导类加载器的情况,getParent()方法返回null。
Ⅳ.3种Classloader搜索路径
appCLassLoader和extClassLoader都是URLClassLoader的子类,所以他们实际也需要一个URL作为参考,去载入类。
1).appCLassLoader所参考的URL是从系统参数java.class.path取出来的。
这个参数,可以在我们执行java.exe时,使用-cp或者-classpath或者CLASSPATH环境变量指定:
?
1
2
String s=System.getProperty("java.class.path");
System.out.println(s);
classpath,代表的是当前类目录(或者说当前项目对应的class目录),已经所引用的jar文件。
使用-cp或者-classpath可以改变,相对于系统的CLASSPATH,优先使用-cp或者-classpath。
2).extClassLoader参考的是系统参数java.ext.dirs:
?
1
2
String s=System.getProperty("java.ext.dirs");
System.out.println(s);
3).bootstrapClassLoader参考的是系统参数,sun.boot.class.path:
?
1
2
String s=System.getProperty("sun.boot.class.path");
System.out.println(s);
∙改变appCLassLoader和extClassLoader对应的参数是有意义的,也会影响它们的搜索路径;
∙改变bootstrapClassLoader的参数是没有易用的,bootstrapClassLoader的搜索路径只是与sun.boot.class.path的预设值相同,并不是参考使用了sun.boot.class.path。
∙appCLassLoader和extClassLoader在内存中分别只有一个实例,所以在内存中动态改变参数是行不通的。
∙如果需要加载其他路径的class文件,推荐自定义类加载器,并设置自定义类加载器的搜索路径。
Ⅴ.自定义类加载器
JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。
查阅API文档中关于ClassLoader的方法不难发现,ClassLoader中包含了大量的protected方法——这些方法都可以被子类重写。
ClassLoader类有如下两个关键方法:
∙loadClass(Stringname,booleanresolve):
该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。
∙findClass(Stringname):
根据二进制名称,在自己的查找路径中,查找名称为name的类,并调用defineClass方法,加载该类。
返回的结果是java.lang.Class类的实例。
如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,当然,我们推荐重写findClass()方法,而不是重写loadClass()方法。
loadClass()方法的执行步骤如下:
∙用findLoadedClass(Stringname)来检查是否已经加载类,如果已经加载,则直接返回。
∙在父类加载器上调用loadClass()方法。
如果父类加载器为null,则直接使用根类加载器加载。
∙调用findClass(Stringname)方法查找类。
从以上步骤可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托,缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
在ClassLoader里还有一个核心方法:
ClassdefineClass(Stringname,byte[]b,intoff,intlen),该方法负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte[]b内,并把它转换为Class对象,该字节码文件可以源于文件、网络等。
defineClass()方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。
不过不用担心,程序员无须重写该方法。
事实上,该方法是final型,即使我们想重写也没有机会。
除此之外,ClassLoader里还包含如下一些普通方法:
∙findSystemClass(Stringname):
在本地文件系统装入文件。
它在本地文件系统中寻找类文件,如果存在,就使用defineClass()方法将原始字节转换成Class对象,以将该文件转换成类。
∙staticgetSystemClassLoader():
这是一个静态方法,用于返回系统类加载器。
∙getParent():
获取该类加载器的父类加载器。
∙resolveClass(Class
>c):
链接指定的类。
类加载器可以使用此方法来链接类c。
读者无须理会关于此方法的太多细节。
∙findLoadedClass(Stringname):
如果此java虚拟机已经加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。
此方法是java类加载缓存机制的体现。
loadClass()、findClass()、findLoadedClass()和defineClass的关系可以用如下代码表示(这也是实现下述“双亲委派模型”的主要逻辑):
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Class loadClass(String name,boolean resolve) {
// 检查类是否已被装载过
Class c = findLoadedClass(name);
if (c == null) {
// 指定类未被装载过
try {
if (parent !
= null) {
// 如果父类加载器不为空, 则委派给父类加载
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为空, 则委派给启动类加载加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 启动类加载器或父类加载器抛出异常后, 当前类加载器将其
// 捕获, 并通过findClass方法,调用defineClass(), 由自身加载
c = findClass(name);
}
}
}
Ⅵ.双亲委派模型
所谓的双亲委派模型,就是,classloader实例当有类需要载入时,会先让parent采用他的搜寻路径去加载类,如果parent加载不到,那么才由自己去加载。
其实,所谓双亲委派模型只有两个过程:
∙首先,自底向上,挨个加载器检查是否已经加载了指定类,直到根加载器(bootstrapClassLoader)。
这个过程中,如果已经加载,那么直接返回该类Class实例的引用。
这个过程主要是findLoadedClass()方法的调用。
∙然后,自顶向下尝试加载类。
如果根加载器(bootstrapClassLoader)也未加载成功该类,那么就会抛出异常,然后自顶向下挨个尝试加载。
直到customClassloader调用findClass()尝试加载。
如果还未加载成功,就抛出ClassNotFoundException给调用者。
代码逻辑:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Class loadClass() {
// 检查类是否已被装载过
Class c = findLoadedClass(name);
if (c == null) {
// 指定类未被装载过
try {
if (parent !
= null) {
// 如果父类加载器不为空, 则委派给父类加载
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为空, 则委派给启动类加载加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 启动类加载器或父类加载器抛出异常后, 当前类加载器将其
// 捕获, 并通过findClass方法, 由自身加载
c = findClass(name);
}
}
}
一般来说,通过XXXX.class.getClassLoader()或者Class.forName()获取当前类的加载器,然后用该加载器加载目标类,例如“com.sunchp.demo”。
此时,都是以当前类的加载器为起点,依次找它的父亲加载器,进行加载目标类。
(这个地方,对理解后面的“线程上下文类加载器”很重要)
情景一:
?
1
2
ClassLoader cl=XXXX.class.getClassLoader();
cl.loadClass("com.sunchp.Demo");
∙如果XXXX是由自定义加载器加载的,则从该自定义加载器开始查找"com.sunchp.Demo",customClassloader->appClassLoader->extClassLoader->bootstrapClassLoader;
∙如果XXXX是由appClassLoader加载的,则加载过程是appClassLoader->extClassLoader->bootstrapClassLoader;XXXX的加载器,看不到CustomClassLoader。
∙如果XXXX是由extClassLoader加载的,则加载过程是extClassLoader->bootstrapClassLoader;
∙如果XXXX是由bootstrapClassLoader加载的,则加载过程只有bootstrapClassLoader。
情景二:
?
1
2
3
4
5
public class A {
public void sayHello() {
Class demoClass = Class.forName("com.sunchp.Demo");
}
}
因为Class.forName用的是前类的加载器,即A.class.getClassLoader(),所以
∙如果当前类A是由自定义加载器加载的,则从该自定义加载器开始查找"com.sunchp.Demo",customClassloader->appClassLoader->extClassLoader->bootstrapClassLoader;同上。
∙如果当前类A是由appClassLoader加载的,则加载过程是appClassLoader->extClassLoader->bootstrapClassLoader;同上。
∙如果当前类A是