一文让你理解Class类加载机制

理解类加载机制

Class文件是各类编译器编译生成的二进制文件,在Class文件中描述了各类与该类相关的信息,可是Class文件自己是一个静态的东西,想要使用某个类的话,须要java虚拟机将该类对应的Class文件加载进虚拟机中以后才能进行运行和使用。java

举个例子,Class文件就比如是各个玩具设计商提供的设计方案,这些方案自己是不能直接给小朋友玩的,须要玩具生产商根据方案的相关信息制造出具体的玩具才能够给小朋友玩。那么不一样的设计商有他们本身的设计思路,只要最终设计出来的方案符合生产商生产的要求便可。生产商在生产玩具时,首先会根据本身的生产标准对设计商提交来的方案进行阅读,审核,校验等一系列步骤,若是该方案符合生产标准,则会根据方案建立出对应的模具,当经销商须要某个玩具时,生产商则拿出对应的模具生产出具体的玩具,而后把玩具提交给经销商。数组

对于java而言,虚拟机就是玩具生产商,设计商提交过来的方案就是一个个的Class文件,方案建立的模具就 总的来讲,类的加载过程,包括卸载在内的整个生命周期共有如下7个阶段:安全

加载、验证、准备、初始化、卸载这5个阶段的顺序是肯定的,可是解析阶段不必定,在某些状况下解析能够在初始化以后再执行,为了支持java的运行时绑定,也成为动态绑定或晚期绑定。invokedynamic指令就是用于动态语言支持,这里“动态”的含义是必须等到城市实际运行到这条指令的时候,解析动做才开始执行。bash

加载

“加载”是“类加载”过程当中的一个阶段,在加载阶段,虚拟机须要作如下3件事情:数据结构

  • 经过类的全限定名得到该类的二进制字节流多线程

  • 将这个字节流所表明的静态存储结构转换成方法区中的某个运行时数据结构spa

  • 在方法区内存(对于HotSpot虚拟机)中生成一个表明该类的java.lang.Class对象,做为访问方法区中该类的运行时数据结构的外部接口线程

加载阶段中“经过类的全限定名得到该类的二进制字节流”这个动做,被放到java虚拟机外部实现,目的是最大限度的让应用程序去决定该如何获取所需的类,而实现该动做的代码模块就是类加载器(ClassLoader),JVM提供了3种类加载器:设计

  • 启动类加载器(Bootstrap ClassLoader):负责加载 JAVAHOME\lib 目录中的,或经过-Xbootclasspath参数指定路径中的,且被虚拟机承认(按文件名识别,如rt.jar)的类。指针

  • 扩展类加载器(Extension ClassLoader):负责加载 JAVAHOME\lib\ext 目录中的,或经过java.ext.dirs系统变量指定路径中的类库。

  • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中了。

验证

加载完成后,紧接着(更确切的说是交叉执行)虚拟机会对加载的字节流进行验证。虚拟机若是不检查输入的字节流,对其安全信任的话,极可能会由于载入了有害的字节流而致使系统崩溃。验证阶段大体会完成4中不一样的检验动做:

文件格式验证

文件格式验证主要是校验该字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机所接受。这个阶段包括但不限于如下验证点:

  • 是否以魔数0xCAFEBABE开头
  • 主、次版本号是否在当前虚拟机处理的范围以内
  • 常量池中是否有不支持的常量类型(经过tag校验)
  • 常量的索引是否有指向不存在或不符合类型的常量
  • ...

元数据验证

元数据验证主要是对字节流中的描述信息(描述符)进行语义分析,以确保其描述的信息符合java语言规范的要求。这个阶段包括但不限于如下验证点:

  • 这个类是否有父类,除了java.lang.Object,全部的类都应该有父类
  • 这个类的父类是否继承了不容许被继承的类,如被final修饰的类
  • 若是这个类不是抽象类,是否实现了父类或接口中要求的全部的方法
  • ...

字节码验证

字节码验证主要是对类的方法体进行分析,确保在方法运行时不会有危害虚拟机的事件发生。这个阶段包括但不限于如下验证点:

  • 操做数栈的数据类型与指令码中所需类型是否相符
  • 校验跳转指令是否会跳转到方法体之外的字节码指令上
  • 校验方法体中类型转换是不是有效的
  • ...

符号引用验证

