ClassLoad详解

1.什么是ClassLoader?

大家都知道,当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

2.Java类装载过程

装载:通过类的全限定名获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构,在内存中生成Java.lang.class对象;

 

链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;

  校验:检查导入类或接口的二进制数据的正确性;(文件格式验证,元数据验证,字节码验证,符号引用验证)

准备:给类的静态变量分配并初始化存储空间;
  解析:将常量池中的符号引用转成直接引用;

初始化:激活类的静态变量的初始化Java代码和静态Java代码块,并初始化程序员设置的变量值。

 

3.ClassLoader加载类的原理

3.1、原理介绍

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。获取父类加载器通过getParent方法

3.2、为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

 3.3、 但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确:

1)、在web服务器上建一个org.classloader.simple.NetClassLoaderSimple.java类

1

2

3

4

5

6

7

8

9

10

package org.classloader.simple; 

    

public class NetClassLoaderSimple { 

        

    private NetClassLoaderSimple instance; 

    

    public void setNetClassLoaderSimple(Object obj) { 

        this.instance = (NetClassLoaderSimple)obj; 

    } 

}

org.classloader.simple.NetClassLoaderSimple类的setNetClassLoaderSimple方法接收一个Object类型参数,并将它强制转换成org.classloader.simple.NetClassLoaderSimple类型。

2)、测试两个class是否相同(NetWorkClassLoader.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

package classloader; 

    

public class NewworkClassLoaderTest { 

    

    public static void main(String[] args) { 

        try { 

            //测试加载网络中的class文件 

            String rootUrl = "http://localhost:8080/httpweb/classes"; 

            String className = "org.classloader.simple.NetClassLoaderSimple"; 

            NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl); 

            NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl); 

            Class<?> clazz1 = ncl1.loadClass(className); 

            Class<?> clazz2 = ncl2.loadClass(className); 

            Object obj1 = clazz1.newInstance(); 

            Object obj2 = clazz2.newInstance(); 

            clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2); 

        } catch (Exception e) { 

            e.printStackTrace(); 

        } 

    } 

        

}

首先获得网络上一个class文件的二进制名称,然后通过自定义的类加载器NetworkClassLoader创建两个实例,并根据网络地址分别加载这份class,并得到这两个ClassLoader实例加载后生成的Class实例clazz1和clazz2,最后将这两个Class实例分别生成具体的实例对象obj1和obj2,再通过反射调用clazz1中的setNetClassLoaderSimple方法。

3)、查看测试结果

结论:从结果中可以看出,虽然是同一份class字节码文件,但是由于被两个不同的ClassLoader实例所加载,所以JVM认为它们就是两个不同的类。

3.4、ClassLoader的体系架构:

系统提供的类加载器主要有下面三个: 

(1)启动类加载器(Bootstrap ClassLoader)--它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader; 

这个类加载器负责将存放在JAVA_HOME/lib下的,或者被-Xbootclasspath参数所指定的路径中的,并且是被虚拟机识别的类库(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)加载到虚拟机内存中;启动类加载器无法被Java程序直接引用;

(2)扩展类加载器(Extension ClassLoader)--它用来加载 Java 的扩展库; 

 这个加载器负责加载JAVA_HOME/lib/ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;

(3)应用类加载器(Application ClassLoader)--它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类; 

这个加载器是ClassLoader中getSystemClassLoader()方法的返回值(可以通过此方法获取),所以一般也称它为系统类加载器。它负责加载用户类路径(Classpath)上所指定的类库,可直接使用这个加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器;

 

 

 

我们可以看到getParent()实际上返回的就是一个ClassLoader对象parentparent的赋值是在ClassLoader对象的构造方法中,它有两个情况: 
1.
由外部类创建ClassLoader时直接指定一个ClassLoaderparent 
2.
getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader

 

测试3:用Bootstrcp ClassLoader来加载ClassLoaderTest.class,有两种方式

1、在jvm中添加-Xbootclasspath参数,指定Bootstrcp ClassLoader加载类的路径,并追加我们自已的jar(ClassTestLoader.jar)

2、将class文件放到JAVA_HOME/jre/classes/目录下(上面有提到)

方式1:(我用的是Eclipse开发工具,用命令行是在java命令后面添加-Xbootclasspath参数)

打开Run配置对话框:

配置好如图中所述的参数后,重新运行程序,产的结果如下所示:(类加载的过程,只摘下了一部份)

打印结果:

[Loaded classloader.ClassLoaderTest from C:\Program Files\Java\jdk1.6.0_22\jre\classes] 

null  //这是打印的结果 

C:\Program Files\Java\jdk1.6.0_22\jre\lib\resources.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\rt.jar; 

C:\Program Files\Java\jdk1.6.0_22\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\jsse.jar; 

