服务器之家

服务器之家 > 正文

JAVA提高第七篇 类加载器解析

时间:2021-01-28 12:29     来源/作者:pony1223

今天我们学习类加载器,关于类加载器其实和jvm有很大关系,在这里这篇文章只是简单的介绍下类加载器,后面学习到jvm的时候还会详细讲到类加载器,本文分为下面几个小节讲解:

一、认识类加载器

1.什么是类加载器?

所谓的类加载器可以从其作用来理解,其功能就是将classpath目录下.class文件,加载到内存中来进行一些处理,处理完的结果就是一些字节码.那是谁把这些class类加载到内存中来的呢?就是类加载器。

2.jvm中默认的类加载器有哪些?

java虚拟机中可以安装多个类加载器,系统默认三个主要的类加载器,每个类加载器负责加载不同位置的类:bootstrap,extclassloader,appclassloader

注意的是:

1.类加载器本身也是一个java类,因为类加载器本身也是一个java类,那么这个特殊的java类【类加载器】是有谁加载进来的呢?这显然要有第一个类加载器,这第一个类加载器不是一个java类,它是bootstrap。

2.bootstrap不是一个java类,不需要类加载器java加载,他是嵌套在java虚拟机内核里面的。java 虚拟机内核已启动的时候,他就已经在那里面了,他是用c++语言写的一段二进制代码。他可以去加载别的类,其中别的类就包含了类加载器【如上面提到的ext  和 app】。

案例:

下面我们写个例子来获取classloadertest这个类的类加载器的名字,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package study.javaenhance;
 
import java.util.arraylist;
 
public class classloadertest
{
  public static void main(string[] args) throws exception
  {
    //获取类加载器,那么这个获取的是一个实例对象,我们知道类加载器也有很多种,那么因此也有其对应的类存在,因此可以获取到对应的字节码
    system.out.println(classloadertest.class.getclassloader());
    //获取类加载的字节码,然后获取到类加载字节码的名字
    system.out.println(classloadertest.class.getclassloader().getclass().getname());
    //下面我们看下获取非我们定义的类,比如system arraylist 等常用类
    system.out.println(system.class.getclassloader());
    system.out.println(arraylist.class.getclassloader());
    
    
  }
 
}

结果如下:

sun.misc.launcher$appclassloader@1c78e57
sun.misc.launcher$appclassloader
null
null

结果分析:

classloadertest的类加载器的名称是appclassloader。也就是这个类是由appclassloader这个类加载器加载的。
system/arraylist的类加载器是null。这说明这个类加载器是由bootstrap加载的。因为我们上面说了bootstrap不是java类,不需要类加载器加载。所以他的类加载器是null。
==================================
我们说了java给我们提供了三种类加载器:bootstrap,extclassloader,appclassloader。这三种类加载器是有父子关系组成了一个树形结构。bootstrap是根节点,bootstrap下面挂着extclassloader,extclassloader下面挂着appclassloader.

代码演示如下:

?
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
package study.javaenhance;
 
import java.util.arraylist;
 
public class classloadertest
{
  public static void main(string[] args) throws exception
  {
    //获取类加载器,那么这个获取的是一个实例对象,我们知道类加载器也有很多种,那么因此也有其对应的类存在,因此可以获取到对应的字节码
    system.out.println(classloadertest.class.getclassloader());
    //获取类加载的字节码,然后获取到类加载字节码的名字
    system.out.println(classloadertest.class.getclassloader().getclass().getname());
    //下面我们看下获取非我们定义的类,比如system arraylist 等常用类
    system.out.println(system.class.getclassloader());
    system.out.println(arraylist.class.getclassloader());
    
    
    //演示java 提供的类加载器关系
    classloader classloader = classloadertest.class.getclassloader();
    while(classloader != null)
    {
      system.out.print(classloader.getclass().getname()+"-->");
      classloader = classloader.getparent();
    }
    system.out.println(classloader);
    
  }
 
}

输出结果为:

sun.misc.launcher$appclassloader-->sun.misc.launcher$extclassloader-->null

