1 什么是ClassLoader

当我们编写的程序在运行的时候,需要调用其他.class文件中的方法,在Java中并不会一次性加载程序的所要用的所有的class文件,而是根据程序的需要,通过Java的类加载机制来动态加载某个.class文件到内存当中,只有class文件被加载到内存中后,才能被其他clss所引用,JVM负责加载.class字节码到内存,而ClassLoader就负责将.class字节码加载到JVM中。
ClassLoader除了能将Class加载到JVM中之外,还有两个作用:
1、审查每个类应该由谁加载;
2、Class字节码重新解析成JVM统一要求的对象格式;

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

ClassLoader常用方法

defineClass(byte[],int,int)

protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError

用来将byte字节流解析成JVM能够识别的Class对象

findClass(String)

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

defineClass通常和findClass方法一起使用,通过直接覆盖ClassLoader父类的findClass方法来实现类的加载规则,从而取得要加载类的字节码。然后调用defineClass方法生成类的Class对象。

resolveClass(Class<?>)

在类被加载到JVM中调用该方法就会被链接(Link)

loadClass(String , boolean)

protected Class<?> loadClass(String name, boolean resolve)
       throws ClassNotFoundException
   {
       synchronized (getClassLoadingLock(name)) {
           // 1.检查这个类是否已经被加载了
           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) {
                   // 如果仍未加载,调用findClass方法加载
                   long t1 = System.nanoTime();
                   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();
               }
           }
           //根据第二个参数来决定是否链接(Link)
           if (resolve) {
               resolveClass(c);
           }
           return c;
       }
   }

ClassLoader的等级加载机制

BootstrapClassLoader

主要加载JVM自身工作需要的类,完全由JVM自己控制,别人访问不了这个类,它仅仅是一个类的加载工具而已,既没有更高一级的父加载器,也没有子加载器。

ExtClassLoader

ExtClassLoader称为扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)。

AppClassLoader

AppClassLoader应用类加载器,又称为系统类加载器,负责在JVM启动时,加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。
我们在实现自己的类加载器的时候,不管是直接实现抽象类ClassLoad还是继承URLClassLoad类,或是其他子类,他的父加载器都是AppClassLoad,因为不管调用哪个父类构造器,创建的对象都必须最终调用getSystemClassLoader()作为父加载器,而getSystemClassLoader()方法返回的正是AppClassLoader。

JVM加载class文件到内存的两种方式:

  • 隐式加载:不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存的方式,例如:当我们在类中继承或者引用某个类时,JVM在解析当前这个类时发现引用的类不在内存中,那么就会自动将这些类加载到内存中。
  • 显示加载:在代码中通过调用ClassLoader类来加载一个类的方式,例如:调用this.getClass.getClassLoader().loadClass()或者Class.forName(),或者是调用我们自己实现的ClassLoader的findClass()方法等。

3 如何加载class文件

用ClassLoader加载一个class文件到JVM时需要经过的步骤如下:
在这里插入图片描述
第一个阶段是找到.class文件并把这个文件包含的字节码加载到内存中。
第二个阶段又可以分为三个步骤,分别是字节码验证、Class类数据结构及相应的内存分配和最后的符号表的链接。
第三个阶段是类中静态属性和初始化赋值,以及静态块的执行等。

加载字节码到内存

查看URLClassLoader部分源码:

public class URLClassLoader extends SecureClassLoader implements Closeable {
    private final URLClassPath ucp;
    private final AccessControlContext acc;
	//构造方法必须要指定一个URL数据才能够创建URLClassLoader对象,也就是必须要指定这个ClassLoader默认到哪个目录下去查找class文件
    public URLClassLoader(URL[] urls) {
        super();
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = AccessController.getContext();
        //通过一个URLClassPath类帮助取得要加载的class文件字节流,URLClassPath也就定义了到哪去找这个class文件,如果找到了这个class文件,在读取他的byte字节流,通过调用defineClass()方法来创建类对象。
        ucp = new URLClassPath(urls, acc);
    }
	protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        //这里就用到了ucp来取得要加载类的字节码
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                            	//生成Class对象
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
}

