最近总是被类加载器搞混,于是下决心好好的去看一下,总结一下类加载器的内容,之前也看过很多博客,但是没有总结过,也没有细细的去看过,隔一段时间,好像又忘记了,趁着这次机会,好好学习一下,自己总结一下,这样也能加深对类加载器的理解,有不对的地方,请大家指正,当然网上有非常的多的博客可以参考,有的写的也非常的好,这里只是一个学习笔记。
一、java的类加载器
类加载器其实是将java的类的编译文件class文件加载到JVM虚拟机中,程序就会正常运行了。在jvm启动的时候并不是一次性把所有的class文件都加载到内存中,而是根据需要动态的进行加载,当然有一部分class文件需要已启动就加载到内存中,否则jvm就无法正常运行了。
首先,我们通过一个例子来看看什么时候java的类加载器,其实如果用过反射的话,应该都清楚经常要用到classLoader这方面的参数。
@Test
public void testClazzLoader(){
ClassLoader loader = Test.class.getClassLoader();
System.out.println("class loader:"+loader.toString());
}
看到上面的例子,一个非常简单的获取class的例子,得到class对象之后,调用对应的方法,getClassLoader,可以得到我们想要的ClassLoader对象,下面我们输出一下这个对象:
class loader:sun.misc.Launcher$AppClassLoader@776ec8df
我们可以看到输出的对象是AppClassLoader这个类,那么这个是什么呢,这个就是java的三大类加载器之一。先不管这个是什么加载器,那么三大加载器是什么呢?
三种类加载器:Bootstrap Classloader, Extension Classloader, Application Classloader。
这里先做一个简单的介绍:
Bootstrap Classloader: 最顶层的类加载器,加载核心库,比如我们熟悉的%JRE_HOME%\lib的一些jar包等。
Extension Classloader:扩展的类加载器,主要加载%JRE_HOME%\lib\ext下面的jar包。
Application Classloader: 应用程序类加载器,该加载器由sun.misc.Launcher$AppClassloader()方法直接获取,所以又称为系统类加载器。就是加载当前classPath下面的所有类。
那么这么多的类加载器,他们时候用呢?有什么作用呢?
下面先回到上面那个例子,我们下看下Classloader这个类,这个类加载器有一个属性parent,这个属性的类型也是一个ClassLoader,说明他也是加载器,我们先把这个打印出来看看。
private final ClassLoader parent;
打印的结果如下,发现当前加载器的parent是一个ExtClassLoader加载器。class loader:sun.misc.Launcher$AppClassLoader@776ec8df
loader parent:sun.misc.Launcher$ExtClassLoader@6acbcfc0
这说明的AppClassLoader的父加载器是ExtCLassLoader,那么我们肯定任务,每个加载器都有一个父类加载器,那什么时候是个头呢,直接采用getParent()方法,发现报错了,空指针错误。public void testClazzLoader(){
ClassLoader loader = Test.class.getClassLoader();
System.out.println("class loader:"+loader.toString());
System.out.println("loader parent:"+loader.getParent().toString());
System.out.println("loader parent parent:"+loader.getParent().getParent().toString());
}
这是为什么呢,难道是没有父类加载器了么?这是以为 AppClassLoader和ExtClassLoader在调用构造方法传递parent对象的时候,AppClassloader传递的对象是ExtClassLoader,这就是为什么AppClassLoader的父加载器是ExtClassLoader,而在构造ExtClassloader的时候,传递的是null,因此他没有父加载器。那么问题来了,还有一个Bootstrap ClassLoader这个是啥时候加载的呢?该类是采用C++程序编写,是java虚拟机的本身的一部分,并非java中的类,因此无法获取到,它在java虚拟器启动的时候去加载一些系统的类库,本身并没有父加载器,好了以上的简介就到这里了。现在的问题是,有这么多的加载器,他们是如何加载的呢?
二、双亲委托机制
这里就不得不提到一个双亲委托机制,这个机制可以很好的解决了类加载器重复加载和安全性的问题。举个例子,现在编写了一个类TestDemo.java,那么谁去加载他呢?如果没有规则和约束,可能Bootstrap Classloader去加载他,然后AppClassLoader又去加载了一遍,这样耗费内存资源。java虚拟机,通过采用双亲委派模型来解决这个问题,各个ClassLoader之间的关系是通过组合关系来复用父加载器,当一个classLoader收到一类加载的请求的时候,首先把该请求委托派给该类的父类classloader进行处理,当父类无法处理的时候,才提交给当前的类classloader来处理。总之,对于每一次请求,优先由父类来进行处理,当父类无法处理的时候,才由子类进行处理。
处理的顺序是为:bootstrap classloader、ExtClassloader 、AppClassLoader、用户自定义的类加载器,通过下面图先了解一下大概:
这种流程说明,当一个类加载的请求过来的时候,优先发给底层的类加载器,但是底层的类加载器,比如说用户自定义的加载,他不会自己先去加载,而是去请求父加载器来进行加载,一直到顶层加载器,当顶层加载器加载不了的时候,在让子加载器进行加载。这个图说的不是很形象,这里借用网友的一个图。
这个图就说明的非常的形象,当一个请求过来的时候,先从自己的缓存中判断是否已经加载,如果已经加载,则不在进行加载,如果没有加载,则传递给父加载器进行加载。这个在加载器的源码中也能看的出来,下面是源码:
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); //父加载器不为null的时候,调用父加载器进行加载
} else {
c = findBootstrapClassOrNull(name); //父加载器为null则调用bootstrap进行加载
}
} 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); //当父加载器加载失败的时候,调用findClass
// 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;
}
}
以上的逻辑也说明了类加载器的双亲委托模型的原理。
那么如何判断两个class是否相同呢?
java在判断两个class是否相同的时候,不仅要判断两个类名是否相同,而且还要判断是否是同一个类加载器加载的,只有当两者条件都满足的时候,jvm才任务这两个是相同的。如果是两个不同的classloader加载的,jvm也会任务他们是不同的class。
三、自定义类加载器
自定义加载器是为了针对实际应用中出现的不同版本的jar包,不同的路径的问题,可以根据实际需要,加载指定目录下的指定jar包。其次,自定义类加载器可以保证一定的安全性,比如说.class文件很容易被反编译过来,但是如果你按照自己的编码方式进行编码,然后采用自己的类加载器进行加载,这样在传输的过程中即使被拦截,也无法解析出来。最后,可以实现类的热部署功能,可以检查已经加载的类是否被修改,如果被修改了,则进行重新加载。自定义类加载器的实现过程比较简单,直接继承classLoader类即可, 然后重写其中的某些方法,下面我们看个例子:
public class DiskClassLoader extends ClassLoader{
private String jarPath;
public DiskClassLoader(String jarPath) {
this.jarPath = jarPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String filePath = jarPath+name.replace(".", "/")+".class";
System.out.println("filepath="+filePath);
File file = new File(filePath);
FileInputStream fis =null;
try {
fis = new FileInputStream(file);
} catch (FileNotFoundException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = 0 ;
try {
while((len=fis.read(buf))!=-1){
baos.write(buf, 0, len);
}
return defineClass(name, buf, 0, buf.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
这里就是继承了ClassLoader这个类,然后重写了其中的findClass方法。该方法就是把字节码文件加载到内存中,一般我们都会覆盖这个方法,方便加载自定义的类。然后调用defineClass()这个方法进行解析字节码。下面看下我们调用这个自定义加载器来进行加载
@Test
public void testLoaderDemo() throws ClassNotFoundException, InstantiationException, IllegalAccessException, Exception, Exception{
DiskClassLoader loader = new DiskClassLoader("/tmp/project_1/open-api/src/test/java/");
Class clazz = loader.loadClass("com.java.em.TestDemo");
if(clazz!=null){
Object obj = clazz.newInstance();
Method m = clazz.getMethod("say",null);
m.invoke(obj, null);
}
}
通过上的调用,可以正确的获取到TestDemo这个类的字节码,然后通过反射的方式,得到对应的方法,最后执行该方法。以上的方式看上去比较简单,我们先了解一下ClassLoader这个类的主要几个API
public abstract class ClassLoader {
public Class loadClass(String name);
protected Class defineClass(byte[] b);
public URL getResource(String name);
public Enumeration getResources(String name);
public ClassLoader getParent();
}
这里面有一个比较重要的方法,loadClass(),它接受一个全类名,然后得到一个Class类型的实例。
defineClass()这个方法是将得到的字节数组进行实例化,调用的是jvm的native方法进行实例化,如上面我们的重写的findClass方法最后也是得到字节数组,通过defineClass方法进行实例化。
getParent方法是返回当期类加载器的父ClassLoader,如果是ExtClassLoader的话,其父加载器则为null。getResource
和getResources
方法,从给定的repository中查找URLs,同时它们也具备类似loadClass
一样的代理机制,我们可以将loadClass
视为:defineClass(getResource(name).getBytes())
。
由于classLoader采用的是双亲委托的模型,我们在自定义的时候,可以采用这个模型,也可以不采用这个模型,一般的来说我们重写了findClass()方法就是,如果不重写loadClass()方法的话就是采用默认的双亲委托模型。如果不想采用该模型,这直接重写该方法。
以上就是自定义类加载器的一些方法,下面还有一些小问题,也是在别人的博客中看来的,再次记录一下
.class和getCLass()方法区别:
.class用于类名,而getClass是一个final native的方法,只能用于类的实例。
.class是在编译期间就确定了一个Class对象,而getClass()是一个实例方法,只有具体的实例才有该方法,具体的类是没有的,是在运行期间确定一个类实例的Class对象。