类加载器

来源:互联网 发布:vga 网络机顶盒 编辑:程序博客网 时间:2024/06/06 13:56

一、类与类加载器

回顾一下,加载阶段进行的操作:
1、通过一个类的全限定名来获取其定义的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在JAVA虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间。也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。

  • Class对象的equals方法自然是判断两个Class对象在堆中的地址是否一样;
  • Class对象的public native boolean isAssignableFrom(Class< ?> cls);判断Class对象之间是否有联系。如果入参的Class对象来自这个接口,或者就是这个类本身,或者是其子类,返回true, 否则返回false。java.lang.Class.isAssignableFrom()用法解析
    // 入参:circleClass是一个Class对象,判断circleClass代表的类,是否实现,继承,等同 Shape.    if(Shape.class.isAssignableFrom(circleClass)){            System.out.println("AAAAAAAAAAAAAAAA");    }
  • Class对象的public native boolean isInstance(Object obj);判断对象obj是否是一个接口的实例,或者是这个类及其子类的实例。
  • instanceof关键字 和 Class的isInstance()方法 功能完全相同
import java.io.IOException;import java.io.InputStream;public class ClassLoaderTest {    public static void main(String[] args) throws Exception {        ClassLoader myLoader = new ClassLoader() {            @Override            public Class<?> loadClass(String name) throws ClassNotFoundException {                try {                    String fileName = name.substring(name.lastIndexOf('.') + 1) + ".class";                    InputStream is = this.getClass().getResourceAsStream(fileName); //相对路径:该内部类表示的class文件所处的目录下找ClassLoaderTest.class                    if (is == null) {                        return super.loadClass(name);                    }                    byte[] b = new byte[is.available()];                    is.read(b);                    return defineClass(name, b, 0, b.length);                } catch (IOException e) {                    throw new ClassNotFoundException(name);                }            }        };        Object obj = myLoader.loadClass("ClassLoaderTest").newInstance();        /**         * Exception in thread "main" java.lang.LinkageError:         * loader (instance of  ClassLoaderTest$1): attempted  duplicate class definition for name: "ClassLoaderTest"         */        //System.out.println(myLoader.loadClass("ClassLoaderTest")); //唯一性        System.out.println(obj.getClass());        System.out.println(obj instanceof ClassLoaderTest);     }}
class ClassLoaderTestfalse

从输出结果可以看出来:

  1. 这个obj对象确实是类ClassLoaderTest实例化出来的
  2. 这个obj对象与类ClassLoaderTest做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,另一个是由我们自定义的类加载器myLoader加载的,虽然都来自同一个Class文件(都加载的同一个Class文件),但是依然是两个独立的类。

二、双亲委派模型

类加载器分类

  第一类是启动类加载器(Bootstrap ClassLoader)这个类加载器主要加载JVM自身工作需要的类。这个类加载器由C++语言实现(特指HotSpot),是虚拟机自身的一部分。负责将存放在%JAVA_HOME%\lib目录中的,或者被-Xbootclasspath参数所指定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类加载到虚拟机内存中。
  另一类就是所有其他的类加载器,这些加载器都是由java实现,独立于虚拟机外部都继承自抽象类java.lang.ClassLoader
  
  扩展类加载器(Extension ClassLoader):这个类即在其有sun.misc.Launcher的内部类 ExtClassLoader实现,它负责加载%JAVA_HOME%\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  
  应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径上指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  
  应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

1)在执行非置信代码之前,自动验证数字签名。

2)动态地创建符合用户特定需要的定制化构建类。

3)从特定的场所取得Java class,例如数据库中和网络中。

事实上当使用Applet的时候,就用到了特定的ClassLoader,因为这时需要从网络上加载java class,并且要检查相关的安全信息,应用服务器也大都使用了自定义的ClassLoader技术。

如何获得ClassLoader

  1. this.getClass().getClassLoader();//使用当前类的ClassLoader
  2. Thread.currentThread().getContextClassLoader();//使用当前线程的ClassLoader
  3. ClassLoader.getSystemClassLoader();//使用系统ClassLoader

[Tips]如何获得类的Class对象:
1. Class.forName(类路径全名);
2. this.getClass();
3. 使用.class,比如String.class
4. 对于基本数据类型有:Class c1 = int.class(class只是约定标记,不是成员属性) 或者Class c2 = Integer.TYPE

JVM加载class文件到内存有两种方式:
  1. 隐式加载:所谓的隐式加载就是不通过在代码里调用ClassLoader来记载需要的类,而是通过JVM来自动加载需要的类到内存的方式。例如,当我们在类中集成或者引用某个类是,JVM在解析当前这个类时发现引用的类不在内存中,那么就会自动将这些类加载到内存中。
  2. 显示加载:相反的显示加载就是我们在代码中通过调用ClassLoader类来加载一个类的方式,例如,调用this.getClass.getClassLoader().loadClass()或者Class.forName(),或者我们自己实现的ClassLoader的findClass()方法等。

双亲委派模型

JVM中的类加载器的层次关系如下图所示:
这里写图片描述
双亲委派模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)之外,其余的类加载器都应当有自己的父类加载器(ClassLoader里有parent属性)。这里类加载器之间的父子关系一般不会以inheritance的关系实现而是都是使用组合Composition关系来复用父类加载器的代码。
双亲委派模型不是一个强制性的约束模型,而是java设计者推荐给开发者的一种类加载器实现方式。在java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过3次较大的“被破坏”情况,这个稍后再阐述。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委托机制的作用是防止系统jar包被本地替换,因为查找方法过程都是从最底层开始查找。 因此,一般我们自定义的classloader都需要采用这种机制,我们只需要继承java.lang.ClassLoader实现findclass即可,如果需要更多控制,自定义的classloader就需要重写loadClass方法了,比如tomcat的加载过程,这个比较复杂,可以通过其他文档资料查看相关介绍。
使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。

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); //在父类加载器上调用loadClass方法                    } else {                        c = findBootstrapClassOrNull(name); //如果父类加载器为null,则使用虚拟机的内置类加载器                    }                } 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;        }    }

