类加载机制

自定义ClassLoader可以做什么

  1. 热部署。(在不重启程序的情况下动态替换类的实现)
  2. 应用的模块化和相互隔离。(不同的ClassLoader可以加载相同的类但是相互隔离、互不影响)
  3. 从不同的地方灵活加载。(系统默认的ClassLoader一般从本地的.class文件或者jar文件中加载字节码文件,通过自定义的ClassLoader,我们可以从共享的web服务器、数据库、缓存服务器等其他地方加载字节码)

类的基本加载过程和机制

负责加载类的我们称作类加载器,输入的是类名,输出的是Class对象。类加载器不只有一个,一般程序运行时会有三个。

  1. 启动类加载器(Bootstrap ClassLoader)这个加载器是java虚拟机的一部分,一般是由C++实现,负责加载Java的基础类,主要加载/lib/rt.jar。
  2. 扩展类加载器(Extension ClassLoader)这个加载器的实现类是sun.misc.Launcher$ExtClassLoader,它负责加载java的扩展类,主要是/lib/ex/*中的jar包。
  3. 应用程序类加载器(Application ClassLoader)这个加载器的实现类是sun.misc.Launcher$ClassLoade,它负责加载应用程序的类,包括自己写的和第三方类库。

三者的关系:可以认为三者是父子关系:即Application ClassLoader的父类是Extension ClassLoader,Extension ClassLoader的父类是Bootstrap Classloader
但是不是父子继承关系,而是父子委派关系,子ClassLoader有个变量parent指向父ClassLoader,在Class-Loader加载类的时候一般先通过夫ClassLoader加载。

  1. 判断是否已经加载,如果已经加载过了,直接返回Class对象,一个类只会被一个Class-Loader加载一次。
  2. 如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
  3. 在父ClassLoader没有加载成功的前提下,自己尝试加载类。

上述加载过程一般被称作双亲委派模型,也就是优先让父ClassLoader加载。这么做的原因为避免Java类库被覆盖。双亲委派虽然是一般模型,但是也有例外:

  1. 自定义加载顺序:自定义的ClassLoader可以不遵从“双亲委派”约定,不过即使不遵从也不能加载以java开头的类,这是由java的安全机制保证的。
  2. 网络加载顺序:在OSGI框架中,类加载器之间的关系是一个网,每个模块都有一个加载器,不同模块之间可能有依赖关系,即加载一个类的时候可能自己加载也可能委派给别的加载器加载。
  3. 父加载器委派给子加载器加载:例如JDNI服务。

一个程序运行时,一般会创建一个Application ClassLoader,在程序中用到ClassLoader的地方,如果没有指定,那么一般用的就是这个ClassLoader,所以Application ClassLoader也被称作系统类加载器。

理解ClassLoader

类ClassLoader是一个抽象类,Application ClassLoader和Extension ClassLoader的具体实现类分别是sun.misc.Launcher$AppClassLoader和sin.misc.Launcher$ExtClassLoader,BootStrapClassLoader
不是java实现的所以没有对应的类。
每个Class对象多有获取自己的加载器的方法:public ClassLoader getClassLoader()
ClassLoader可以获取父ClassLoader:public final ClassLoader getParent()
如果ClassLoader是Bootstrap ClassLoader返回值为null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ClassLoaderDemo{
public static main(String [] args){
ClassLoaer cl1=ClassLoaderDemo.class.getClassLoaer();
while(cl1!=null){
System.out.println(c1.getClass.getName();
c1=cl1.getParent();
}
System.out.println(String.class.getClassLoaer());
}
}
//ruselt
//sun.misc.Launcher$AppClassLoader
//sun.misc.Launcher$ExtClassLoader
//null

ClassLoader可以获取默认的系统类加载器:public static ClassLoader getSystemClassLoader()
ClassLoader用于加载类的主要方法:public Class<?> loadClass(String name) throws ClassNotFoundException
ClassLoader的loadClass方法
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
public Class<T> loadClass(String name) throws ClassNotFoundException{
return loadClass(name , false);
}
//loadClass(String name,boolean flag)的主要代码
public Class<T> loadClass(String name,boolean resolve) throws ClassNotFoundException{
synchronized(getClassLoadingLock(name)){
//检查是否已经被加载
Class c=findLoadedClass(name);
if(c==null){
//没有被加载,先委派父ClassLoader或者BootStrapLoader加载
try{
if(parent!=null){
//委派父ClassLoader,resolve参数固定为false
c=parent.loadClass(name,false);
}else{
c=findBootStrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//没找到,捕获异常,然后尝试自己加载
}
if(c==null){
//自己加载,findClass才是当前的ClassLoader真正的加载方法
c=findClass(name);
}
}
if(resolve){
//链接,执行static语句块
resolveClass(c);
}
return c;
}
}

类加载的应用:可配置策略

通常可以通过classLoader的loadClass或者Class.forName自己加载类,但是什么情况下需要自己加载呢?

大多数应用使用面向接口编程,接口的具体实现类可能很多,适用于不同的场景,具体使用那个实现类在配置文件中配置,通过更改配置,不用改变代码,就可以改变程序的行文,在设计模式中,这个算是一种策略模式。

Demo


定义接口TestService

1
2
3
public interface TestService{
public void action();
}

定义ConfigurableStrategyDemo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class  ConfigurableStrategyDemo{
public static TestService CreateService(){
try{
Properties prop =new Properties();
String fileName="filepath/config.properties";
prop.load(new FileInputStream (fileName));
String className=prop.getProperty("service");
Class<?> cls=Classs.forName(className);
return (TestService)cls.newInstance();
}catchd(Excepiton e){
throw new RuntimeException(e);
}
}
public static void main(String [] args){
TestService service=CreateService();
service.action();
}
}

config.properties的内容
service=io.caoxx123.service.TestService

自定义ClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ClassLoaderDemo extends ClassLoader{
privte static final String BASE_DIR="io/caoxx123/"
@Overrride
protected Class<?> findClass(String name) throws ClassNotFoundException{
String filename=name.replaceAll("\\","/");
filename=BASE_DIR+filename+".class";
try{
byte[] bytes=BinaryFileUtiles.readFileToByteArray(filename);
return defineClass(name,bytesm,0,bytes.length);
}catch (IOException ex){
throw new ClassNotFoundException("faile to load class"+name,ex);
}
}
}

上述代码并没有指定父加载器,默认的是ClassLoaer.getSystemClassLoader()的返回值。如果想要指定父加载器可以重写

1
protected ClassLoader(ClassLoader parent)

classLoaderDemo可以拿到同一个类的不同Class对象:

1
2
3
4
5
6
7
8
9
10
11
12
public class TestClassLoader(){
public static void main(String [] args){
String className="io.caoxx123.service.TestService";
ClassLoaderDemo cl1=new ClassLoaderDemo();
Class<?> class1=cl1.loadClass(className);
ClassLoaderDemo cl2=new ClassLoaderDemo();
Class<?> class2=cl2.loadClass(className);
if(!class1==class2){
System.out.println("different");
}
}
}

上述例子可以看到,虽然class1、class2的类名是一样的,但是却不是同一个对象。通过这种可以实现

  1. 隔离:一个复杂的程序,内部可能是按模块,不同的模块可能使用的同一个类。
  2. 热部署:使用同一个ClassLoader,类只会被加载一次,加载后,即使class文件发生变化,再次加载,得到的还是原来的ClassLoader,而使用ClassLoaderDemo,则可以先创建一个新的ClassLoader,再用它加载Class得到的Class对象就是新的。
    热加载的实现:

  3. 定义接口

    1
    2
    3
    public interface TestService{
    public void sayTest();
    }
  4. HotDeployDemo
    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
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    public class HotDeployDemo {

    private static final String CLASS_NAME = "io.caoxx123.TestImpl";
    private static final String FILE_NAME = "test/"
    +CLASS_NAME.replaceAll("\\.", "/")+".class";
    private static volatile TestService TestService;

    public static TestService getTestService() {
    if (TestService != null) {
    return TestService;
    }
    synchronized (HotDeployDemo.class) {
    if (TestService == null) {
    TestService = createTestService();
    }
    return TestService;
    }
    }

    private static TestService createTestService() {
    try {
    MyClassLoader cl = new MyClassLoader();
    Class<?> cls = cl.loadClass(CLASS_NAME);
    if (cls != null) {
    return (TestService) cls.newInstance();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    return null;
    }

    public static void client() {
    Thread t = new Thread() {
    @Override
    public void run() {
    try {
    while (true) {
    TestService TestService = getTestService();
    TestService.sayHello();
    Thread.sleep(1000);
    }
    } catch (InterruptedException e) {
    }
    }
    };
    t.start();
    }

    public static void monitor() {
    Thread t = new Thread() {
    private long lastModified = new File(FILE_NAME).lastModified();

    @Override
    public void run() {
    try {
    while (true) {
    Thread.sleep(100);
    long now = new File(FILE_NAME).lastModified();
    if (now != lastModified) {
    lastModified = now;
    reloadTestService();
    }
    }
    } catch (InterruptedException e) {
    }
    }
    };
    t.start();
    }

    public static void reloadTestService() {
    TestService = createTestService();
    }

    public static void main(String[] args) {
    monitor();
    client();
    }

    }
文章作者: Anders Cao
文章链接: http://yoursite.com/2019/05/12/类的加载机制/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Anders's Blog
打赏
  • 微信
  • 支付寶