类加载器
java被编译以后会生成一个.class的文件,这个文件是存放了一些对象的字节码信息,而这些字节码需要被加载到内存中,,这时候就需要我们用到ClassLoade了
先看下面的代码
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
// sun.misc.Launcher$AppClassLoader
//sun.misc.Launcher$ExtClassLoader
while (classLoader != null) {
System.out.println(classLoader.getClass().getName());
classLoader = classLoader.getParent();
}
// System类的加载器的名称:null
System.out.println("System类的加载器的名称:"+System.class.getClassLoader());
// List类的加载器的名称:null
System.out.println("List类的加载器的名称:"+ List.class.getClassLoader());
// this类的加载器的名称:sun.misc.Launcher$AppClassLoader
System.out.println("this类的加载器的名称:"+ ClassLoaderTest.class.getClassLoader().getClass().getName());
从打印结果中可以看出来 ClassLoaderTest类时由AppClassLoader
类加载器加载的
Java虚拟机中类加载器
java系统默认三个主要的类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader,AppClassLoader;
前两我们我们看到过了,没有见过的就是第一个BootStrap,那么这个是干什么用的,这里我们先思考一个问题,ExtClassLoader,AppClassLoader这两个都是java文件,既然是java文件,那么它们也需要个类加载器,所以可以认为加载它们的应该不能再是一个java类了,那是什么东东?其实就是BootStrap,ç是用c/c++写的,封装到JVM内核当中了
- BootStrap 负责将 它负责将
下面的核心类库或/lib -Xbootclasspath
选项指定的jar包加载到内存中,由于是虚拟机内核中的代码,开发者是无法引用的 - ExtClassLoader 由Sun的
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)
实现的。它负责将< Java_Runtime_Home >/lib/ext
或者由系统变量-Djava.ext.dir
指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。 - AppClassLoader 由Sun的
AppClassLoader(sun.misc.Launcher$AppClassLoader)
实现的。它负责将系统类路径java -classpath
或-Djava.class.path
变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。
JVM在加载类时默认采用的是双亲委派机制
。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
类加载器的委托机制:
当Java虚拟机要加载第一个类的时候,到底派出哪个类加载器去加载呢?
- 首先当前线程的类加载器去加载线程中的第一个类(当前线程的类加载器:Thread类中有一个
get/setContextClassLoader(ClassLoader cl)
;方法,可以获取/指定本线程中的类加载器) - 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B
- 还可以直接调用ClassLoader.loadClass(String className)方法来指定某个类加载器去加载某个类
每个类加载器加载类时,又先委托给其上级类加载器当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则会抛出ClassNotFoundException
,不是再去找发起者类加载器的儿子,因为没有getChild()方法。 - 如上图所示: CustomClassLoader->AppClassLoader->ExtClassLoader->BootStrap.自定定义的CustomClassLoader首先会先委托给AppClassLoader,AppClassLoader会委托给ExtClassLoader,ExtClassLoader会委托给BootStrap,这时候BootStrap就去加载,如果加载成功,就结束了。如果加载失败,就交给ExtClassLoader去加载,如果ExtClassLoader加载成功了,就结束了,如果加载失败就交给AppClassLoader加载,如果加载成功,就结束了,如果加载失败,就交给自定义的CustomClassLoader类加载器加载,如果加载失败,就报ClassNotFoundException异常,结束。
现在咱们再想一下上面的代码的打印结果,System类,List,Map等这样的系统提供jar类都在rt.jar中,所以由BootStrap类加载器加载,因为BootStrap是祖先类,不是Java编写的,所以打印出class为null,默认情况它的类加载时由AppClassLoader来完成的,如果我们将项目打包成.jar并将文件拷贝到Java的安装目录中的Java/jre7/lib/ext/
目录下,这时候就发,这时候就说明了上面的结论是正确的,因为ExtClassLoader加载jre/ext/*.jar
,首先AppClassLoader类加载器发请求给ExtClassLoader,然后ExtClassLoader发请求给BootStrap,但是BootStrap没有找到ClassLoaderTest类,所以交给ExtClassLoader处理,这时候ExtClassLoader在.jar
中找到了ClassLoaderTest类,所以就把它加载了,然后结束了。
自定义类加载器
先看下他的构造方法
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
通过上面的分析,默认的类加载器是AppClassLoader,如果我们自定义CloassLoader的话,主要是自定义加载Class文件的方式,可以查看loadClass
里面的源码
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 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
}
}
return c;
}
仔细分析源码发现,只需定义findClass方法即可,findClass这个方法就是根据name来查找到class文件,在loadClass方法中用到,所以我们只能重写这个方法了,只要在这个方法中找到class文件,返回一个Class对象即可。
问题来了,如果将一个class文件变成Class对象呢?我们需要用到下面的这个方法:defineClass,这个方法很简单就是将class文件的字节数组编程一个Class对象,这个方法不能重写,内部实现是在C/C++代码中实现的
下面我们重新定义个ClassLoader,重写里面的findClass方法,我们去加载一个加密的文件,在findClass里面去解密
protected Class> findClass(String name) throws ClassNotFoundException {
//class文件的路径
String classPathFile = classDir + "/" + name + ".class";
try {
//将class文件进行解密
FileInputStream fis = new FileInputStream(classPathFile);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 这里是解密操作
encodeAndDecode(fis,bos);
byte[] classByte = bos.toByteArray();
//将字节流变成一个class
return defineClass(classByte,0,classByte.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
这个时候我们调用我们自定义的那个ClassLoader的话能够调用成功,通过getClass().getClassLoader().getClass().getName()
打印出来的ClassLoader就是我们自己定义的ClassLoader,
到此不要以为结束了,这里还有很多的问题呀,看一下问题的结果是没有问题,但是这里面还有很多的东西需要去理解的,首先来看一下,按照我们之前说的类加载机制,应该是先交给父级的类加载器,AppClassLoader->ExtClassLoader->BootStrap,ExtClassLoader和BootStrap没有找到ClassLoaderAttach.class,但是AppClassLoader类加载器应该能找到呀,可以为什么也没有找到呢?这时因为loadClass方法在使用系统类加载器的时候需要传递全称(包括包名),我们传递ClassLoaderAttachment的话,AppClassLoader也是没有找到这个ClassLoaderAttachment,所以还是MyClassLoader处理了,如果我们这么处理后,应该是能够正常加载的
Class classDate = new MyClassLoader("class_temp").loadClass("ClassLoaderAttachment");
改成
Class classDate = new MyClassLoader("class_temp").loadClass("com.ding.demo.ClassLoaderAttachment");
这样修改后就变成AppClassLoader了,这个时候我们如果用了加密后的ClassLoaderAttachment文件,是会报错的,无法加载该文件的.