三、破坏双亲委派模型

上文提到了到目前为止,双亲委派模型出现过3次较大规模的“被破坏”的情况,这里详细阐述一下。
  第一次。发生在双亲委派模型出现之前(jdk1.2发布之前)。由于双亲委派模型在jdk1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在jdk1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,java设计者引入双亲委派模型时不得不做出了一些妥协。历史已经成为过去,具体的在此不赘述,需要注意的是jdk1.2之后不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()中,这样保证符合双亲委派模型的规则。
  第二次。由模型本身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类的统一问题,基础类之所以称为基础,是因为它们总是作为被用户代码调用的API,但是,如果基础类又要调用回用户的代码,怎么办呢?当父类加载器需要请求子类加载器去完成类加载动作,比如JNDI服务:它的代码由启动类加载器去加载,但JNDI的目的是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的Classpath下的JNDI提供者(SPI)的代码,但是启动类加载器不“认识”这些代码。这是就要用到了线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。
  这里怎么又出来一个context classloader呢?它有什么用呢?我们在建立一个线程Thread的时候,可以为这个线程通过setContextClassLoader方法来指定一个合适的classloader作为这个线程的context classloader,当此线程运行的时候,我们可以通过getContextClassLoader方法来获得此context classloader,就可以用它来载入我们所需要的Class。默认的是system classloader。利用这个特性,我们可以“打破”classloader委托机制了,父classloader可以获得当前线程的context classloader,而这个context classloader可以是它的子classloader或者其他的classloader,那么父classloader就可以从其获得所需的 Class,这就打破了只能向父classloader请求的限制了。这个机制可以满足当我们的classpath是在运行时才确定,并由定制的 classloader加载的时候,由system classloader(即在jvm classpath中)加载的class可以通过context classloader获得定制的classloader并加载入特定的class(通常是抽象类和接口,定制的classloader中是其实现),例如web应用中的servlet就是用这种机制加载的.
  第三次。 由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是当前一些非常“热门”的名称:代码热替换、模块热部署等,类似于鼠标键盘热拔插。具体的可以看一下OSGi,在OSGi环境下,类加载器不再是双亲委派模型中的树型结构,而是一种网状结构。具体的可以翻阅一些相关资料。


四、自定义类加载器

findClass()的功能是找到class文件并把字节码加载到内存中。自定义的ClassLoader一般覆盖该方法,以便使用不同的加载路径,然后调用defineClass()解析字节码。AppClassLoader只负责加载用户类路径上指定的类库。
defineClass()方法用来将byte字节流解析成JVM能够识别的Class对象。有了这个方法意味着我们不仅仅可以通过class文件实例化对象,还可以通过其他方式实例化对象,如我们通过网络接收到一个类的字节码,拿这个字节码流直接创建类的Class对象形式实例化对象。
自定义的加载器可以覆盖方法loadClass()以便定义不同的加载机制(不一定就是双亲委派模型)。
  
如果自定义的加载器仅覆盖了findClass(),而未覆盖loadClass(即加载规则一样,但加载路径不同);则调用getClass().getClassLoader()返回的仍然是AppClassLoader!因为真正的load类,还是AppClassLoader。
自定义类加载器

原创粉丝点击