iceflow 发表于 2018-10-29 15:30

深入理解类加载器

#深入理解类加载器
## 类加载的过程与作用已经在上面阐述了
## 类缓存

在介绍类加载器之前,先来说说 `JVM` 的缓存机制。

我们这里所讨论的不是某个方法中所需要用到的变量缓存,而是类缓存。我们知道标准的 `JAVASE` 类加载器可以按照要求来查找类,一旦某个类被加载到类加载器中,它将维持加载(**也就是缓存**)一段时间。在有方法被调用的时候,一般都是直接在缓存中直接取出该类并创建它的一个副本引用。

需要注意的是,虽然有类缓存的存在可以使性能提高,但 `JVM` 中的垃圾回收机制还是会回收这些 `Class` 对象的。

> <font color=#9F79EE>一般来说,类都只被加载了一次!</font>

一个简单的测试 demo

```java
public class demo01 {
    public static void main(String[] args) throws ClassNotFoundException {
         // 当A对象被创建的时候 先会执行静态代码块,再实执行A的构造方法
         A a = new A();
         System.out.println(a.width);
         // 输出顺序为创建初始化类A--> 创建A对象--> width = 300
         A a2 = new A();
         // 只会打印 创建A对象,不会再加载第二次
    }
}
class A {
    public static int width = 100;
    public static final int MAX = 200;
    static {
      System.out.println("静态初始化类A");
      width = 300;
    }
    public A(){
      System.out.println("创建A对象");
    }
}
```

## java.class.ClassLoader类介绍

一句话简单介绍一下 ClassLoader 的具体作用:就是将 `.class` 文件加载到jvm虚拟机中去。

- `java.class.ClassLoader` 类的基本职责就是根据一个制定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 `JAVA` 类,即 `java.lang.Class` 类的一个实例。

- 除此之外,`ClassLoader` 还负责加载 `Java` 应用所需的资源,如图像文件和配置文件等。


## 类加载器的层次结构(树状结构)

先介绍一下 `JAVA` 中的四个重要的类加载器,加载器按照父级到子级的顺序进行排序。可以通过 `getParent()` 获取其父类的加载器,这个在下面会详细介绍。