符号引用验证主要是对类自身之外的信息进行匹配新校验,包括常量池中的各类符号引用。这个阶段包括但不限于如下验证点:

  • 符号引用中经过字符串描述的全限定类名是否能找到对应的类
  • 在指定的类中是否存在符合方法的字段描述符以及简单名称所描述的字段和方法
  • ...

准备

准备阶段是为类变量(static)在方法区中分配内存并设置初始值(默认值,如int的默认值为0)的阶段。实例变量将会在对象实例化时随着对象一块儿分配在java堆中。为类变量设置初始值跟该变量是否有final修饰符有关系。 若是没用final进行修饰,以下列的代码:

// 准备阶段执行完成后,value变量的值为int的“零值”,即:0
// 把value赋值为10的putstatic指令是程序被编译后,
// 存放于类构造器<clinit>()方法中的,因此具体赋值的操做会在初始化阶段执行
public static int value = 10;
复制代码

若是使用了final进行修饰,以下列的代码:

// 若是类字段的字段属性表中有ConstantValue属性,
// 则会在准备阶段将变量的值初始化为ConstantValue所指定的值
// 准备阶段执行完成后,VALUE变量的值被赋值为20
public final static int VALUE = 20;
复制代码

解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程,在该阶段将会进行符号引用的校验。 符号引用是Class文件中用来描述所引用目标的符号,能够是任何形式的字面量。 直接引用是虚拟机在内存中引用具体类或接口的,能够是直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。 简单的来讲,符号引用是Class类文件用来定位目标的,直接引用是虚拟机用来在内存中定位目标的。

初始化

初始化阶段是执行类的构造器方法()的过程,类的构造方法由类变量的赋值动做和静态语句块按照在源文件中出现的顺序合并而成,该合并操做由编译器完成。

  • ()方法对于类或接口不是必须的,若是一个类中没有静态代码块,也没有静态变量的赋值操做,那么编译器不会生成()方法
  • ()方法与实例构造器方法()不一样,不须要显式的调用父类的方法,虚拟机会保证父类的优先执行
  • 为了防止屡次执行,虚拟机会确保方法在多线程环境下被正确的加锁同步执行,若是有多个线程同时初始化一个类,那么只有一个线程可以执行方法,其它线程进行阻塞等待,直到执行完成
  • 执行接口的方法是不须要先执行父接口的,只有使用父接口中定义的变量时,才会执行。

java虚拟机规范严格规定了有且只有一下5中状况必须当即对类进行初始化:

  1. 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,生成这4条指令的最多见的java代码场景是:使用new实例化对象时,读取或设置类的静态字段时,调用一个类的静态方法时
  2. 使用java.lang.reflect对类进行反射调用时,如经过Class.forName()建立对象时
  3. 当初始化一个类时,若是父类尚未初始化,则要先触发父类的初始化,即先要执行父类的构造器方法()
  4. 启动虚拟机时,须要初始化包含main方法的类
  5. 在JDK1.7中,若是java.lang.invoke.MethodHandler实例最后的解析结果REFgetStatic、REFputStatic、REF_invokeStatic的方法句柄,而且这个方法句柄对应的类没有进行初始化

如下几种状况,不会触发类初始化:

  • 经过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
class Parent {
    static int a = 100;    
    static   {        
        System.out.println("parent init!");    
    }
}
class Child extends Parent {    
    static {        
        System.out.println("child init!");    
    }
}
public class Init{      
    public static void main(String[] args){  
        // 只会执行父类的初始化,不会执行子类的初始化
        // 将打印:parent init!
        System.out.println(Child.a);      
    }
}
复制代码
  • 定义对象数组,不会触发该类的初始化。
public class Init{      
    public static void main(String[] args){ 
        // 不会有任何输出
        Parent[] parents = new Parent[10];    
    }  
}
复制代码
  • 常量在编译期间会存入调用类的常量池中,本质上并无直接引用定义常量的类,不会触发定义常量所在的类的初始化。
class Const {    
    static final int A = 100;    
    static {        
        System.out.println("Const init");    
    }
}
public class Init{      
    public static void main(String[] args){ 
        // Const.A会存入Init类的常量池中,调用时并不会触发Const类的初始化
        // 将打印:100
        System.out.println(Const.A);      
    }  
}
复制代码
  • 经过类名获取Class对象,不会触发类的初始化。
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args){ 
        // 不会打印任何信息
        Class catClazz = Class.class;      
    }  
}
复制代码
  • 经过Class.forName加载指定类时,若是指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args) throws ClassNotFoundException{ 
        // 不会打印任何信息
        Class catClazz = Class.forName("com.test.Cat",false,Cat.class.getClassLoader());      
    }  
}
复制代码
  • 经过ClassLoader默认的loadClass方法,也不会触发初始化动做
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args) throws ClassNotFoundException{ 
        // 不会打印任何信息
        new ClassLoader(){}.loadClass("com.test.Cat");      
    }  
}
复制代码

