Java类加载机制(ClassLoader)详解

  • Post author:
  • Post category:java


类加载机制总结

一.类加载器基本概念

类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。如下图。

其实可以一句话来解释:

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

二. 在什么时候才会启动类加载器?

其实,类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

其中加载(除了自定义加载)+链接的过程是完全由jvm负责的,什么时候要对类进行初始化工作(加载+链接在此之前已经完成了),jvm有严格的规定(四种情况):


1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,加入类还没进行初始化,则马上对其进行初始化工作。其实就是3种情况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们已经被塞进常量池了)、以及执行静态方法的时候。


2.使用java.lang.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。


3.初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。


4.当jvm启动时,用户指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。


以上4种预处理称为对一个类进行主动的引用,其余的其他情况,称为被动引用,都不会触发类的初始化。

三. 从哪个地方去加载.class文件

在这里进行一个简单的分类。例举了5个来源

(1)本地磁盘

(2)网上加载.class文件(Applet)

(3)从数据库中

(4)压缩文件中(ZAR,jar等)

(5)从其他文件生成的(JSP应用)

有了这个认识之后,下面就开始讲讲,类加载机制了。首先看的就是类加载机制的过程。

四. 类加载的过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:

其中类加载的过程包括了

加载、验证、准备、解析、初始化五个阶段

。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

下面就一个一个去分析一下这几个过程。


1)、加载

”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:

(1)通过一个类的全限定名来获取其定义的二进制字节流

(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构

(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。

相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载


2)、验证

验证的主要作用就是

确保被加载的类的正确性

。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:

(1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。

(2)元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。

(3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。

(4)符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。

对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。


3)、准备


准备阶段主要为类变量分配内存并设置初始值

。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:

(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,

(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如

public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。

当然还有其他的默认值。

注意,在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。


4)、解析


解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程

。什么是符号应用和直接引用呢?

符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)

直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。


5)、初始化

这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。

在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

①声明类变量是指定初始值

②使用静态代码块为类变量指定初始值

JVM初始化步骤

1、假如这个类还没有被加载和连接,则程序先加载并连接该类

2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下几种:

(1)、创建类的实例,也就是new的方式

(2)、访问某个类或接口的静态变量,或者对该静态变量赋值

调用类的静态方法

(3)、反射(如 Class.forName(“com.Test”))

(4)、初始化某个类的子类,则其父类也会被初始化

(5)、Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类

好了,到目前为止就是类加载机制的整个过程,但是还有一个重要的概念,那就是类加载器。在加载阶段其实我们提到过类加载器,说是在后面详细说,在这就好好地介绍一下类加载器。

5. 类加载器


分类:


系统级别的类加载器:


Bootstrap ClassLoader


(启动类加载器)


Extention ClassLoader


(扩展类加载器)


Appclass Loader


(系统类加载器)


用户级别的类加载器


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


系统级别的类加载器:


(1)Bootstrap ClassLoader

:最顶层的加载类,主要加载核心类库,也就是我们环境变量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。


(2)Extention ClassLoader

:扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。


(3)Appclass Loader

:也称为SystemAppClass。 加载当前应用的classpath的所有类。我们看到java为我们提供了三个类加载器,应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。这三种类加载器的加载顺序是什么呢?

Bootstrap ClassLoader > Extention ClassLoader > Appclass Loader


一张图来看一下他们的层次关系


代码演示:


Bootstrap ClassLoader


(启动类加载器)

代码:

public  static void   bootClassLoaderLoadingPath(){

        String bootClassLoaderLoadingPath = System.getProperty("sun.boot.class.path");

        List<String> asList = Arrays.asList(bootClassLoaderLoadingPath.split(";"));

        for (String s : asList) {

            System.out.println("【启动类加载器====加载目录】"+s);

        }

    }

输出:


Extention ClassLoader


(扩展类加载器)职责

代码演示:

   public  static void   extClassLoaderLoadingPath(){

        String bootClassLoaderLoadingPath = System.getProperty("java.ext.dirs");

        List<String> asList = Arrays.asList(bootClassLoaderLoadingPath.split(";"));

        for (String s : asList) {

            System.out.println("【拓展类加载器====加载目录】"+s);

        }

    }

输出:


Appclass Loader


(系统类加载器)职责

代码演示:

public  static void   appClassLoaderLoadingPath(){

        String appClassLoaderLoadingPath = System.getProperty("java.class.path");

        List<String> asList = Arrays.asList(appClassLoaderLoadingPath.split(";"));

        for (String s : asList) {

            System.out.println("【系统类加载器====加载目录】"+s);

        }

    }

输出:

3)双亲委派原则

工作流程是: 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:

a.可以避免重复加载,父类已经加载了,子类就不需要再次加载

b.更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。

4)、自定义类加载器


在这一部分第一小节中,我们提到了


