一、三种类加载器
JVM并不是把所有的类一次性全部加载到JVM中的,也不是每次用到一个类的时候都去查找,对于JVM级别的类加载器在启动时就会把默认的 JAVA_HOME/lib里的class文件加载到JVM中,因为这些是系统常用的类,对于其他的第三方类,则采用用到时就去找,找到了就缓存起来的, 下次再用到这个类的时候就可以直接用缓存起来的类对象了。
AppClassLoader的Parent是ExtClassLoader,而ExtClassLoader的Parent为Bootstrap ClassLoader。
之所以要定义这么多类加载器(当然还可以自己扩展)是因为java是动态加载类的,用到什么就加载什么以节省内存,
采用逐级加载的方式。
(1)首先加载核心API,让系统最基本的运行起来。比如启动类加载器会加载jdk包里的rt.jar(里面有java.lang.*,
所以不需要我们在import了,当然还有其他很多包)
(2)加载扩展类
(3)加载用户自定义的类
1、启动类装载器
启动(也称为原始)类加载器—bootstrap classloader,负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中。即——对于JVM级别的类加载器在启动时就会把默认的JAVA_HOME/lib里的class文件以及rt.jar中的java.lang包加载到JVM中,因为这些是系统常用的类。启动类装载器是用C++写的,它是Java虚拟机的一部分。
这里给出一个sun.boot.class.path的概念,它是系统属性,它包含了核心类库的类路径,如果启动类加载器需要加载核心类库时,就可以根据该路径去查找类了。
System.out.println("boot:"+System.getProperty("sun.boot.class.path"));
在我的计算机上的结果为:
boot: C:\Program Files\Java\jre1.8.0_121\lib\resources.jar;
C:\ProgramFiles\Java\jre1.8.0_121\lib\rt.jar;
C:\ProgramFiles\Java\jre1.8.0_121\lib\jsse.jar;
C:\ProgramFiles\Java\jre1.8.0_121\lib\jce.jar;
C:\ProgramFiles\Java\jre1.8.0_121\lib\charsets.jar;
C:\ProgramFiles\Java\jre1.8.0_121\lib\jfr.jar
2、扩展类装载器
扩展类加载器—extension classloader,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext)中JAR的类包。这为引入除Java核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放入这个目录的 JAR类包对所有的JVM和systemclassloader都是可见的。在这个实例上调用方法getParent()总是返回空值null,因为启动类装载器不是一个真正的ClassLoader实例。
3、系统类装载器
系统(也称为应用)类加载器—system classloader,它负责在JVM被启动时,它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。
补充:关于获取工程路径(运行时,类存放的路径)ClassLoader提供了两个方法用于从装载的类路径中取得资源:
public URL getResource (String name);
public InputStream getResourceAsStream (String name);
这里name是资源的类路径,它是相对与“/”根路径下的位置。getResource得到的是一个URL对象来定位资源,而getResourceAsStream取得该资源输入流的引用保证程序可以从正确的位置抽取数据。但是真正使用的不是ClassLoader的这两个方法,而是Class的 getResource和getResourceAsStream方法,因为Class对象可以从你的类得到(如YourClass.class或 YourClass.getClass()),而ClassLoader则需要再调用一次YourClass.getClassLoader()方法,不过根据JDK文档的说法,Class对象的这两个方法其实是“委托”(delegate)给装载它的ClassLoader来做的,所以只需要使用 Class对象的这两个方法就可以了。
下面是一些得到classpath和当前类的绝对路径的一些方法。你可能需要使用其中的一些方法来得到你需要的资源的绝对路径。
(1) this.getClass().getResource("")(在main方法中不能用this!)
得到的是当前类class文件的URI目录。不包括自己!,注该方法得到的是项目目录,也就是Bin的上一级!例如
如:file:/D:/workspace/jbpmtest3
(2) this.getClass().getResource("/").toURI().getPath()
得到的是当前的classpath的绝对URI路径 。
如:file:/D:/workspace/jbpmtest3/bin/
(3) this.getClass().getClassLoader().getResource("").toURI().getPath()
得到的也是当前ClassPath的绝对URI路径 。
如:file:/D:/workspace/jbpmtest3/bin/
(4) ClassLoader.getSystemResource("").toURI().getPath()
得到的也是当前ClassPath的绝对URI路径 。
如:file:/D:/workspace/jbpmtest3/bin/
(5) Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath()
得到的也是当前ClassPath的绝对URI路径 。
如:file:/D:/workspace/jbpmtest3/bin/
(6) ServletActionContext.getServletContext().getRealPath(“read.txt”)
输出:
/D:/Software/apache-tomcat-9.0.0.M22/apache-tomcat-9.0.0.M22/wtpwebapps/Servlet/WEB-INF/classes/read.txt
Web应用程序 中,得到Web应用程序的根目录的绝对路径。这样,我们只需要提供相对于Web应用程序根目录的路径,就可以构建出定位资源的绝对路径。
如:file:/D:/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/WebProject
注意点:
(1) 尽量不要使用相对于System.getProperty("user.dir")当前用户目录的相对路径。这是一颗定时炸弹,随时可能要你的命。
(2) 尽量使用URI形式的绝对路径资源。它可以很容易的转变为URI,URL,File对象。
(3)尽量使用相对classpath的相对路径。不要使用绝对路径。使用上面ClassLoaderUtil类的public static URL getExtendResource(String relativePath)方法已经能够使用相对于classpath的相对路径定位所有位置的资源。
(4) 绝对不要使用硬编码的绝对路径。因为,我们完全可以使用ClassLoader类的getResource("")方法得到当前classpath的绝对路径。如果你一定要指定一个绝对路径,那么使用配置文件,也比硬编码要好得多!
补充: JAVA_HOME、CLASSPATH和PATH之间的区别
(1)JAVA_HOME指向的是JDK的安装路径,如D:\JDK_1.4.2,在这路径下应该能够找到bin、lib等目录。
(2)classpath环境变量Classpath设置的目的,在于告诉Java执行环境,在哪些目录下可以找到您所要执行的Java程序所需要的类或者包。JVM和其他JDK工具通过依次搜索平台库,扩展库,和类路径来查找类。
(3)path环境变量指定了JDK命令搜索路径,设置path的作用是让操作系统可以找到JDK命令(如javac 、java)。path环境变量原来Windows里面就有,只需修改一下,使他指向JDK的bin目录,这样在控制台下面编译、执行程序时就不需要再键入一大串路径了。
二、类的加载过程
在版本1.2中,装载本地可用的class文件的工作被分配到多个类装载器中。启动类装载器负责装载核心的Java API文件。因为核心Java API class文件是用于“启动”Java虚拟机的,所以启动类装载器的名字也因此而得。用户自定义类装载器负责其他class文件的装载,例如应用程序运行的class文件。在应用程序启动以前,它至少创建一个用户自定义类装载器,也可能会是多个,所有的这些类装载器被连接在一个“双亲——孩子”的关系链中,其顶端是启动类装载器,其末端是系统类装载器。系统类装载器是右Java应用程序创建的,用于用户自定义类装载器的默认委派双亲。这个默认的委派双亲本质也是一个用户自定义的类装载器(实际上它是由Java虚拟机实现提供的),用于装载本地的class文件。
每个ClassLoader加载Class的过程是:
1.检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2
2.如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4
3.依托“双亲模式”请求parent classloader载入,如果成功到8,不成功到5
4.请求jvm从bootstrap classloader中载入,如果成功到8
5.寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.
6.从文件中载入Class,到8.
7.抛出ClassNotFoundException.
8.返回Class.
通俗地讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归(所有的类加载请求最终都会传送到顶层的“启动类加载器上”),如果父类加载器可以完成类加载任务,就成功返回;只有所有父类加载器无法完成此加载任务时,才自己去加载。(在没有明确地情况下,一般默认系统类装载器为初始的父类加载器)
源码如下:protected synchronizedClass<?> loadClass(String name,Boolean resolve) throws ClassNotFoundException{
//首先,检查类是否已经加载过了
Class c = findLoadedClass(name);
If(c==null){
try{
if(parent!=null){
c=parent.loadClass(name,false);
}else{
c=findBootstrapClassorNull(name);
}
}catch(ClassNotFoundException e){
//如果父类加载器抛出ClassNotFoundException
//说明福类加载器无法完成加载请求
}
If(c==null){
//在父类加载器无法加载器的时候
//再调用本身的findClass方法来进行类加载
c= findClass(name);
}
}
If(resolve){
resolveClass(name);
}
return c;
}
三、案例分析
下面给出一个例子,让我们来理解一下“双亲—孩子”模型的类加载顺序:
例子1:定义一个自定义类加载器—FileSystemClassLoader
package com.classLoader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* @authorzhegao
* 自定义一个类装载器:通常需要对findClass进行重写
*/
public class FileSystemClassLoader extends ClassLoader{
private String rootPath;
public FileSystemClassLoader(String rootPath){
this.rootPath=rootPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//获取字节数组
byte[] classData = getClassData(name);
if(classData==null){
throw new ClassNotFoundException();
}else{
return defineClass(name,classData,0,classData.length);
}
}
/**
* 读取文件的字节
*/
private byte[] getClassData(String className){
String classPath = classNameToPath(className);
System.out.println(classPath);
try {
InputStream is =new FileInputStream(classPath);
ByteArrayOutputStream bao =new ByteArrayOutputStream();
byte[] buffer =new byte[5000];
int point =0;
while((point=is.read())!=-1){
bao.write(point);
}
return bao.toByteArray();
} catch (Exception e) {
//e.printStackTrace();
}
return null;
}
/**
* 得到完整的类路径
*/
private String classNameToPath(String className){
//在这里需要做一个判断:一个是直接给类名;另一个是包下的类
if(className.contains(".")){
return rootPath+className.replace(".","\\")+".class";
}else{
return rootPath+className+".class";
}
}
}
package com.classLoader;
public class test {
public static void main(String[] args) {
String rootpath ="D:\\Software\\eclipse\\JavaJVM\\bin\\";
FileSystemClassLoader fsc =new FileSystemClassLoader(rootpath);
String className="com.examples.Sons";
try {
Class<?> class1 =fsc.loadClass(className); //加载Sample类
Object obj1 =class1.newInstance(); //创建对象
System.out.println(obj1.getClass().getName());//输出类名
System.out.println(obj1.getClass().getClassLoader());//输出类加载器名称
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果: com.examples.Sons
sun.misc.Launcher$AppClassLoader@4e25154f
分析: 类加载器的“双亲—孩子”模型得知,最底层到最高层依次是:我们的自定义类加载器—FileSystemClassLoader、系统类加载器、扩展类加载器和启动类加载器。在类第一次被加载时(一般运行时,类则被视为第一次被加载),自定义类加载器会委任给父类系统类加载器,然后像会“递归”依次委任给父类直到启动类加载器。然而启动类加载器只会根据sun.boot.class.path提供的路径加载相应的jar包,所以启动类加载器不能加载返回null;同样扩展类加载器会依据java.ext.dirs提供的路径加载相应的类,所以它也无法加载返回null;然后,到了系统类加载器,它会依据ClassPath去加载相应的类,显然能够找到,所以最终加载Sons类,其加载器就是“系统类加载器”。
例子2:假如还是上面的程序,我对com.classLoader.test做个修改
public class test {
public static void main(String[] args) {
String rootpath ="D:\\Software\\eclipse\\JavaJVM\\bin\\";
FileSystemClassLoader fsc =new FileSystemClassLoader(rootpath);
String className="com.examples.Son";//和上面比,修改了类名,由Sons变成了Son
try {
Class<?> class1 =fsc.loadClass(className); //加载Sample类
Object obj1 =class1.newInstance(); //创建对象
System.out.println(obj1.getClass().getName());
System.out.println(obj1.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果:C:\com\examples\Son.class
java.lang.ClassNotFoundException
at com.classLoader.FileSystemClassLoader.findClass(FileSystemClassLoader.java:22)
at java.lang.ClassLoader.loadClass(UnknownSource)
at java.lang.ClassLoader.loadClass(UnknownSource)
at com.classLoader.test.main(test.java:11)
分析:和上面分析过程一样,根据“双亲—孩子”模式,然而启动类加载器只会根据sun.boot.class.path提供的路径加载相应的jar包,所以启动类加载器不能加载返回null;同样扩展类加载器会依据java.ext.dirs提供的路径加载相应的类,所以它也无法加载返回null;然后,到了系统类加载器,会发现该类路径ClassPath无法找到Son这个类,所以也是返回null。在所有父类均不能加载类的前提下,自定义类加载器将会试着自己加载该类。首先,在使用父类的load()方法中会调用findClass()方法即判断该类在路径上是否存在,而这里的findClass()方法是由FileSystemClassLoader重写的。所以在执行findClass()方法时会抛“ClassNotFoundException”异常。
所以:这就是为何只有在发生异常时才会调用FileSystemClassLoader的findClass()方法,是因为正常加载时,由父类“系统类加载器”加载类,只有父类加载不了时,自定义类加载器才会试着去加载类,才会触发了自定义类加载器的findClass()方法。