阿里面试题,深刻理解Java类加载机制

类的生命周期

包括如下 7 个阶段:java

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

其中解析过程在某些状况下能够在初始化阶段以后再开始,这是为了支持 Java 的动态绑定。git

类初始化时机

虚拟机规范中并无强制约束什么时候进行加载,可是规范严格规定了有且只有下列五种状况必须对类进行初始化(加载、验证、准备都会随着发生):程序员

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,若是类没有进行过初始化,则必须先触发其初始化。最多见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。github

  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,若是类没有进行初始化,则须要先触发其初始化。数据库

  • 当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。数组

  • 当虚拟机启动时,用户须要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;安全

  • 当使用 JDK 1.7 的动态语言支持时,若是一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化;服务器

以上 5 种场景中的行为称为对一个类进行主动引用。除此以外,全部引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:网络

  • 经过子类引用父类的静态字段,不会致使子类初始化。
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
  • 经过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
SuperClass[] sca = new SuperClass[10];
  • 常量在编译阶段会存入调用类的常量池中,本质上并无直接引用到定义常量的类,所以不会触发定义常量的类的初始化。
System.out.println(ConstClass.HELLOWORLD);

类加载过程

包含了加载、验证、准备、解析和初始化这 5 个阶段。多线程

1. 加载

加载是类加载的一个阶段,注意不要混淆。

加载过程完成如下三件事:

  • 经过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所表明的静态存储结构转化为方法区的运行时存储结构。
  • 在内存中生成一个表明这个类的 Class 对象,做为方法区这个类的各类数据的访问入口。

其中二进制字节流能够从如下方式中获取:

  • 从 ZIP 包读取,这很常见,最终成为往后 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,这种场景最典型的应用是 Applet。
  • 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其余文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
  • 从数据库读取,这种场景相对少见,例若有些中间件服务器(如 SAP Netweaver)能够选择把程序安装到数据库中来完成程序代码在集群间的分发。 ...

2. 验证

确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。

  • 文件格式验证:验证字节流是否符合 Class 文件格式的规范,而且能被当前版本的虚拟机处理。
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
  • 字节码验证:经过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
  • 符号引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身之外(常量池中的各类符号引用)的信息进行匹配性校验。

3. 准备

类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。

实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一块儿分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在全部实例化操做以前,而且类加载只进行一次,实例化能够进行屡次)

初始值通常为 0 值,例以下面的类变量 value 被初始化为 0 而不是 123。

public static int value = 123;

若是类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。

public static final int value = 123;

4. 解析

将常量池的符号引用替换为直接引用的过程。

5. 初始化

初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。

在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员经过程序制定的主观计划去初始化类变量和其它资源。

<clinit>() 方法具备如下特色:

  • 是由编译器自动收集类中全部类变量的赋值动做和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它以前的类变量,定义在它以后的类变量只能赋值,不能访问。例如如下代码:
public class Test {
    static {
        i = 0;                // 给变量赋值能够正常编译经过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}
  • 与类的构造函数(或者说实例构造器 <init>())不一样,不须要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行以前,父类的 <clinit>() 方法已经执行结束。所以虚拟机中第一个执行 <clinit>() 方法的类确定为 java.lang.Object。

  • 因为父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操做。例如如下代码:

static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

public static void main(String[] args) {
     System.out.println(Sub.B);  // 输出结果是父类中的静态变量 A 的值,也就是 2。
}
  • <clinit>() 方法对于类或接口不是必须的,若是一个类中不包含静态语句块,也没有对类变量的赋值操做,编译器能够不为该类生成 <clinit>() 方法。

  • 接口中不可使用静态语句块,但仍然有类变量初始化的赋值操做,所以接口与类同样都会生成 <clinit>() 方法。但接口与类不一样的是,执行接口的 <clinit>() 方法不须要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的 <clinit>() 方法。

  • 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,若是多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。若是在一个类的 <clinit>() 方法中有耗时的操做,就可能形成多个线程阻塞,在实际过程当中此种阻塞很隐蔽。

类加载器

实现类的加载动做。在 Java 虚拟机外部实现,以便让应用程序本身决定如何去获取所须要的类。

类与类加载器

两个类相等:类自己相等,而且使用同一个类加载器进行加载。这是由于每个类加载器都拥有一个独立的类名称空间。

这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字作对象所属关系断定结果为 true。

类加载器分类

从 Java 虚拟机的角度来说,只存在如下两种不一样的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;

  • 全部其余类的加载器,这些类由 Java 实现,独立于虚拟机外部,而且全都继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度看,类加载器能够划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,而且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即便放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器没法被 Java 程序直接引用,用户在编写自定义类加载器时,若是须要把加载请求委派给启动类加载器,直接使用 null 代替便可。

  • 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的全部类库加载到内存中,开发者能够直接使用扩展类加载器。

  • 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。因为这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以通常称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者能够直接使用这个类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。

双亲委派模型

应用程序都是由三种类加载器相互配合进行加载的,若是有必要,还能够加入本身定义的类加载器。

下图展现的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其他的类加载器都应有本身的父类加载器。这里类加载器之间的父子关系通常经过组合(Composition)关系来实现,而不是经过继承(Inheritance)的关系实现。

(一)工做过程

一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器没法完成类加载请求时才尝试加载。

(二)好处

使得 Java 类随着它的类加载器一块儿具备一种带有优先级的层次关系,从而是的基础类获得统一。

例如 java.lang.Object 存放在 rt.jar 中,若是编写另一个 java.lang.Object 的类并放到 ClassPath 中,程序能够编译经过。由于双亲委派模型的存在,因此在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,由于 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。正由于 rt.jar 中的 Object 优先级更高,由于程序中全部的 Object 都是这个 Object。

(三)实现

如下是抽象类 java.lang.ClassLoader 的代码片断,其中的 loadClass() 方法运行过程以下:先检查类是否已经加载过,若是没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试本身去加载。

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                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
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

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

自定义类加载器实现

FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),而后读取该文件内容,最后经过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。

java.lang.ClassLoader 类的方法 loadClass() 实现了双亲委派模型的逻辑,所以自定义类加载器通常不去重写它,而是经过重写 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

免费Java资料须要本身领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G。 
传送门: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q