java


系统为我们提供的三种类加载器,还给出了他们的层次关系图,最下面就是自定义类加载器,那么我们如何自己定义类加载器呢?这主要有两种方式

(1)遵守双亲委派模型:继承ClassLoader,重写findClass()方法。

(2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。

我们看一下实现步骤


(1)创建一个类继承ClassLoader抽象类


(2)重写findClass()方法


(3)在findClass()方法中调用defineClass()

代码演示:


被加载的类:

/**

 * @ClassName Sun

 * @Description TODO   被加载的类

 * @Author 胡泽

 * @Date 2020/2/25 21:32

 * @Version 1.0

 */

public class Sun {

    static{

        System.out.println("Hello,HZ ,Sun 已经初始化了");

    }

}


自定义classloader 类

package com.company;

import java.io.ByteArrayOutputStream;

import java.io.FileInputStream;

import java.io.InputStream;



/**

 * @ClassName SunClassloader

 * @Description TODO   自定义classloader

 * @Author 胡泽

 * @Date 2020/2/25 21:33

 * @Version 1.0

 */

public class SunClassloader extends ClassLoader {



    /**

     * class文件的路径

     */

    private String path;



    public SunClassloader(String path) {

        this.path = path;

    }



    public SunClassloader() {

    }



    public SunClassloader(ClassLoader parent) {

        super(parent);

    }

//defineClass 通常是和findClass 方法一起使用的,我们通过覆盖ClassLoader父类的findClass 方法来实现类的加载规则,

    // 从而取得要加载类的字节码,然后调用defineClass方法生成类的Class 对象,如果你想在类被加载到JVM中时就被链接,

    // 那么可以接着调用另一个 resolveClass 方法,当然你也可以选择让JVM来解决什么时候才链接这个类。



    @Override

    protected Class<?> findClass(String name) throws ClassNotFoundException {

        System.out.println("hello i am hz‘s loader.");

       String classPath = path  + ".class";



        try (InputStream in = new FileInputStream(classPath)) {

            ByteArrayOutputStream out = new ByteArrayOutputStream();

            int i = 0;

            while ((i = in.read()) != -1) {

                out.write(i);

            }

            byte[] byteArray = out.toByteArray();

            //defineClass 方法用来将 字节流解析成 JVM 能够识别的 Class 对象

            return defineClass(null,byteArray, 0, byteArray.length);

        } catch (Exception e) {

            e.printStackTrace();

        }

        System.out.println("hello i am hz‘s loader.");

         return  null;

    }

    @Override

    public Class<?> loadClass(String name) throws ClassNotFoundException {

        System.out.println("hello i am hz‘s loader.");

        return super.loadClass(name);

    }

}





实现类加载:

public static void main(String[] args) throws Exception {

         SunClassloader sunClazz = new SunClassloader("E:\\classloader\\src\\com\\company\\Sun");

       Class<?> clazz = sunClazz.findClass("Sun");

        clazz.newInstance();

}

6.ClassLoader 核心方法

如下图


从图可以看出顶层的类加载器是ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),这里我们主要介绍ClassLoader中几个比较重要的方法。


  1. loadClass(String)


该方法加载指定名称(包括包名)的二进制类型,该方法在


J


DK1.2


之后不再建议用户重写但用户可以直接调用该方法





loadClass()


方法是


ClassLoader


类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,


loadClass(String name, boolean resolve)


是一个重载方法,


resolve


参数代表是否生成


class


对象的同时进行解析相关操作。


主要源码:

protected Class<?> loadClass(String name, boolean resolve)

      throws ClassNotFoundException

  {

      synchronized (getClassLoadingLock(name)) {

          // 先从缓存查找该class对象,找到就不用重新加载

          Class<?> c = findLoadedClass(name);

          if (c == null) {

              long t0 = System.nanoTime();

              try {

                  if (parent != null) {

                      //如果找不到,则委托给父类加载器去加载

                      c = parent.loadClass(name, false);

                  } else {

                  //如果没有父类,则委托给启动加载器去加载

                      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

                  // 如果都没有找到,则通过自定义实现的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


方法所展示的,当类加载请求到来时,先


从缓存中查找该类对象


,如果存在直接返回,如果不存在则交给该


类加载去的父加载器去加载


,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用


findClass()


方法去加载(关于


findClass()


稍后会进一步介绍)。从


loadClass


实现也可以知道如果不想重新定义加载类的规则


,也没


有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用


this.getClass().getClassLoder.loadClass(“className”)


,这样就可以直接调用


ClassLoader





loadClass


方法获取到


class


对象。

7.ClassLoader 核心方法举例

(1)重写findClass方法:


代码举例:

package com.company;

import java.io.*;

/**

 * @ClassName MyClassLoader

 * @Description TODO

 * @Author 胡泽

 * @Date 2020/3/3 12:24

 * @Version 1.0

 */

public class MyClassLoader extends  ClassLoader {



    private String classLoaderName;

    private String fileExtension = ".class";



    public MyClassLoader(String classLoaderName) {

        this.classLoaderName = classLoaderName;

    }



    public MyClassLoader(ClassLoader parent, String classLoaderName) {

        super(parent);

        this.classLoaderName = classLoaderName;

    }



    @Override

    protected Class<?> findClass(String name) throws ClassNotFoundException {

        byte[] classData = getClassData(name);

        return defineClass(null, classData, 0, classData.length);

    }



    public byte[] getClassData(String className) {

        byte[] bytes = null;

        InputStream is = null;

        ByteArrayOutputStream bos = null;



        try {

            is = new FileInputStream(new File(className, fileExtension));

            bos = new ByteArrayOutputStream();



            int ch = 0;

            while ((ch = is.read()) != -1) {

                bos.write(ch);

            }



            bytes = bos.toByteArray();

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            try {

                is.close();

                bos.close();

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

        return bytes;

    }



    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {





        MyClassLoader myClassLoader=new MyClassLoader("MyLoader");



        Class<?> loadClass = myClassLoader.loadClass("com.company.People");

        Object instance = loadClass.newInstance();

        System.out.println(instance.getClass().getClassLoader());

        //此时打印的类加载器任然是系统类加载器

        //原因:系统类加载器加载的是项目中的类,People就是项目中的类,

        //按照双亲委托模型:MyClassLoader,将任务委托给父加载器(系统类加载器),父加载器发现可以加载,就加载类了,并不需要在给子类去加载。

        //故MyClassLoader的findClass方法不会走



    }

}


输出:

//同样的道理,只要加载的类不是项目中的类,findClass方法就会走,因为父加载器都不会加载。实例如下

package com.company;

import java.io.*;



/**

 * @ClassName MyClassLoader2

 * @Description TODO

 * @Author 胡泽

 * @Date 2020/3/3 12:29

 * @Version 1.0

 */

public class MyClassLoader2  extends  ClassLoader{



    private String classLoaderName;

    private String fileExtension = ".class";

    private String path;



    public MyClassLoader2(String classLoaderName) {

        super();

        this.classLoaderName = classLoaderName;

    }



    public void setPath(String path) {

        this.path = path;

    }



    public MyClassLoader2(ClassLoader parent, String classLoaderName) {

        super(parent);

        this.classLoaderName = classLoaderName;

    }



    @Override

    protected Class<?> findClass(String name) throws ClassNotFoundException {

        System.out.println("findClass invoke: "+name);

        System.out.println("class loader name: "+classLoaderName);

        byte[] classData = getClassData(name);

        return defineClass(null, classData, 0, classData.length);

    }



    public byte[] getClassData(String className) {

        byte[] bytes = null;

        InputStream is = null;

        ByteArrayOutputStream bos = null;



        className=className.replaceAll("\\.","\\\\");

        try {



            File file=new File(this.path,className+fileExtension);



            is = new FileInputStream(file);

            bos = new ByteArrayOutputStream();



            int ch = 0;

            while ((ch = is.read()) != -1) {

                bos.write(ch);

            }



            bytes = bos.toByteArray();

        } catch (Exception e) {

            e.printStackTrace();

        } finally {

            try {

                is.close();

                bos.close();

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

        return bytes;

    }





    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {

        MyClassLoader2 myTest=new MyClassLoader2("MyLoader");

        myTest.setPath("C:\\Users\\14255\\Desktop");

        Class<?> loadClass = myTest.loadClass("com.People");

        Object instance = loadClass.newInstance();

        System.out.println(instance.getClass().getClassLoader());

        System.out.println(instance);

        //打印的类加载器是自定义的MyClassLoader2

    }

}


输出:



以上例子充分体现了双亲委派原则。

  1. 重写loadClass方法



代码举例:

package com.company;



import java.io.IOException;

import java.io.InputStream;

public class MyClassLoader3 extends ClassLoader {



    @Override

    public Class<?> loadClass(String name)

            throws ClassNotFoundException {

        try {

            // 这个getClassInputStream根据情况实现



            InputStream is = getClassInputStream(name);



            if (is == null) {



                return super.loadClass(name);

            }



            byte[] bt = new byte[is.available()];



            is.read(bt);



            return defineClass(name, bt, 0, bt.length);



        } catch (IOException e) {



            throw new ClassNotFoundException("Class " + name + " not found.");

        }

    }



    private InputStream getClassInputStream(String name) {



        String filename = name.replace('.', '/') + ".class";



        InputStream is = getClass().getResourceAsStream(filename);

        return is;

    }



}



输出:

(3)调用顺序



版权声明:本文为huzecom原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。