java类加载器以及双亲委派机制

来源:互联网 发布:国家安全网络领导小组 编辑:程序博客网 时间:2024/05/16 06:40

                                                                     java类加载器以及双亲委派机制

一、类加载器

      JVM定义了三类类加载,分别是:

       1)Bootstrap ClassLoader /启动类加载器

   是用本地代码实现的类装入器,它负责将$JAVA_HOMEjre/lib/rt.jar下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,C++实现,不是ClassLoader子类
,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  2)Extension ClassLoader/扩展类加载器

   是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。负责加载java平台中扩展功能的一些jar包即负责将$JAVA_HOME/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

 3)App ClassLoader/系统类加载器

  是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

二、双亲委派机制以及该机制的好处

  JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

 好处:

   假设没有双亲委派模型,试想一个场景:
     黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
   而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
   或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

 但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
 举个简单例子:
 ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

三、类的加载机制

 类被加载到虚拟机内存包括加载、链接、初始化几个阶段。其中链接又细化分为验证、准备、解析。

 

 1.加载  

  加载就是将二进制的字节码通过IO输入到JVM中,我们的字节码是存在于硬盘上面的,而所用的类都必须加载到内存中才能运行起来,加载就是通过IO把字节码从硬盘迁移到内存中。

  加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义类加载器去控制字节流的获取方式。
  (1)通过类的全名产生对应类的二进制数据流。
  (2)将这些二进制数据流转换为方法区的运行时数据结构。
  (3)创建代表这个类的java.lang.Class对象。作为方法区这些数据的访问入口。

 2.链接

  (1)验证(Verify):字节码验证器将验证生成的字节码是否正确,如果验证失败,将提示验证错误;

  为什么这里还要验证加载类的正确性,难道通过Java虚拟机的javac编译器生成的字节码还会有错误不成?当然,javac编译出来的类都是正确的,但是如果是通过其他途径生成的字节码呢?是不是正确的呢?就比如你自己建一个文本文件,然后重命名该文件为Test.class,然后让JVM来运行这个类,显然是错误的。当然因为JDK的源码是开放的,所以JVM字节码的生成规则也是公开的,所以也有一些第三方的软件可以生成符合JVM规范的字节码文件,如CGlib

 (2) 准备(Prepare):对于所有静态变量,内存将会以默认值进行分配;所有原始类型的值都为0。如float为0f、 int为0、boolean为0、引用类型为null。

 (3) 解释(Resolve):有符号存储器引用都将替换为来自方法区(Method Area)的直接引用。

 符号引用是一个字符串,它唯一标识一个类、一个字段、一个方法等目标。而直接引用对于类变量、类方法指的是指向方法区的指针,然后对于实例方法、实例对象来说就是偏移量,比如一个实例方法,子类中方法表中的偏移量和父类是一致的,这个偏移量可以确定某个方法的位置。

 3.初始化

  这是类加载的最后阶段,所有的静态变量都将被赋予原始值,并且静态区块将被执行。

  为类的静态变量赋予正确的初始值,上面是赋予默认值,这里是赋予正确的初始值,什么是正确的初始值,就是用户给赋予的值。我们来看一个例子

class Test{

    Private static int a = 1;

}

 我们知道,这个类加载好之后,a的值就是1,但实际是这样子的,类在加载的连接阶段,将a初始化为默认值0int的默认值是0),然后在初始化阶段将a的值赋予为正确的初始值1.我们看到最终a的值是等于1,但是实际的运行中是有一个将0赋予a的过程,这个过程放生在连接的准备阶段。

四、自定义类加载器

 ClassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象。

 ClassLoader主要方法:

   1)Class loadClass( String name, boolean resolve ); ClassLoader.loadClass() 是 ClassLoader 的入口点  

  loadClass默认实现如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {        return loadClass(name, false);    }
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);                    } 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;        }    }
   findLoadedClass 充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法  

        从上面代码可以明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:

   首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
   也就是说,如果自定义类加载器,就必须重写findClass方法!

   i)findClass方法

    默认实现:

protected Class<?> findClass(String name) throws ClassNotFoundException {        throw new ClassNotFoundException(name);    }

  抽象类ClassLoader的findClass函数默认是抛出异常的。而前面我们知道,loadClass在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass函数,因此我们必须要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.如果是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为Class对象呢?很简单,Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象啦~

  2)defineClass 方法是 ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。假设class文件是加密过的,则需要解密后作为形参传入defineClass函数。

  默认实现:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)        throws ClassFormatError    {        return defineClass(name, b, off, len, null);    }

   3)示例    

public class TestClassLoader {    public void hello() {        System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass()                + " 加载进来的");    }}

 注意:

 如果你是直接在当前项目里面创建,待TestClassLoader.java编译后,请把TestClassLoader.class文件拷贝走,再将TestClassLoader.java删除。因为如果TestClassLoader.class存放在当前项目中,根据双亲委派模型可知,会通过sun.misc.Launcher$AppClassLoader 类加载器加载。为了让我们自定义的类加载器加载,我们把TestClassLoader.class文件放入到其他目录。

自定义类加载器

package test;import java.io.FileInputStream;import java.lang.reflect.Method;/** * Created on 2017/10/25. */public class OwnClassloader {    static class MyClassLoader extends ClassLoader {        private String classPath;        public MyClassLoader(String classPath) {            this.classPath = classPath;        }        private byte[] loadByte(String name) throws Exception {            name = name.replaceAll("\\.", "/");            FileInputStream fis = new FileInputStream(classPath + "/" + name                    + ".class");            int len = fis.available();            byte[] data = new byte[len];            fis.read(data);            fis.close();            return data;        }        protected Class<?> findClass(String name) throws ClassNotFoundException {            try {                byte[] data = loadByte(name);                return defineClass(name, data, 0, data.length);            } catch (Exception e) {                e.printStackTrace();                throw new ClassNotFoundException();            }        }    };    public static void main(String args[]) throws Exception {        MyClassLoader classLoader = new MyClassLoader("D:/test");        Class clazz = classLoader.loadClass("TestClassLoader");        Object obj = clazz.newInstance();        Method helloMethod = clazz.getDeclaredMethod("hello", null);        helloMethod.invoke(obj, null);    }}

    运行结果:

  

原创粉丝点击