通过这段程序可以看出来,classloadertest由appclassloader加载,appclassloader的父类节点是extclassloader,extclassloader的父节点是bootstrap。

JAVA提高第七篇 类加载器解析

每一个类加载器都有自己的管辖范围。 bootstrap根节点,只负责加载rt.jar里的类,刚刚那个system就是属于rt.jar包里面的,extclassloader负责加载jre/lib/ext/*.jar这个目录文件夹下的文件。而appclassloader负责加载classpath目录下的所有jar文件及目录。
最后一级是我们自定义的加载器,他们的父类都是appclassloader。

二、类加载器的双亲委派机制

除了系统自带了类加载器,我们还可以自定义类加载器。然后把自己的类加载器挂在树上。作为某个类加载器的孩子。所有自定义类加载器都要继承classloader。实现里面的一个方法classloader()如下:

JAVA提高第七篇 类加载器解析

通过上面的知识,我们知道java提供了三个类加载器,而且我们也可以自定义类加载器,并且通过上面的类加载图也看到了之前的关系,那么对于一个类的.class 到底是谁去加载呢?

当java虚拟机要加载第一个类的时候,到底派出哪个类加载器去加载呢?

(1). 首先当前线程的类加载器去加载线程中的第一个类(当前线程的类加载器:thread类中有一个get/setcontextclassloader(classloader cl);方法,可以获取/指定本线程中的类加载器)

(2). 如果类a中引用了类b,java虚拟机将使用加载类a的类加载器来加载类b

(3). 还可以直接调用classloader.loadclass(string classname)方法来指定某个类加载器去加载某个类

每个类加载器加载类时,又先委托给其上级类加载器当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则会抛出classnotfoundexception,不是再去找发起者类加载器的儿子,因为没有getchild()方法。例如:如上图所示: myclassloader->appclassloader->ext->classloader->bootstrap.自定定义的myclassloader1首先会先委托给appclassloader,appclassloader会委托给extclassloader,extclassloader会委托给bootstrap,这时候bootstrap就去加载,如果加载成功,就结束了。如果加载失败,就交给extclassloader去加载,如果extclassloader加载成功了,就结束了,如果加载失败就交给appclassloader加载,如果加载成功,就结束了,如果加载失败,就交给自定义的myclassloader1类加载器加载,如果加载失败,就报classnotfoundexception异常,结束。

这样的好处在哪里呢?可以集中管理,不会出现多份字节码重复的现象。有两个类要再在system,如果让底层的类加载器加载,可能会出现两份字节码。而都让爷爷加载,爷爷加载到已有,当再有请求过来的时候,爷爷说:哎,我加载过啊,直接把那份拿出来给你用啊。就不会出现多份字节码重复的现象。

现在有一道面试题:能不能自己写一套java.lang.system.?

分析:你写了也白写,因为类加载器加载,直接到爷爷那里去找,找成功了,分本就不回来理你的那个。
答案:通常不可以,因为委托机制委托给爷爷,爷爷在rt.jar包加载到这个类以后就不会加载你自己写了那个system类了。但是,我也有办法加载,我写一个自己的类加载器,不让他用委托机制,不委托给上级了,就可以了.

因为system类,list,map等这样的系统提供jar类都在rt.jar中,所以由bootstrap类加载器加载,因为bootstrap是祖先类,不是java编写的,所以打印出class为null

对于classloadertest类的加载过程,打印结果也是很清楚的。

三、自定义类加载器

下面来看一下怎么定义我们自己的一个类加载器myclassloader:

自定义的类加载器必须继承抽象类classloader然后重写findclass方法,其实他内部还有一个loadclass方法和defineclass方法,这两个方法的作用是:

loadclass方法的源代码:

?
1
2
3
public class<?> loadclass(string name) throws classnotfoundexception {
    return loadclass(name, false);
  }

再来看一下loadclass(name,false)方法的源代码:

?
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
protected class<?> loadclass(string name, boolean resolve)throws classnotfoundexception{
     //加上锁,同步处理,因为可能是多线程在加载类
     synchronized (getclassloadinglock(name)) {
       //检查,是否该类已经加载过了,如果加载过了,就不加载了
       class c = findloadedclass(name);
       if (c == null) {
         long t0 = system.nanotime();
         try {
           //如果自定义的类加载器的parent不为null,就调用parent的loadclass进行加载类
           if (parent != null) {
             c = parent.loadclass(name, false);
           } else {
             //如果自定义的类加载器的parent为null,就调用findbootstrapclass方法查找类,就是bootstrap类加载器
             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();
           //如果parent加载类失败,就调用自己的findclass方法进行类加载
           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;
     }
   }

在loadclass代码中也可以看到类加载机制的原理,这里还有这个方法findbootstrapclassornull,看一下源代码:

?
1
2
3
4
5
6
private class findbootstrapclassornull(string name)
  {
    if (!checkname(name)) return null;
 
    return findbootstrapclass(name);
  }

就是检查一下name是否是否正确,然后调用findbootstrapclass方法,但是findbootstrapclass方法是个native本地方法,看不到源代码了,但是可以猜测是用bootstrap类加载器进行加载类的,这个方法我们也不能重写,因为如果重写了这个方法的话,就会破坏这种委托机制,我们还要自己写一个委托机制。

defineclass这个方法很简单就是将class文件的字节数组编程一个class对象,这个方法肯定不能重写,内部实现是在c/c++代码中实现的findclass这个方法就是根据name来查找到class文件,在loadclass方法中用到,所以我们只能重写这个方法了,只要在这个方法中找到class文件,再将它用defineclass方法返回一个class对象即可。

这三个方法的执行流程是:每个类加载器:loadclass->findclass->defineclass

前期的知识了解后现在就来实现了

首先来看一下需要加载的一个类:classloaderattachment.java:

?
1
2
3
4
5
6
7
8
9
10
package study.javaenhance;
 
public class classloaderattachment {
  @override
  public string tostring() {
    return "hello classloader!";
    
  }
 
}

这个类中输出一段话即可:编译成classloaderattachment.class

再来看一下自定义的myclassloader.java:

 

?
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
package study.javaenhance;
 
import java.io.bytearrayoutputstream;
import java.io.fileinputstream;
import java.io.fileoutputstream;
import java.io.inputstream;
import java.io.outputstream;
 
public class myclassloader extends classloader
{
  //需要加载类.class文件的目录
  private string classdir;
   
  //无参的构造方法,用于class.newinstance()构造对象使用
  public myclassloader(){
  }
   
  public myclassloader(string classdir){
    this.classdir = classdir;
  }
 
  @override
  protected class<?> findclass(string name) throws classnotfoundexception {
    system.out.println(name);
    string classpathfile = classdir + "\\" + name.substring(name.lastindexof(".")+1) + ".class";
    system.out.println(classpathfile);
    try
    {
      system.out.println("my");
       //将class文件进行解密
      fileinputstream fis = new fileinputstream(classpathfile);
      bytearrayoutputstream bos = new bytearrayoutputstream();
      encodeanddecode(fis,bos);
      byte[] classbyte = bos.tobytearray();
      //将字节流变成一个class
      return defineclass(classbyte,0,classbyte.length);
    } catch (exception e)
    {
      e.printstacktrace();
    }
    
    return super.findclass(name);
  }
  
   //测试,先将classloaderattachment.class文件加密写到工程的class_temp目录下
  public static void main(string[] args) throws exception{
    //配置运行参数
    
    string srcpath = args[0];//classloaderattachment.class原路径
    string despath = args[1];//classloaderattachment.class输出的路径
    string desfilename = srcpath.substring(srcpath.lastindexof("\\")+1);
    string despathfile = despath + "/" + desfilename;
    fileinputstream fis = new fileinputstream(srcpath);
    fileoutputstream fos = new fileoutputstream(despathfile);
    //将class进行加密
    encodeanddecode(fis,fos);
    fis.close();
    fos.close();
  }
  
  
 
   /**
   * 加密和解密算法
   * @param is
   * @param os
   * @throws exception
   */
  private static void encodeanddecode(inputstream is,outputstream os) throws exception{
    int bytes = -1;
    while((bytes = is.read())!= -1){
      bytes = bytes ^ 0xff;//和0xff进行异或处理
      os.write(bytes);
    }
  }
}

