1.0 类加载机制概念

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。*.Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能,这里就是我们经常能见到的Class类。

image.png

1.1 类的加载时机

虚拟机规范了下面情况必须对类进行初始化?
1. 遇到new,getstatic,putstaic,invokestatic四个字节码指令时,如果类没有进行初始化,
则先进行类的初始化过程(new,读取或者设置类的static字段,final的放在常量池除外,调用类的静态方法)

  1. 使用java.lang.reflect包进行反射调用
    3. 当初始化一个类的方法,如果父类没有初始化,先初始化父类
    4. main方法所在的类在虚拟机启动时加载
    5.当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
    6.当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

不会初始化(可能会加载)
1.通过子类引用父类的静态字段, 只会触发父类的初始化, 而不会触发子类的初始化.

比如类A中有静态属性A1, 类B继承类A, 通过B.A1, 这里只会初始化类A, 而不会初始会类B

2.定义对象数组, 不会触发该类的初始化

比如Student[] s1 = new Student[10]; 这里不会初始化Student类,因为虚拟机只需要知道数组长度即可,跟放的是什么对象类型是没关系的

3.常量在编译期间会存入调用类的常量池, 本质上没有直接引用定义常量的类, 不会触发定义常量所在类

4.通过类名获取Class对象, 不会触发类的初始化,Hello.class不会让Hello类初始化

5.通过Class.forName加载指定类时,如果指定参数initialize为false时, 也不会触发初始化, 其实这个参数是告诉虚拟机, 是否要对类进行初始化. Class.forName("jvm.Hello")默认会加载Hello

6.通过ClassLoader默认的loadClass方法, 也不会触发初始化动作(加载了, 但是不会初始化)



1.2 类的加载过程

示图:
4.类加载器 - 图2

1.加载 :::info 在装载阶段,虚拟机需要完成以下3件事情
(1) 通过一个类的全限定名来获取定义此类的二进制字节流
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3) 在Java堆中生成一个代表这个类的java``.lang.Class对象,作为方法区这些数据的访问入口。
虚拟机规范中并没有准确说明二进制字节流应该从哪里获取以及怎样获取,这里可以通过定义自己的类加载器去控制字节流的获取方式 :::

2.验证 :::info 如果我们是从自己本地的class文件加载类信息肯定不会出错,
但是类的加载只是加载了一系列的二级制字节码,无法保证字节码的正确性,所以需要验证 :::

3.准备 :::info 准备阶段是正式为类变量分配并设置类变量初始值的阶段,
这些内存都将在方法区中进行分配(因为这里的变量都是类变量,实例变量在堆,类变量在方法区) 如:
public static int value = 123;
value在准备阶段过后的初始值为0而不是123,而把value赋值的putstatic指令将在初始化阶段才会被执行 :::

4.解析 :::info 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行 :::

5.初始化 :::info 类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。
在准备阶段变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主管计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器()方法的过程. ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的
()方法与实例构造器()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类
()方法执行之前,父类的()方法方法已经执行完毕

—————
就是由于父类的()方法先与子类执行,所以在父类的static语句先于子类执行,然后是父类的非static语句块和构造方法,接下来是子类的非static语句块和构造方法
————— :::


1.3 类型的来源有哪些?

  • 本地磁盘
  • 网络下载.class文件
  • war, jar下加载.class文件
  • 从专门的数据库中读取的.class文件
  • 将Java源文件动态编译成class文件
    • 典型的就是动态代理,通过运行期生成class文件
    • jsp会被转成servlet. 而servlet是一个java文件, 会被编译成class文件

1.3.1 通过什么来进行加载?(类加载器)

image.png

1.3.2 类加载器的分类以及各种加载职责以及层级结构

1.系统级别

  1. - 启动类加载器
  2. - 主要加载路径: $.JAVA_HOMEjre/lib/rt.jar
  3. - 底层使用C++实现, 不是`ClassLoader`的子类
  4. - 扩展类加载器
  5. - 主要加载路径: $.JAVA_HOMEjre\lib\ext, 可以通过-Djava.ext.dirs来修改路径
  6. - 底层Java实现,是`ClassLoader`子类, 位于`sun.misc.Launcher.ExtClassLoader`
  7. - 系统类加载器(App类加载器)
  8. - 主要加载路径: 工程目录下classpath下的class以及jar
  9. - 底层Java实现, `ClassLoader`子类, 位于`sun.misc.Launcher.AppClassLoader`

2.用户级别

  - 自定义类加载器(继承ClassLoader)

3.层级结构
4.类加载器 - 图4

4.类加载器加载路径测试代码:

package com.yuanzi.jvm;
import java.util.Arrays;
import java.util.List;

public class DiffClassLoaderLoadingPath {
    public static void main(String[] args) {
//        bootClassLoaderLoadingPath();
//        extClassLoaderLoadingPath();
//        appClassLoaderLoadingPath();

        System.out.println(DiffClassLoaderLoadingPath.class.getClassLoader());
        System.out.println(DiffClassLoaderLoadingPath.class.getClassLoader().getParent());
        System.out.println(DiffClassLoaderLoadingPath.class.getClassLoader().getParent().getParent());

    }


