类加载器

引入

一般开发都不会接触classloader的概念,但程序的运行却是建立在classloader之上的
为什么要有类加载器: java程序的启动首先就需要jvm去加载class
而代码中的对象创建类调用【new Person(); Autil.currentTime();】  都需要class被jvm加载
使用类的静态方法,或者创建对象前,需要先加载class
而每个class必然由一个classloader加载

方向

貌似类加载的事情不需要我们操心,虚拟机已经帮我们解决了
那学这个能干嘛: 
* 更了解虚拟机; 
* 在复杂的生产环境可能遇到class找不到异常,了解这个有助于排查错误;
* 自研服务/插件,动态加载代码;
* tomcat的项目代码热部署,jsp热部署;
* 各种热部署插件;
* 软件破解;

划重点

一个class在内存中只存一份,以及限定条件 defineClass决定class的类加载器是谁 线程上下文类加载器用于隐式传参 一个对象的类由哪个类加载器去加载,取决于显示使用类或显示使用类加载器

类的加载方式有哪些

也可以说类在哪些情况下会被加载

  • 以类名方式直接使用时必然会被加载

      new A()
      B.hello()
    
  • Class.forName

      Class.forName("com.mysql.jdbc.Driver")
      Class.forName("com.mysql.jdbc.Driver", false, myclassloader)
    
  • 直接调用myclassloader.loadClass(“xx.yy.Abc”)
    有些人的类加载器例子会这么写myclassloader.findClass(“xx.yy.Abc”),有时会出问题。

1
2
3
4
MyClassloader c1 = new MyClassloader();
MyClassloader c2 = new MyClassloader();
Class cp1 = Class.forName("fluffy.mo.Person", false, c1); 
Class cp2 = Class.forName("fluffy.mo.Person", false, c2); 

class在JVM中只能加载一份, 但此处cp1与cp2都会被加载,且二者内存地址不同。 Person的class在JVM中分为 姓(某Classloader对象)和名(类全名 fluffy.mo.Person) 此处二者加载的都是fluffy.mo.Person, 但二者姓不同,也就是c1和c2不同,所以冲突。

注意:
类的加载,与类的加载器可能会有所误解。 当一个Person类由classloaderA去加载时,classloaderA可能会通过父类委托给classloaderB。 如果classloaderB成功的加载了Person类,则Person的类加载器其实是classloaderB,而不是classloaderA。

判定Person类的类加载器是谁, 并不是依据classloaderA或classloaderB 调用了findClass方法。 而是看谁最终调用defineClass方法, 即调用defineClass方法的classloader才是Person类的类加载器。

关于类加载器的详细介绍 https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html

自定义类加载器-实例

主要步骤

  • 继承ClassLoader
  • 重写findClass()方法
  • 调用defineClass()方法
 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
import java.io.*;

class CustomClassLoader extends ClassLoader {
    final String basePath;

