欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

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

今天我们学习类加载器,关于类加载器其实和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这个类的类加载器的名字,代码如下:

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.

代码演示如下:

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方法的源代码:

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

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

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,看一下源代码:

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:

package study.javaenhance;

public class classloaderattachment {
  @override
  public string tostring() {
    return "hello classloader!";
    
  }

}

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

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

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文件。

下面来看一下测试类: 

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增强视频

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