类加载器

来源:互联网 发布:手机淘宝竟然被挤爆了 编辑:程序博客网 时间:2024/06/07 21:10

1 什么是类加载器

类加载器,顾名思义,就是把类加载到JVM的一个模块。一般来说JVM利用类加载器读取一个.class字节码文件并将其转换为java.lang.Class对象。然后可以通过Class的newInstance方法生成一个类的实例。基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。而实际上,ClassLoader除了能将Class加载到JVM中之外,还有一个重要的作用就是判断一个类该由谁加载,它是一个父级优先的加载机制,这就是双亲委派模型。

2 类加载器的结构

2.1 类加载器的常用方法

我们在使用类加载器的时候,经常会用到或拓展ClassLoader,下面是它的几个主要方法。

ClassLoader中与加载类相关的方法

方法

说明

getParent()

返回该类加载器的父类加载器。

loadClass(String name)

加载名称为 name的类,返回的结果是 java.lang.Class类的实例。

findClass(String name)

查找名称为 name的类,返回的结果是 java.lang.Class类的实例。

findLoadedClass(String name)

查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。

defineClass(String name, byte[] b, int off, int len)

把字节数组 b中的内容转换成 Java类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。

resolveClass(Class<?> c)

链接指定的 Java类。


defineClass方法用来将byte字节流解析成JVM能识别的Class对象,有了这个方法意味着我们不仅可以通过class文件实例化对象,还可以通过类似网络接收的方法获取字节流然后生成Class对象。这也是Java发明类加载器的原因。期初只是为了方便JavaApplet,因为需要从远程下载 Java 类文件到浏览器中并执行。现在虽然已经几乎看不到JavaApplet的身影了,但是类加载器依然有很大的使用空间。

defineClass方法通常和findClass方法一起使用,我们可以直接通过覆盖ClassLoader父类的findClass方法来实现类的加载规则,从而取得要加载的类的字节码。然后调用defineClass方法生成类的Class对象


2.2 ClassLoader的等级加载机制

等级加载机制,也就是我们经常看到的双亲委派模型。从Java虚拟机的角度来说,分类两类不同的加载器,一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是由C++语言实现,它是虚拟机自身的一部分;另一种就是其他类的加载器,他们都继承自ClassLoader抽象类。下面是类加载器的结构图

  • BootstrapClassloader :它的主要工作就是加载JVM自身工作需要的类,它完全由JVM控制,别人也访问不到这个类,它既没有更高一级的父加载器,也没有子加载器。所有上图中它并没有与下面的加载器连接在一起。因为对于某些虚拟机的实现来说,当一个类的parent为BootstrapClassLoader的时候,它的getParent方法返回null。它默认加载<JAVA_HOME>\lib目录中的的jar包,但只能加载被虚拟机识别的(如rt.jar等),因此即使自己写一个jar包放在这个目录下,也不会被加载的。
  • ExtClassLoader:这个加载器由sun.misc.Launcher$ExtClassLoader实现,可以看出它是Launcher的一个内部类,它负责加载<JAVA_HOME>\lib\ext目录中的或者被java.ext.dirs系统变量执行的路径中的类库。
  • AppClassLoader:它的父类是ExtClassLoader,它是由sun.misc.Launcher$AppClassLoader实现的,它同样是Launcher的内部类。因为它是getSystemClassLoader()方法的返回值,所以通常称为系统类加载器。它负责加载用户自定义的类,也就是说我们自己写的Java代码在没有指定类加载器的时候全部由它来加载,而我们自定义的加载器也都是它的子加载器。

下面通过一个例子来看看类加载器的结构,本例中我自定义了一个MyClassLoader类加载器,并用它实例化了一个对象。通过下面的代码来获取它的加载器“树“。

public static void getParentClassLoader(Object object){ClassLoader loader=object.getClass().getClassLoader();while(loader!=null){System.out.println(loader);loader=loader.getParent();}}
输出结果如下:

test.MyClassLoader@74a14482sun.misc.Launcher$AppClassLoader@4e0e2f2asun.misc.Launcher$ExtClassLoader@3d4eac69


我们可以看到并没有输出BootstrapClassloader,这也跟我们上面的图是吻合的。在Java对getParent()方法描述中,有这样一段话,这是由于有些JVM的实现对于父类加载器是引导类加载器的情况,getParent()方法返回null下面来看一下Object的类加载器情况,它的输出是不是更加可以理解上面那个getParent的描述,而且由此可以知道Object类就是由BootstrapClassloader加载的。

ClassLoader classLoader=Object.class.getClassLoader();System.out.println(classLoader);
输出结果如下:

Null

3 双亲委派模型

3.1 父亲办不到啊!

在上面我们曾提到双亲委派模型,也提到getParent方法,从类加载器的结构图上来看也有了一些认识。这种层次结构就称为双亲委派模型。在双亲委派模型中,除了顶层的引导加载器,其他的加载器都有自己的父类加载器。

在这种模式下,一个加载器在收到加载请求的时候,它并不是去尝试加载这个类,而是将这个请求委派给它的父类加载器去完成,直到顶层加载器。只有当父类加载器办不到的时候,才会尝试自己去加载。