在URLClassLoader中通过一个URLClassPath类帮助取得要加载的class文件字节流,而这个URLClassPath定义了到哪里去找这个class文件,如果找到了这个class文件,再读取它的byte字节流,通过调用defineClass()方法来创建类对象。

在创建URLClassPath对象时会根据传过来的URL数组中的路径来判断是文件还是jar包,根据路径的不同分别创建FileLoader或者JarLoader,或者使用默认的加载器。当JVM调用findClass时由这几个加载器来将class文件的字节码加载到内存中。

验证与解析

  • 字节码验证,类装入器对于类的字节码要做许多检测,以确保格式正确、行为正确。
  • 类准备,在这个阶段准备代表每个类中定义的字段、方法和实现接口所必须的数据结构。
  • 解析,在这个阶段类装入器装入类所引用的其他所有类。可以用许多方式引用类,如超类、接口、字段、方法签名、方法中使用的本地变量。

    初始化Class对象

    在类中包含的静态初始化器都会被执行,设置为默认值。

    4 常见加载类错误分析

    ClassNotFoundException

    这个异常通常发生在显式加载类的时候。
    显式加载通常有如下方式:
  • 通过类Class中的forName()方法;
  • 通过类ClassLoader中的loadclass()方法;
  • 通过类ClassLoader中的findSystemClass()方法

出现这类错误也很好理解,就是当JVM要加载指定文件的字节码到内存时,并没由找到这个类对应的字节码,也就是说这个字节码.class文件不存在。解决方法就是检测在当前的classpath目录下有没有指定的文件存在,如果不知道classpath路径,就可以通过如下命令获取:

this.getClass().getClassLoader().getResource("").toString()

NoClassDefFoundError

这个异常在第一次使用命令执行Java类时很可能会碰到,如下面这种情况

java -cp example.jar Example

假如在这个jar包里面只有一个类,这个类时net.xx.Exmple ,那么原因很可能是你在命令行中没有加类的包名,正确的写法是这样的:

java cp example.jar net.xx.Example

在JVM的规范中描述了出现NoClassDefFoundError可能的情况就是使用new关键字、属性引用某个类、继承了某个接口或者类,以及方法的某个参数中引用了某个类,这个是出发JVM隐式加载这些类时发现这些类不存在的异常。

解决这个错误的方法就是确保这个类引用的类都在当前的classpath下面

ClassCastException

通常在程序中出现强制类型转换时出现这个错误。
JVM在做类型转换时会按照如下规则进行检查

  • 对于普通对象,对象必须时目标类的实例或目标类的子类的实例。如果目标是是接口,那么会把它当作实现了接口的一个子类。
  • 对于数组类型,目标类必须是数组类型或java.lang.Object、java.lang.Cloneable、java.io.Serializele

如果不满足上面的规则,JVM就会报这个错误。要避免这个错误有两种方式:

  • 在容器类型中现实的指明这个容器所包含的对象类型
  • 先通过instanceof检查是不是目标类型,然后再进行强制类型转换。

    ExceptionInInitializerError

    这个错误在JVM规范中是这样定义的:
  • 如果Java虚拟机试图创建类ExceptionInInitializerError的新实例,但是因为出现Out-Of-Memory-Error而无法创建新实例,那么就抛出OutOfMemoryError对象作为代替。
  • 如果初始化器抛出一些Excepton,而且Exception类不是Error或者它的某个子类,那么就会创建ExceptioinInInitializerError类的一个新实例,并用Exception作为参数,用这个实例代替Excepiton。

    UnsatisfiedLinkError

    这个异常倒不是很常见,但是出错的话,通常是在JVM启动的时候,如果一不小心将在JVM的某个lib删除了,可能就会报这个错误。

参考文献:
1 https://blog.csdn.net/u013412772/article/details/80837735