最后,附上一幅Class类加载过程的思惟导图:

是Class文件加载进虚拟机中的类,生产的玩具就是类的实例对象。

所以,从Class文件到对象须要通过的步骤大体为: Class文件-->类-->实例对象 而类的加载机制,就是负责将Class文件转换成虚拟机中的类的一个过程。

总的来讲,类的加载过程,包括卸载在内的整个生命周期共有如下7个阶段:

加载、验证、准备、初始化、卸载这5个阶段的顺序是肯定的,可是解析阶段不必定,在某些状况下解析能够在初始化以后再执行,为了支持java的运行时绑定,也成为动态绑定或晚期绑定。invokedynamic指令就是用于动态语言支持,这里“动态”的含义是必须等到城市实际运行到这条指令的时候,解析动做才开始执行。

加载

“加载”是“类加载”过程当中的一个阶段,在加载阶段,虚拟机须要作如下3件事情:

  • 经过类的全限定名得到该类的二进制字节流

  • 将这个字节流所表明的静态存储结构转换成方法区中的某个运行时数据结构

  • 在方法区内存(对于HotSpot虚拟机)中生成一个表明该类的java.lang.Class对象,做为访问方法区中该类的运行时数据结构的外部接口

加载阶段中“经过类的全限定名得到该类的二进制字节流”这个动做,被放到java虚拟机外部实现,目的是最大限度的让应用程序去决定该如何获取所需的类,而实现该动做的代码模块就是类加载器(ClassLoader),JVM提供了3种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):负责加载 JAVAHOME\lib 目录中的,或经过-Xbootclasspath参数指定路径中的,且被虚拟机承认(按文件名识别,如rt.jar)的类。

  • 扩展类加载器(Extension ClassLoader):负责加载 JAVAHOME\lib\ext 目录中的,或经过java.ext.dirs系统变量指定路径中的类库。

  • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中了。

验证

加载完成后,紧接着(更确切的说是交叉执行)虚拟机会对加载的字节流进行验证。虚拟机若是不检查输入的字节流,对其安全信任的话,极可能会由于载入了有害的字节流而致使系统崩溃。验证阶段大体会完成4中不一样的检验动做:

文件格式验证

文件格式验证主要是校验该字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机所接受。这个阶段包括但不限于如下验证点:

  • 是否以魔数0xCAFEBABE开头
  • 主、次版本号是否在当前虚拟机处理的范围以内
  • 常量池中是否有不支持的常量类型(经过tag校验)
  • 常量的索引是否有指向不存在或不符合类型的常量
  • ...

元数据验证

元数据验证主要是对字节流中的描述信息(描述符)进行语义分析,以确保其描述的信息符合java语言规范的要求。这个阶段包括但不限于如下验证点:

  • 这个类是否有父类,除了java.lang.Object,全部的类都应该有父类
  • 这个类的父类是否继承了不容许被继承的类,如被final修饰的类
  • 若是这个类不是抽象类,是否实现了父类或接口中要求的全部的方法
  • ...

字节码验证

字节码验证主要是对类的方法体进行分析,确保在方法运行时不会有危害虚拟机的事件发生。这个阶段包括但不限于如下验证点:

  • 操做数栈的数据类型与指令码中所需类型是否相符
  • 校验跳转指令是否会跳转到方法体之外的字节码指令上
  • 校验方法体中类型转换是不是有效的
  • ...

符号引用验证

符号引用验证主要是对类自身之外的信息进行匹配新校验,包括常量池中的各类符号引用。这个阶段包括但不限于如下验证点:

  • 符号引用中经过字符串描述的全限定类名是否能找到对应的类
  • 在指定的类中是否存在符合方法的字段描述符以及简单名称所描述的字段和方法
  • ...

准备

准备阶段是为类变量(static)在方法区中分配内存并设置初始值(默认值,如int的默认值为0)的阶段。实例变量将会在对象实例化时随着对象一块儿分配在java堆中。为类变量设置初始值跟该变量是否有final修饰符有关系。 若是没用final进行修饰,以下列的代码:

// 准备阶段执行完成后,value变量的值为int的“零值”,即:0
// 把value赋值为10的putstatic指令是程序被编译后,
// 存放于类构造器<clinit>()方法中的,因此具体赋值的操做会在初始化阶段执行
public static int value = 10;
复制代码

若是使用了final进行修饰,以下列的代码:

// 若是类字段的字段属性表中有ConstantValue属性,
// 则会在准备阶段将变量的值初始化为ConstantValue所指定的值
// 准备阶段执行完成后,VALUE变量的值被赋值为20
public final static int VALUE = 20;
复制代码

解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程,在该阶段将会进行符号引用的校验。 符号引用是Class文件中用来描述所引用目标的符号,能够是任何形式的字面量。 直接引用是虚拟机在内存中引用具体类或接口的,能够是直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。 简单的来讲,符号引用是Class类文件用来定位目标的,直接引用是虚拟机用来在内存中定位目标的。

初始化

初始化阶段是执行类的构造器方法()的过程,类的构造方法由类变量的赋值动做和静态语句块按照在源文件中出现的顺序合并而成,该合并操做由编译器完成。

  • ()方法对于类或接口不是必须的,若是一个类中没有静态代码块,也没有静态变量的赋值操做,那么编译器不会生成()方法
  • ()方法与实例构造器方法()不一样,不须要显式的调用父类的方法,虚拟机会保证父类的优先执行
  • 为了防止屡次执行,虚拟机会确保方法在多线程环境下被正确的加锁同步执行,若是有多个线程同时初始化一个类,那么只有一个线程可以执行方法,其它线程进行阻塞等待,直到执行完成
  • 执行接口的方法是不须要先执行父接口的,只有使用父接口中定义的变量时,才会执行。

java虚拟机规范严格规定了有且只有一下5中状况必须当即对类进行初始化:

  1. 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,生成这4条指令的最多见的java代码场景是:使用new实例化对象时,读取或设置类的静态字段时,调用一个类的静态方法时
  2. 使用java.lang.reflect对类进行反射调用时,如经过Class.forName()建立对象时
  3. 当初始化一个类时,若是父类尚未初始化,则要先触发父类的初始化,即先要执行父类的构造器方法()
  4. 启动虚拟机时,须要初始化包含main方法的类
  5. 在JDK1.7中,若是java.lang.invoke.MethodHandler实例最后的解析结果REFgetStatic、REFputStatic、REF_invokeStatic的方法句柄,而且这个方法句柄对应的类没有进行初始化

如下几种状况,不会触发类初始化:

  • 经过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
class Parent {
    static int a = 100;    
    static   {        
        System.out.println("parent init!");    
    }
}
class Child extends Parent {    
    static {        
        System.out.println("child init!");    
    }
}
public class Init{      
    public static void main(String[] args){  
        // 只会执行父类的初始化,不会执行子类的初始化
        // 将打印:parent init!
        System.out.println(Child.a);      
    }
}
复制代码
  • 定义对象数组,不会触发该类的初始化。
public class Init{      
    public static void main(String[] args){ 
        // 不会有任何输出
        Parent[] parents = new Parent[10];    
    }  
}
复制代码
  • 常量在编译期间会存入调用类的常量池中,本质上并无直接引用定义常量的类,不会触发定义常量所在的类的初始化。
class Const {    
    static final int A = 100;    
    static {        
        System.out.println("Const init");    
    }
}
public class Init{      
    public static void main(String[] args){ 
        // Const.A会存入Init类的常量池中,调用时并不会触发Const类的初始化
        // 将打印:100
        System.out.println(Const.A);      
    }  
}
复制代码
  • 经过类名获取Class对象,不会触发类的初始化。
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args){ 
        // 不会打印任何信息
        Class catClazz = Class.class;      
    }  
}
复制代码
  • 经过Class.forName加载指定类时,若是指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args) throws ClassNotFoundException{ 
        // 不会打印任何信息
        Class catClazz = Class.forName("com.test.Cat",false,Cat.class.getClassLoader());      
    }  
}
复制代码
  • 经过ClassLoader默认的loadClass方法,也不会触发初始化动做
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args) throws ClassNotFoundException{ 
        // 不会打印任何信息
        new ClassLoader(){}.loadClass("com.test.Cat");      
    }  
}
复制代码

最后,附上一幅Class类加载过程的思惟导图:

相关文章
相关标签/搜索