这个类中定义了一个加密和解密的算法,很简单的,就是将字节和oxff异或一下即可,而且这个算法是加密和解密的都可以用!

当然我们还要先做一个操作就是,将classloaderattachment.class加密后的文件存起来,也就是在main方法中执行的,这里我是在项目中新建一个

JAVA提高第七篇 类加载器解析

同时采用的是参数的形式来进行赋值的,所以在运行的myclassloader的时候要进行输入参数的配置:右击myclassloader->run as -> run configurations

 JAVA提高第七篇 类加载器解析

第一个参数是classloaderattachment.class文件的源路径,第二个参数是加密后存放的目录,运行myclassloader之后,刷新class_temp文件夹,出现了classloaderattachment.class,这个是加密后的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
package study.javaenhance;
 
import java.util.arraylist;
 
public class classloadertest
{
  public static void main(string[] args) throws exception
  {
    //获取类加载器,那么这个获取的是一个实例对象,我们知道类加载器也有很多种,那么因此也有其对应的类存在,因此可以获取到对应的字节码
    system.out.println(classloadertest.class.getclassloader());
    //获取类加载的字节码,然后获取到类加载字节码的名字
    system.out.println(classloadertest.class.getclassloader().getclass().getname());
    //下面我们看下获取非我们定义的类,比如system arraylist 等常用类
    system.out.println(system.class.getclassloader());
    system.out.println(arraylist.class.getclassloader());
    
    
    //演示java 提供的类加载器关系
    classloader classloader = classloadertest.class.getclassloader();
    while(classloader != null)
    {
      system.out.print(classloader.getclass().getname()+"-->");
      classloader = classloader.getparent();
    }
    system.out.println(classloader);
    
    
    
    try {
      //class classdate = new myclassloader("class_temp").loadclass("classloaderattachment");
      class classdate = new myclassloader("class_temp").loadclass("study.javaenhance.classloaderattachment");
      object object = classdate.newinstance();
      //输出classloaderattachment类的加载器名称
      system.out.println("classloader:"+object.getclass().getclassloader().getclass().getname());
      system.out.println(object);
    } catch (exception e1) {
      e1.printstacktrace();
    }
  }
 
}