    /**
     * 启动类加载器加载的职责
     */
    private static void bootClassLoaderLoadingPath() {

        //获取启动类加载器加载的目录
        String bootStrapLoadingPath = System.getProperty("sun.boot.class.path");

        //把加载的目录转为集合
        List<String> bootLoadingPathList = Arrays.asList(bootStrapLoadingPath.split(";"));

        for (String bootPath : bootLoadingPathList) {
            System.out.println("[启动类加载器---加载的目录]: " + bootPath);
        }


    }


    /**
     * 扩展类加载器加载的职责
     */
    private static void extClassLoaderLoadingPath() {

        //获取扩展类加载器加载的目录
        String bootStrapLoadingPath = System.getProperty("java.ext.dirs");

        //把加载的目录转为集合
        List<String> bootLoadingPathList = Arrays.asList(bootStrapLoadingPath.split(";"));

        for (String bootPath : bootLoadingPathList) {
            System.out.println("[扩展类加载器---加载的目录]: " + bootPath);
        }
    }

    /**
     * 系统类加载器加载的职责
     */
    private static void appClassLoaderLoadingPath() {
        //获取启动类加载器加载的目录
        String bootStrapLoadingPath = System.getProperty("java.class.path");

        //把加载的目录转为集合
        List<String> bootLoadingPathList = Arrays.asList(bootStrapLoadingPath.split(";"));

        for (String bootPath : bootLoadingPathList) {
            System.out.println("[系统类加载器---加载的目录]: " + bootPath);
        }
    }
}

image.png

1.3.3 添加引用类的几种方式

1.放到JDK的lib/extx下, 或者-Djava.ext.dirs
2.java -cp/classpath 或者class文件放到当前路径
3.自定义ClassLoader加载
4.拿到当前执行类的ClassLoader, 反射调用addUrl方法添加Jar或路径(JDK9无效)
image.png


1.4 双亲委派模型

类加载的时候, 遵循双亲委派模型
1.先检查类是否被加载后
2.再自底向上, 依次递归到父类,
3.递归到最底后即bootStarp加载器, 再执行findClass()执行加载
4.父类没有加载成功,则由子类再加载, 依次再递归回来.
image.png

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。 不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的, 而是通常使用组合(Composition)关系来复用父加载器的代码。

源码分析

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

委派机制的流程图

4.类加载器 - 图8

双亲委派的好处
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。


1.5 自定义类加载器

1.5.1 自定实现类加载器

<blockquote><pre>
  class NetworkClassLoader extends ClassLoader {
          String host;
          int port;

          public Class findClass(String name) {
              byte[] b = loadClassData(name);
              return defineClass(name, b, 0, b.length);
          }

          private byte[] loadClassData(String name) {
              // load the class data from the connection
              &nbsp;.&nbsp;.&nbsp;.
          }
      }
</pre></blockquote>

1.写一个继承ClassLoader
2.重写loaderClassData(); 功能将一个文件变成byte数组, 关键是返回的byte数组, 也可以使用其他方法替代.
3.重写findClass()

示例代码:

package com.yuanzi.jvm;

import java.io.*;

public class YuanziClassLoader extends ClassLoader {
    private final static String fileSuffixExt = ".class";
    private String classLoaderName;
    private String loadPath;

    public void setLoadPath(String loadPath) {
        this.loadPath = loadPath;
    }

    /**
     * 执行当前类加载器的父类加载器
     */
    public YuanziClassLoader(ClassLoader parent, String classLoaderName) {
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    /**
     * 使用appClassLoader加载器,作为本类的加载器
     */
    public YuanziClassLoader(String classLoaderName) {
        super();
        this.classLoaderName = classLoaderName;
    }

    public YuanziClassLoader(ClassLoader classLoader) {
        super(classLoader);
    }

    /**
     * 方法实现说明: 创建我们的class的二进制名称
     */
    private byte[] loadClassData(String name) {
        byte[] data = null;
        ByteArrayOutputStream baos = null;
        InputStream is = null;

        try {
            name = name.replace(".", "\\");
            String fileName = loadPath + name + fileSuffixExt;
            File file = new File(fileName);
            is = new FileInputStream(file);

            baos = new ByteArrayOutputStream();
            int ch;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null) {
                    baos.close();
                }
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException{
        byte[] data = loadClassData(name);
        System.out.println("YuanziClassLoader 加载的类: ==>" + name);
        return defineClass(name,data,0,data.length);
    }
}

1.5.2 类加载器的类名称空间

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间

这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等.

  • 同一个命名空间的类是相互可见的。
  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器能看见根类加载器加载的类。
  • 由父类加载器加载的类不能看见子加载器加载的类。
  • 如果两个加载器没有父子关系,那么他们自己加载的类互相不可见

image.png


1.6 打破双亲委派模型

1.6.1 上层的基础类需要调用下层用户代码

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,
如果有基础类型又要调用回用户的代码,那该怎么办呢?

一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?
image.png

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread ContextClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

Thread.currentThread().setContextClassLoader(this.loader);

image.png

Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。
不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,
为了消除这种极不优雅的实现方式,在JDK 6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。

1.6.2 热部署

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“**动态性**”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(HotDeployment)等。
说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是大型系统或企业级软件开发者具有很大的吸引力。

OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,**每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。**

在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构,
当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将以java.*开头的类,委派给父类加载器加载。
2)否则,将委派列表名单内的类,委派给父类加载器加载。
3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的