美文网首页JAVA
Java类加载机制

Java类加载机制

作者: zhong0316 | 来源:发表于2019-02-21 09:40 被阅读39次

    Java中一个类的生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段。其中准备、解析和初始化统称为连接。七个阶段的顺序如下:

    类的生命周期

    加载

    类加载有以下三个阶段:

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

    验证

    验证阶段主要校验Class字节流是否符合当前虚拟机的规范,分为文件格式验证、元数据验证、字节码验证、符号引用验证。

    文件格式验证

    • 是否以魔术0xCAFEBABE开头
    • 主、次版本号是否在当前虚拟机处理范围之内
    • 常量池的常量中是否有不被支持的常量类型
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
      ...

    元数据验证

    主要对字节码描述的信息进行语义分析,保证其描述符合Java语言的要求。主要进行以下验证:

    • 类是否有父类(除了Object之外,其他类都应该有父类)
    • 是否继承了不允许被继承的类(final修饰过的类)
    • 如果这个类不是抽象类,是否实现其父类或接口中所有要求实现的方法
    • 类中的字段、方法是否与父类产生矛盾(如:覆盖父类final类型的字段,或者不符合个则的方法)

    字节码验证

    最复杂的一个阶段。主要目的是通过数据量和控制流分析,确定程序语义是合法的,符合逻辑的。
    保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。

    符号引用验证

    符号引用中通过字符串描述的全限定名是否能找到对应的类。
    在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
    符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

    准备

    准备阶段正式为类变量分配内存并设置初始值阶段。

    public static int value=123;初始后为 value=0;

    对于static final类型,在准备阶段会被赋予正确的值:public static final value=123;初始化为 value=123;

    如果是boolean值默认赋值为:false。

    如果是对象引用默认赋值为:null。

    解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

    符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能够无歧义地定位到目标即可。

    直接引用:直接引用可以是指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

    解析又分为:类或者接口的解析、字段解析和接口方法解析。

    初始化

    • 执行类构造器<clinit>
    • 初始化静态变量、静态块中的数据等(一个类加载器只会初始化一次)
    • 子类的<clinit>调用前保证父类的<clinit>被调用

    双亲委派模型

    JVM中主要有三种类型的类加载器:

    1. 启动类加载器(BootStrap ClassLoader):引导类装入器是用本地代码实现的类装入器,它负责将 jdk中jre/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
    2. 扩展类加载器(Extension ClassLoader):扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将jdk中
      jre/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
    3. 应用程序类加载器(Application ClassLoader):系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

    在双亲委派模型中,如果一个类加载器收到了一个类的加载请求,它会首先委派给父类加载器来加载这个类,如果父类加载器加载不了再由自己来加载。这样所有的加载请求最终都会传送到顶层的启动类加载器中。这个过程如下图所示:

    双亲委派模型

    双亲委派模型的好处

    Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。

    ClassLoader

    上面介绍了Java中的三种类型的类加载器,其中扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)的类结构如下图所示:

    ClassLoader

    可以看到扩展类加载器和应用程序类加载器都继承了ClassLoader抽象类。

    public abstract class ClassLoader {
    
        // 类加载器的父类,收到类加载请求后先委派给parent进行类加载,失败后再由自己加载
        private final ClassLoader parent;
        
        // 此加载器已经加载过的类,这个表的唯一作用是保持对已经加载过的类的强引用。还记得之前我们说过一个Class被
        // GC的条件吗?其中一项就是加载这个Class的ClassLoader已经被GC,如果加载这个Class的ClassLoader还未被GC
        // 则当前Class不能被GC,就是因为这里保持了对加载过的类的强引用。
        private final Vector<Class<?>> classes = new Vector<>();    
        
        // 加载类的逻辑
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) { // 首先获取要加载的类的类加载锁
                // First, check if the class has already been loaded
                
                // 检查对应的Class是否已经被加载过,注意这里的findLoadedClass调用的是一个native方法,而不是上面所说的classes
                Class<?> c = findLoadedClass(name); 
                if (c == null) { // 如果还未被加载,则加载
                    long t0 = System.nanoTime();
                    try {
                        // 首先如果这个加载器的parent不为null(只有启动类加载器的parent为空),则委派给这个类加载器的父类加载器来进行加载
                        if (parent != null) {
                            c = parent.loadClass(name, false); // 调用parent来处理这个加载请求
                        } else {
                            c = findBootstrapClassOrNull(name); // 如果parent为空,则证明当前类加载器是启动类加载器
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // 如果父类加载器没有加载到Class,则由当前类加载器自己去加载
                        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中的classes用于保持对它已经加载过的类的引用,这就保证了一个Class被GC的必要提交就是加载这个Class的ClassLoader已经被GC了,否则这个Class就还有强引用,不能被GC。

    在loadClass方法中首先检查当前的Class是否已经加载过,如果没有加载则进行加载,否则直接返回这个Class。
    首先检查类加载器的parent是否为空,如果不为空交由parent来加载这个类,如果parent为空则证明当前类加载器为启动类加载器。
    如果父类加载器加载失败,则由当前类加载器自己去加载,也就是findClass方法,这个方法在ClassLoader中没有具体的实现,默认是抛ClassNotFoundException异常,需要子类去实现自己的加载逻辑。
    最终判断加载完成的Class是否需要进行解析,如果需要解析则进行解析,然后返回这个Class。

    参考资料

    《深入理解Java虚拟机——JVM高级特性与最佳实践》-周志明

    相关文章

      网友评论

        本文标题:Java类加载机制

        本文链接:https://www.haomeiwen.com/subject/mjwlyqtx.html