    public CustomClassLoader(String basePath) {
        this.basePath = basePath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException(name);
            }
            return defineClass(name, result, 0, result.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    private byte[] getClassFromCustomPath(String name) throws Exception {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = basePath + name.replace(".", "/") + ".class";
        try {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int len = 0;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } finally {
            try {
                in.close();
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        String myClassPath = "E:/myprogram/myclasses/";
        CustomClassLoader customClassLoader = new CustomClassLoader(myClassPath);
        Class<?> clazz = Class.forName("fluffy.mo.Person", true, customClassLoader);
        Object obj = clazz.newInstance();
        System.out.println(obj.getClass().getClassLoader());
    }
}

自带类加载器与双亲委托

委托机制可实现类共享,保证内存中只出现一次class字节码。
sun.misc.Launcher.ExtClassLoader
sun.misc.Launcher.AppClassLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
+----------------------+
| BootstrapClassloader |
|         +            |
|         |            |
|         v            |
|    ExtClassloader    |
|         +            |
|         |            |
|         v            |
|    AppClassloader    |
|         +            |
|         |            |
|         v            |
|   UserClassLoader    |
+----------------------+
某个类加载器在收到加载类的请求时,则先将加载类任务委托给父类加载器,层层向上请求。
如果父类加载器可以完成类加载任务,就成功返回;在父类加载器加载失败时,才自己去加载。

AppClassloader的父加载器为ExtClassloader
ExtClassloader的父类加载器为BootstrapClassloader 显示为null (因为BootstrapClassLoader是使用C++写的,没有java类)

委托机制是java程序默认的运行方式,但某些情况下我们的程序会不遵守该规则,比如tomcat加载的web应用。

BootstrapClassloader

该类c语言实现 主要用来加载java的核心jar包
如rt.jar、resources.jar、jce.jar等

ExtClassloader

扩展类加载器 默认加载JAVA_HOME/jre/lib/ext/目下的所有jar包

AppClassloader

为ClassLoader.getSystemClassLoader() 用来加载classpath下的class和jar包

如何自定义类加载器

通常来说是继承ClassLoader,重写findClass

如果不想破坏双亲委派机制,则只需要重写findClass方法
如果想打破双亲委派机制,那么就重写整个loadClass方法

ContextClassLoader

线程的上下文类加载器: 每个线程都会有一个classloader的引用保存在context中
java.lang.Thread有两个方法 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 

java程序启动时默认的ContextClassLoader为AppClassloader。
当创建新线程时,新线程的ContextClassLoader会取当前线程的ContextClassLoader。

用法为在某一个时刻为当前线程设置 contextClassLoader , 作为一个全局变量方便使用。 
然后在必要的时候取出该类加载器使用。

思考

能不能自己写个类叫java.lang.System

写自然可以写,但不会被JVM加载。

按照双亲委托机制,JVM在加载该自定义System类前,会先找父加载器,
最后找到`BootstrapClassloader`, `BootstrapClassloader`由c++编写,用于加载系统类,只会去特定位置加载Java核心库。
这样JVM最终加载的是自带的`java.lang.System`, 而不是自己写的`java.lang.System`。

那么可以自己写一个类加载器,并且绕开背双亲委托机制,直接加载自己的写的类呢。
还是不行,因为自定义类加载器在加载class二进制文件后,还是要调用java.lang.ClassLoader#defineClass方法。
深入源码发现其会调用preDefineClass方法,而java.lang.ClassLoader#preDefineClass方法拒绝加载以`java.`开头的类。

类名.class和getClass()的区别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class A{
    public void test() throws Exception {
        Class b1 = B.class;

        // Class cc = Class.forName("B", false, myclassloader);
        Class cc = Class.forName("B");
        Object obj_x = cc.newInstance();
        // Object obj_x = new B();
        Class b2 = obj_x.getClass();

        System.out.println(b1.equals(b2));
    }
}
class B{
}
以代码举例说明
类实例b1是直接以类名获取,其类加载器和代码所处位置有关, 也就是加载B的class时,会让A类加载器去加载。
类实例b2是通过obj_x. getClass()使用方法是间接获取,obj_x对象的类加载器,其父类加载器可以动态化。
b1是怎么加载的,直接看A的类加载器就行。
b2是怎么加载的,要从对象obj_x向上找。 
obj_x如果是new出来的,那也是看A的类加载器。
obj_x如果是cc.newInstance()出来的,那b2和cc相同,找b2就是找cc。
而cc从哪加载的,就要看使用的什么类加载器了。

ContextClassLoader, new, Class.forName, 自定义ClassLoader 之间的关系

1
2
3
Person p1 = new Person();
Class cp = p1.getClass();
ClassLoader cdr = cp.getClassLoader();

以【Person的对象p1 与 p1的class对象】为例说明 在创建p1对象前,JVM会先加载p1的class,即cp。 而cp一定由某个类加载器加载,此处cdr为cp的类加载器。

p1可以由两种方式被创建 new 和 .newInstance() 。 当采用 new 创建的对象p1,p1的class对象cp的类的加载,会由 而 cp.newInstance() 其实是两步 class对象cp被加载 与使用class对象cp创建对象p1 。 这里只需要关心第一步,class对象cp是如何被加载,就足够了。

cp可以由多种渠道获取。
cp最初一定是从类加载器获取的,但从出厂,到消费者手中,中间的过程可以很曲折。

 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
package fluffy.mo;

public class Person {
}
class Test {
    static Class cp;

    static {
        // v1版本
        try {
            Person p1 = new Person();
            cp = p1.getClass();
            cp = Person.class;
            cp = Class.forName("fluffy.mo.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        // v2版本
        try {
            ClassLoader cdr = null;
            cp = Class.forName("fluffy.mo.Person", false, cdr);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        // v3版本
        try {
            // v3-before
            ClassLoader cdrA = null;
            Thread.currentThread().setContextClassLoader(cdrA);

            // v3-body
            ClassLoader cdr = Thread.currentThread().getContextClassLoader();
            cp = Class.forName("fluffy.mo.Person", false, cdr);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
在v1版本下,没有显示指定用某个类加载器。
代码写在Test中,则会找Test的类加载器去加载Person的class。

在v2版本下,显示指定用某个ClassLoader,则自然是用我们指定的去加载Person的class。

在v3版本下,对v2版本升级,代码分成两部分。 
在某一时刻,将一个myClassloader放在了当前线程的context中。
在必要的时候,从当前线程的context中取出该 classLoader为我们所用