### 引导类加载器(bootstrap class loader)
* 它用来加载 `Java` 的核心库( <font color=#B3B3B3>Java_Home/jre/lib/rt.jar</font> ,或者 `sun.boot.class.path` 路径下的内容),使用原生代码写的 -->`C++`,并不继承 `java.lang.ClassLoader` ,它本身就是 `JVM` 虚拟机的一本分,它并不属于一个 `JAVA` 类,所以无法在 `JAVA` 代码中获取到它的引用。
* 引导类加载器的主要功能:加载扩展类和应用程序类加载器,并制定它们的父类加载器。
### 扩展类加载器(extensions class loader)
* 用来加载 `JAVA` 的扩展库(`Java_Home/jre/ext/*.jar`,或者 `java.ext.dirs` 路径下的内容)。如果程序中没有指定该系统属性(`-Djava.ext.dirs = sss/lib)`, `JAVA ` 虚拟机的实现会提供一个扩展库目录(默认是 `$JAVA_HOME/lib/ext` )。该类加载器在此目录里面查找并加载 `JAVA` 类
* 由 `sun.misc.Launcher$ExtClassLoader` 实现
来看一下系统默认指定的 `java.ext.dirs` 路径

```java
public class testLoader
{
    public static void main(String[] args)
    {
      System.out.println(System.getProperty("java.ext.dirs"));
    }
}
```



### 应用程序类加载器(appliaction class loader)
* 它根据 `JAVA` 应用的类路径(`classpath` `java.class.path` )来加载。
    一般来说 `Java` 应用的类都是由它来完成加载的(自己编写的 `JAVA` 类)。

* 由`sun.misc.Launcher$AppClassLoader`实现

    ```java
    public static void main(String[] args)
    {
      System.out.println(ClassLoader.getSystemClassLoader());
      // 执行结果为: sun.misc.Launcher$AppClassLoader@addbf1
    }
    ```
### 自定义类加载器
* 开发人员可以通过继承 `java.lang.ClassLoader` 类的方式实现自己的类加载器,以满足一些特殊的要求。


在自定义类加载器中,开发人员应该注意要实现 **双亲委派机制**,在下面会详解。我们先来看一下代码怎么写

```java
// 委托给父类加载器 这时候进入系统的加载器中,会一层一层的往上送
ClassLoader parent = this.getParent();
c = parent.loadClass(name);
```



### 关于层次结构说明
* 引导类加载器是有底层代码 `C++` 来实现的,它没有继承 `java.lang.ClassLoader` 接口
    <font color=#9F79EE>当使用引导类加载器的时候,因为这是所有加载器的父类,为了安全所以,是无法打印该类的名称的,返回的是 null 。</font>
* 扩展类加载器(``extensions class loader` ),应用程序类加载器( `appliaction class loader` ),自定义类加载器均是用 `JAVA` 实现的,都需要继承 `java.lang.ClassLoader` 接口
* 子父类关系 自定义加载器 -->应用程序类加载器( `appliaction class loader` )-->扩展类加载器( `extensions class loader` )-->引导类加载器( `bootstrap class loader` )

## 类加载器的代{过}{滤}理模式

为什么这里的标题是 **类加载器的代{过}{滤}理模式** 呢,其实这里只是一个笼统的概括,在 `JVM` 的类加载器设计模式中,大多的都是 **代{过}{滤}理模式**

> 双亲委托机制就是代{过}{滤}理模式中的一种

### 双亲委托机制

* 就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,直到最高的爷爷辈的,如果父类加载器可以完成类的加载任务,则成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
自己拿到东西后看都不看就往父级丢(典型的啃老,老一辈的有了先花掉)。
* 双亲委托机制是为了保证 `JAVA` 核心库的类型安全。[**这种机制保证了不会出现用户能自定义 `java.lang.Object` 类的情况**]。 保证核心类无法被用户定义!
* 类加载器除了用于加载类,也是安全的最基本的屏障。


### 双亲委托机制只是代{过}{滤}理模式中的一种
* 并不是所有的代{过}{滤}理模式都采用双亲委托机制

* `tomcat` 服务器类加载其也使用代{过}{滤}理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代{过}{滤}理给父类加载器。这与一般类加载器的顺序是相反的。

## 自定义类加载器
```java
import java.io.*;

/**
* 自定义文件系统类加载器
* Created by BF on 2017/9/18.
*/
public class FileSystemClassLoader extends ClassLoader {
// 传进一个目录
// d://myjava/com/wiceflow/JVM/demo01.class   -->com.wiceflow.JVM.demo01

// 指定根目录
private String rootDir;

public FileSystemClassLoader(String rootDir) {
      this.rootDir = rootDir;
}
// 重写父类方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
      Class<?> c = findLoadedClass(name);
      // 应该先查询有没有加载过这个类。如果有加载过,则直接返回加载好的类,如果没有,则加载新的内容
      if (c!=null){
          return c;
      }else {
          // 交给父类去加载,获得父类加载器
          // 因为自定义加载器的上一层次是app类加载器,所以这里获得到的应用程序类加载器appliaction class loader
          try{
            // 委托给父类加载器 这时候进入系统的加载器中,会一层一层的往上送
            ClassLoader parent = this.getParent();
            c = parent.loadClass(name);
          }catch (Exception e){
            System.out.println("父级异常什么都不做!");
          }
          if (c!=null){
            return c;
          }else{
            // 自定义IO流读取
            byte[] classData =getClassData(name);
            if (classData == null){
                  // 如果还是找不到,就手动抛出异常
                  throw new ClassNotFoundException();
            }else{
                  c = defineClass(name,classData,0,classData.length);
                  return c;
            }
          }
      }
}

/**
   * 自定义字节流读取器
   * @param name com.wiceflow.JVM.demo01 -->d://myjava/com/wiceflow/JVM/demo01.class
   * @return
   */
private byte[] getClassData(String name){
      // 转换路径
      String path = rootDir + "/" + name.replace('.','/') + ".class";
      // 字节输出流
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      // 文件流
      InputStream is = null;
      try {
          is = new FileInputStream(path);
          // 定义接收数组
          byte[] buffer = new byte;
          int temp = 0;
          while ((temp=is.read(buffer))!=-1){
            baos.write(buffer,0,temp);
          }
          return baos.toByteArray();
      } catch (Exception e) {
          e.printStackTrace();
          return null;
      }finally {
          if (is!=null){
            try {
                  is.close();
            } catch (IOException e) {
                  e.printStackTrace();
            }
          }
      }
}
}
```


### 双亲委派机制请求过程

1. 首先检查请求请求的类型是否已经被这个类装载其装载到命名空间中了,如果已经装载,直接返回。
2. 委派类加载请求给父类加载器,如果父类加载器能够完成加载,则返回父类加载器加载的Class实例。
   这里委派个父级处理的时候,如果父级加载器一层一层的往上找都没有对应的,则会在其自身抛出异常终止程序运行,而不是自定义加载器抛出异常,所以我们要进行 `try/catch`操作,防止父类抛异常终止操作。
3. 调用本类加载器的`findClass(...)`方法,试图获取对应的字节码,如果获取到,则调用`defindClass(...)`导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给`loadClass(...)`,`loadClass(...)`转抛异常,终止加载过程

* <font color = red>注意: 被两个类加载器的同一个类,JVM不认为是相同的类,它们的HashCode不同!</font>

```java
FileSystemClassLoader loader1 = new FileSystemClassLoader("D:/myjava");
FileSystemClassLoader loader2 = new FileSystemClassLoader("D:/myjava");
Class<?> c1 = loader1.loadClass("com.wiceflow.HelloWord");
Class<?> c2 = loader1.loadClass("com.wiceflow.HelloWord");
Class<?> c3 = loader2.loadClass("com.wiceflow.HelloWord");
System.out.println(c1.hashCode()==c2.hashCode()); // true
System.out.println(c1.hashCode()==c3.hashCode()); // false
```

* <font color = blue>注意:加密后的 `Class` 文件,正常的类加载器无法加载,会报 `classFormatError` 异常! 这时候就要用到自己定义类加载器来加载,在其中先解密 </font>

> classFormatError 异常还有可能是被重复加载导致的,例如在使用 tomcat 时,启动了两次就回出现这个异常。


## 了解另外两个类加载器
### 线程上下文类加载器

`JAVA` 提供了很多服务提供者接口(`Service Provider Interface`,`SPI`),允许第三方为这些接口提供实现。SPI 实现的 `JAVA` 类一般是由系统类加载器来加载的。

* 双亲委托机制以及类加载器的问题
* 一般情况下,保证同一个类中所关联的其他类都是由当前类的类加载器所加载的。
    比如,`Class A` 本身是在 `Ext` 下找到,那么他里面 `new` 出来的一些类也就只能用 `Ext` 去查找了(不会低一个级别),所以有些明明 `App加载器` 才可以找到的,却找不到了。
* `JDBC API`,它有实现的 `driven` 部分(`musql`,`sql Server`),我们的 `JDBC API` 都是由 `BOOT` 或者 `Ext` 来载入的。但是 `JDBC driven` 却是由 `Ext` 或者 `APP` 载入的,那么就由可能找不到`driven`了,在 `JAVA` 领域中,其实只要分成这种 `API + SPI` 的,都会遇到此问题。

* 常见的SPI有 `JDBC` , `JCE` , `JNDL` , `JAXP` ,和 `JBI` 等,这些 `SPI` 的接口由 `JAVA` 核心库来提供
    eg:`JAXP` 的 SPI 接口定义包含在 `javax.xml.parsers` 包中。
    `SPI` 的接口是 `JAVA` 核心库的一部分,是由引导类加载其来加载的,`SPI` 实现的 `JAVA` 类一般是由系统类加载器来加载的,引导类加载器无法找到 `SPI` 的实现类的,因为它只加载 `JAVA` 核心库。
* 通常当你需要动态加载资源的时候,你至少有三个 `ClassLoader` 选择 :

1. 系统类加载器或叫做应用类加载器( `System ClassLoader` or`appliaction ClassLoader`)
2. 当前类加载器
3. 当前线程类加载器


* **线程类加载器是为了抛弃双亲委派加载链模式**
* 每个线程都有一个关联的上下文类加载器。如果你使用 `new Thread()` 方式来生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文加载器没有任何改动的话,程序中所有线程都将使用系统类加载器作为上下文类加载器。


* 两个方法
* Thread.currentThread().getContextClassLoader()   获取线程上下文加载器
* Thread.currentThread().setContextClassLoader()   设置线程上下文加载器   

`JAVA` 默认的线程上下文类加载器是系统类加载器(`AppClassLoader`)

```java
// Now create the class loader to use to launch the application
try {
    loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
    throw new InternalError(
"Could not create application class loader" );
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
```



## Tomcat 服务器额类加载器
* 一切都是为了安全
    ### Tomcat 不能使用系统默认的类加载器
    * 如果 `Tomcat` 跑你的 `WEB` 项目使用系统的类加载器那是相当危险的,你可以直接是无忌惮的操作系统的各个目录了。
    * 对于运行在 `JavaEE` 容器中的 `Web` 应用来说,类加载器的实现方式与一般 `JAVA` 应用有所不同
    * 每个 `Web` 应用都有对应的一个类加载器实例。该类加载器也使用代{过}{滤}理模式(不同于前面所加的双亲委派机制),所不同的是它首先尝试去加载某个类,如果找不到再代{过}{滤}理给父类加载器。这与一般的加载器的顺序是相反的。但也是为了保证安全,这样核心库就不在查询范围之内。


* 为了安全 `Tomcat` 需要实现自己的类加载器
    * 我可以限制你只能把类写在指定的地方,否则我不给你加载
      eg:`WEB`文件夹

iceflow 发表于 2018-10-29 17:03

请大家忽略 过滤 两个字:rggrg

xiaobaibaibai 发表于 2018-10-29 15:45

非常不错 ~~!!

xiaojiang_320 发表于 2018-10-29 15:50

看不懂技术贴,纯支持,顶一下

babylove4219 发表于 2018-10-29 15:55

纯支持,顶一下

thornfish 发表于 2018-10-29 16:21

不错,学习了

boressa 发表于 2018-10-29 16:58

支持一下,加油

vLove0 发表于 2018-10-29 17:12

学习一下,感谢

wangqiustc 发表于 2018-10-29 17:50

在吾爱排版这么仔细,也是难为你了

faleqi 发表于 2018-10-29 18:14

这里还有java啊
页: [1]
查看完整版本: 深入理解类加载器