C:\Program Files\Java\jdk1.6.0_22\jre\lib\jce.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\charsets.jar; 

C:\Program Files\Java\jdk1.6.0_22\jre\classes;c:\ClassLoaderTest.jar   

//这一段是System.out.println(System.getProperty("sun.boot.class.path"));打印出来的。这个路径就是Bootstrcp ClassLoader默认搜索类的路径 

[Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.6.0_22\jre\lib\rt.jar] 

[Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jdk1.6.0_22\jre\lib\rt.jar]

 

方式2:将ClassLoaderTest.jar解压后,放到JAVA_HOME/jre/classes目录下,如下图所示:

提示:jre目录下默认没有classes目录,需要自己手动创建一个

打印结果:

从结果中可以看出,两种方式都实现了将ClassLoaderTest.class由Bootstrcp ClassLoader加载成功了。

4.定义自已的ClassLoader

4.1. ClassLoader包含的内容

每个ClassLoader对象都是一个java.lang.ClassLoader的实例。每个Class对象都被这些ClassLoader对象所加载,通过继承java.lang.ClassLoader可以扩展出自定义ClassLoader,并使用这些自定义的ClassLoader对类进行加载。

先大体了解一下ClassLoader的API:

package java.lang;

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();

findLoadedClass(String name)

findClass(String name)

}

    最重要的是ClassLoader的loadClass方法,它接受一个全类名,然后返回一个Class类型的实例。

    defineClass方法接受一组字节,然后将其具体化为一个Class类型实例,它一般从磁盘上加载一个文件,然后将文件的字节传递给JVM,通过JVM(native 方法)对于Class的定义,将其具体化,实例化为一个Class类型实例。

    getParent方法返回其parent ClassLoader

getResourcegetResources方法,从给定的repository中查找URLs,同时它们也具备类似loadClass一样的代理机制,我们可以将loadClass视为:defineClass(getResource(name).getBytes())

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

findClass(String name): 找名称为 name的类,返回的结果是 java.lang.Class类的实例。自定义时需要重写的方法

    Java由于其晚绑定和"解释型"的特性,类型的加载是到最晚才进行,一个类型直到被调用构造函数、静态方法或者在字段上使用时才会被加载。

考虑如下代码:

public class A {

public void doSomething() {

B b = new B();

b.doSomethingElse();

}

}

代码:B b = new B();等同于B b = Class.forName("B", false, A.class.getClassLoader()).newInstance();

这代表着,在类型A中使用到的类型,B由加载了类型A的类加载器来进行加载

 

虽然在绝大多数情况下,系统默认提供的类加载器已经可以满足需求。但是在某些情况下,还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。

可以参考扩展类加载器和应用类加载器,可以分别重写ClassLoader中的loadClass方法和findClass方法,但是重写不同的方法,就像扩展类加载器和应用类加载器一样,要达到的目的是不一样的

4.2继承ClassLoad

 