下面来看看Java源码就更好理解了:

 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);//首先利用parent去加载                    } 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                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                    sun.misc.PerfCounter.getFindClasses().increment();                }            }            if (resolve) {                resolveClass(c);            }            return c;        }    }

3.2 类的唯一性

在Java中,对于任意一个类,都需要由加载器它的类加载器和这个类本身一同确立其在Java中的唯一性,每一个类加载器都有一个独立的类命名空间。也就是说想比较两个类是否相等,只有相同的类加载器加载才有意义。来看下面这个例子:

Object example=(new MyClassLoader()).findClass("test.Example").newInstance();System.out.println(example.getClass());System.out.println(example instanceof Example);

输出结果如下:
class test.Examplefalse

这里面在test包下面定义了Example类,同时自定义了一个类加载器,并用它去加载test.Example类并实例化对象。但是当利用instance of检查类型的时候却返回false。这是因为此时虚拟机中有两个Example的Class,一个是由系统默认加载的,一个是由自定义加载器加载的。虽然来自同一个Class文件,但是依然是两个独立的类。

看过上面的例子再来看看双亲委派模型的意义。使用了双亲委派模型以后,所有的类也就根据他们的类加载器定义了一个等级。如java.lang.Object类在rt.jar中,有双亲委派模型可以知道,委派到Bootstrap ClassLoader就可以加载了,因此Object类在程序的各种类加载器环境中都是一个类。如果没有这种模型,各个加载器自行去加载的话,如果我们自己定义一个java.lang.Object的类话,那系统中就会出来很多个Object类,就相当于动摇了Java的根基,也就无法生存的。

 

4 类加载器的几个异常

4.1 ClassNotFoundException

ClassNotFoundException恐怕是Java程序员经常碰到的异常。这个异常通常是发生在显式加载类的时候,如调用Class中的forName()方法,ClassLoader中的loadClass()方法或ClassLoader中的findSystemClass方法。这个异常主要是JVM在加载需要的字节码文件时,没有找到文件,解决办法就是检查classpath路径。获取classpath的方法:

this.getclass().getclassLoader.getResource(“”).toString();

 

4.2 NoClassDefFoundError

在Java文档中,对这个错误的定义如下:

Thrown ifthe Java Virtual Machine or a ClassLoader instance tries to load in thedefinition of a class (as part of a normal method call or as part of creating anew instance using the new expression) and no definition of the class could befound.

The searched-forclass definition existed when the currently executing class was compiled, butthe definition can no longer be found.

也就是在使用New关键字、属性引用某个类、继承了某个接口或类,已经方法的某个参数引用了某个类,这时会触发JVM隐式加载这些类,但发现不存在。

解决这个问题的办法就是确保每个类引用的类都在classpath下面。


5 自定义类加载器

Classloader到底有啥用?前面已经说过了,那既然有JDK提供的,为啥还要自己定义。其实大多数情况下并不需要自定义,但当我们要加载的类是经过特殊处理(如加密等)的时候,就需要自定义加载器了。同时当需要实现类的热部署的时候,自定义类加载器就不可或缺了。

下面通过重写findClass方法类实现一个类加载器

/** * 自定义类加载器 * @author songxu * */public class MyClassLoader extends ClassLoader {String dirPath=ReloadTest.class.getResource("").getPath();String srcPath=dirPath.substring(0,dirPath.indexOf("test"));static byte[]cachedata =new byte[1024];@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte [] classData=getData(name);if(diff(classData, cachedata)){cachedata=classData;System.out.println("新的数据加载");}if(null==classData){System.out.println("类名为:"+name+"无法加载");return super.loadClass(name);}else {return defineClass(null,cachedata,0,cachedata.length);}}/** * 获取字节流 * @param classname * @return */private byte [] getData(String classname){String path=srcPath+classname.replace('.', File.separatorChar)+".class";File file=new File(path);System.out.println("最后修改时间:"+getLastModify(file.lastModified()));FileInputStream  inputStream=null;try {inputStream=new FileInputStream(path);byte [] buffer=new byte[inputStream.available()];inputStream.read(buffer);return buffer;} catch (Exception e) {e.printStackTrace();}finally{try {inputStream.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}return null;}/** * 获取类的最后修改时间 * @param time * @return */private String getLastModify(long time){SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");Date date=new Date(time);return simpleDateFormat.format(date);}/** * 检查是否有新的类加载 * @param newdata * @param cached * @return */private boolean diff(byte []newdata,byte[]cached){if(newdata.length!=cached.length){return true;}for(int i=0;i<newdata.length;i++){if(newdata[i]!=cached[i]){return true;}}return false;}}

5.1是findClass还是loadClass?

对于自己定义类加载器,实际上还可以重写loadClass方法,从JDK源码可以看出,在这个方法中实现了双亲委派模型,而如果我们覆盖了loadClass方法,就很容易破坏到这个委派模型。重新findClass方法还是比较保险的,因为在loadClass方法中,如果双亲没有加载这个类,就会自动调用findClass方法。












 


0 0