结果如下:

sun.misc.launcher$appclassloader@6b97fd
sun.misc.launcher$appclassloader
null
null
sun.misc.launcher$appclassloader-->sun.misc.launcher$extclassloader-->null
classloader:sun.misc.launcher$appclassloader
hello classloader!

这个时候我们会发现调用的app 的类加载器然后输出了结果,这个是正常的,因为这个时候会采用双亲委派机制。

那么这个时候,我们将自己生成的classloaderattachemet class文件,覆盖掉编译的时候生成的class 文件看下结果如何,如果正常应该会报错,因为这个时候走双亲委派机制在对应的classpath 是可以找到这个class 文件,因此app类加载器会处理,但是因为我们的class 是加密的因此会报错,运行结果如:

JAVA提高第七篇 类加载器解析

那么如何让其走到我们自定义的类加载器呢,只需要将编译时候生成的目录下的.class 文件删掉即可,那么这个是app加载不到,则会去调用findclass ,然后就会走到我们定义的类加载器中,运行结果如下:

JAVA提高第七篇 类加载器解析

参考资料:

张孝祥老师java增强视频

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:http://www.cnblogs.com/pony1223/p/7711092.html

标签:

相关文章

热门资讯

2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
Intellij idea2020永久破解,亲测可用!!!
Intellij idea2020永久破解,亲测可用!!! 2020-07-29
背刺什么意思 网络词语背刺是什么梗
背刺什么意思 网络词语背刺是什么梗 2020-05-22
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总 2020-11-13
歪歪漫画vip账号共享2020_yy漫画免费账号密码共享
歪歪漫画vip账号共享2020_yy漫画免费账号密码共享 2020-04-07
返回顶部