4.1.2重写findClass方法(符合代理模式,保证具有相同全名的类不会被重复加载到JVM中

自定义的类加载器的类继承Java.lang.ClassLoader并覆盖其中的findClass方法Java.lang.ClassLoader的loadClass方法封装了代理模式的实现(为了保证类加载器都能正确实现代理模式,最好不要复写此方法),该方法会首先调用findLoadedClass方法来检查该类是否被加载过,如果没有加载过的话,会一级一级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类加载器去进行真正的加载,直至回退到最初的类加载器(如果它自己也不能完成类的加载,那就应报告ClassNoFoundException异常),调用findClass方法在硬盘上来查找该类的字节码文件(.class文件),然后读取该文件内容,最后通过defineClass方法来把这些字节码(返回)转换成java.lang.Class的实例;

4.2.3重写loadClass方法

如果要想在JVM的不同类加载器中保留具有相同全限定名的类,那就要通过重写loadClass来实现,此时首先是通过用户自定义的类加载器来判断该类是否可加载,如果可以加载就由自定义的类加载器进行加载,如果不能够加载才交给父类加载器去加载。

这种情况下,就有可能有大量相同的类,被不同的自定义类加载器加载到JVM中,并且这种实现方式是不符合双亲委派模型。但是不能够说这种实现方式就一定是错误的,有可能当前的场景就需要这样的方式,如容器插件应用场景就适合。

一个插件容器,如下图所示:

 

要允许不同的插件增加到容器中,就需要采用这种方式,因为我们没有办法保证不同的插件中不能够有相同全限定名的类存在,如A插件中存在了test.Test这么一个类,B插件中也可能会有这么一个相同的类,不能说A插件的test.Test类被加载了,B插件的test.Test就不可再加载了,这就会导致B/A插件工作不正常,因为这两个不同的类实现的功能可能完全不同,或者可以说他们之间是一点关系都没有。

但是如果实现自定义类的场景不是类似上面的插件容器场景,最好还是实现findClass,这个也是Sun推荐的实现方式,并且它是符合双亲委派模型的。

 

示例:自定义一个NetworkClassLoader,用于加载网络上的class文件

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

package classloader; 

    

import java.io.ByteArrayOutputStream; 

import java.io.InputStream; 

import java.net.URL; 

    

/**

 * 加载网络class的ClassLoader

 */ 

public class NetworkClassLoader extends ClassLoader { 

        

    private String rootUrl; 

    

    public NetworkClassLoader(String rootUrl) { 

        this.rootUrl = rootUrl; 

    } 

    

    @Override 

    protected Class<?> findClass(String name) throws ClassNotFoundException { 

        Class clazz = null;//this.findLoadedClass(name); // 父类已加载    

        //if (clazz == null) {  //检查该类是否已被加载过 

            byte[] classData = getClassData(name);  //根据类的二进制名称,获得该class文件的字节码数组 

            if (classData == null) { 

                throw new ClassNotFoundException(); 

            } 

            clazz = defineClass(name, classData, 0, classData.length);  //将class的字节码数组转换成Class类的实例 

        //}  

        return clazz; 

    } 

    

    private byte[] getClassData(String name) { 

        InputStream is = null; 

        try { 

            String path = classNameToPath(name); 

            URL url = new URL(path); 

            byte[] buff = new byte[1024*4]; 

            int len = -1; 

            is = url.openStream(); 

            ByteArrayOutputStream baos = new ByteArrayOutputStream(); 

            while((len = is.read(buff)) != -1) { 

                baos.write(buff,0,len); 

            } 

            return baos.toByteArray(); 

        } catch (Exception e) { 

            e.printStackTrace(); 

        } finally { 

            if (is != null) { 

               try { 

                  is.close(); 

               } catch(IOException e) { 

                  e.printStackTrace(); 

               } 

            } 

        } 

        return null; 

    } 

    

    private String classNameToPath(String name) { 

        return rootUrl + "/" + name.replace(".", "/") + ".class"; 

    } 

    

}

测试类:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

package classloader; 

    

public class ClassLoaderTest { 

    

    public static void main(String[] args) { 

        try { 

            /*ClassLoader loader = ClassLoaderTest.class.getClassLoader();  //获得ClassLoaderTest这个类的类加载器

            while(loader != null) {

                System.out.println(loader);

                loader = loader.getParent();    //获得父加载器的引用

            }

            System.out.println(loader);*/ 

                

    

            String rootUrl = "http://localhost:8080/httpweb/classes"; 

            NetworkClassLoader networkClassLoader = new NetworkClassLoader(rootUrl); 

            String classname = "org.classloader.simple.NetClassLoaderTest"; 

            Class clazz = networkClassLoader.loadClass(classname); 

            System.out.println(clazz.getClassLoader()); 

                

        } catch (Exception e) { 

            e.printStackTrace(); 

        } 

    } 

        

}

打印结果:

 

上面是方法原型,一般实现这个方法的步骤是 
1.
findLoadedClass(String)去检测这个class是不是已经加载过了 
2.
执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoaderparentnull,但仍然说Bootstrap ClassLoader是它的父加载器。 
3.
如果向上委托父加载器没有加载成功,则通过findClass(String)查找。

 

4.3 数据库链接为什么使用Class.forName(className)

JDBC  Driver源码如下,因此使用Class.forName(classname)才能在反射回去类的时候执行static块。

static {

try {

java.sql.DriverManager.registerDriver(new Driver());

} catch (SQLException E) {

thrownew RuntimeException("Can't register driver!");

}

}

 

 

4.5 自定义ClassLoader能做什么

  1. 定义加密解密协议
  2. Context ClassLoader 线程上下文类加载器
  3. Context ClassLoader的运用时机

5.加载类

5.1在代码中显示加载某个类有3种方法

this.getClass( ).getClassLoader( ).loadClass( );----> aaa.getClass().getClassLoader().LoadClass("com.B");

Class.forName(className ) ;-------------> Class.forName("com.B");

MyClassLoader.findClass( ); //自定义ClassLoad

补充:反射获取Class

  1. Class bClass = Class.forName("com.B");
  2. Class bClass = B.class;
  3. Class bClass = b.getClass();

5.1分析 Class.forName()和ClassLoader.loadClass

Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);

第2个boolean参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。
一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化。

 

ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);

第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以,
不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行

 

感谢一下博客:

https://blog.csdn.net/ziji3810111/article/details/71122036

http://www.importnew.com/21189.html

http://ifeve.com/classloader/

反射

https://blog.csdn.net/sinat_38259539/article/details/71799078

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值