转载

聊聊Java类加载机制

Java面试中经常会问到Java类加载机制是什么样的,今天我们就从Java类加载器和类加载过程两方面来介绍一下,首先来说一下类的加载过程。

Java面试题中经常会问类的加载过程是什么样的,或者是通过给你一个程序,回答出程序的输出结果是什么,这些知识点都是Java类加载过程相关的,下面就来详细的说一说类加载的7个过程。

类加载过程

加载

将class文件加载到内存中,并在方法区创建对应的class对象

验证

校验加载的class文件是否符合字节码规范

准备

  • 完成验证阶段之后,jvm开始为类变量分配内存并初始化零值。
  • 「类变量」指的是被 static 修饰的变量。
  • 在准备阶段,JVM只会为「类变量」分配内存,而不会为「类成员变量」分配内存。
  • 但如果一个变量是常量(被 static final修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。因为final修饰的值一旦被赋值就不能再更改。

解析

JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

初始化

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

类加载总结

从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:

  • 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
  • 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
  • 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
  • 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。

如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。

类加载器

分类

  • 启动类加载器:Bootstrap ClassLoader,它用来加载Java的核心库,主要是<JAVA_HOME>/lib下的类。该加载器无法被Java程序直接引用。

  • 扩展类加载器:ExtClassLoader,它用来加载Java的扩展类,主要是<JAVA_HOME>/lib/ext下的类。

  • 应用类加载器:AppClassLoader,它负责加载用户类路径所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义自己的类加载器,那么这个就是程序中默认的类加载器。

聊聊Java类加载机制

双亲委派模型

提到类加载器,双亲委派模型是必然会提到的。双亲委派模型是指当我们调用类加载器的loadClass方法进行类加载时,该类加载器会首先请求它的父类加载器进行加载,依次递归。如果所有父类加载器都加载失败,则当前类加载器自己进行加载操作。这样做可以保证Java核心类的安全,比如说你不能够去覆盖java.lang.String类。

下面从loadClass的源码来看一下双亲委派模型是如何实现的

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先检查这个类是不是已经被加载了
        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) {
                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();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
复制代码

自定义类加载器

如果自定义类加载器不破坏双亲委派模型,只需要继承ClassLoader类并重写findClass方法即可。

如果自定义类加载器要破坏双亲委派模型,需要继承ClassLoader类并重写loadClass方法和findClass方法。

谈到破坏双亲委派模型,可能会被问到你知道有哪些打破双亲委派模型的例子吗,如何打破的?

  • Java SPI 在核心类库rt.jar的加载过程中需要加载第三方厂商的类(比如常用的数据库),直接指定使用应用程序类加载器来加载这些类。
  • Tomcat 中的 web 容器类加载器也破坏了双亲委托模式的,自定义的WebApplicationClassLoader除了核心类库外,都是优先加载自己路径下的Class
原文  https://juejin.im/post/5e9c330ae51d4546eb5257de
正文到此结束
Loading...