Class文件最终都要加载到虚拟机中才能被使用,本文即介绍虚拟机的类加载过程;
类加载的时机:
虚拟机明确规范,有且只有5中情况会进行类的加载:1.使用new关键字实例化对象的时候,使用到类的静态变量的时候(被final修饰的静态变量除外),以及调用类的静态方法的时候;2.使用反射操作类的时候,如果类没有初始化,则先初始化类,3.如果初始化一个类,发现其父类没有初始化,则首先初始化父类,4.当虚拟机初始化的时候会执行主类(包含main方法的类),故虚拟机要首先加载这个类;5.使用方法句柄操作类的静态方法时,如果该类没有被创建,则要创建该类;
接口的加载过程与类的加载有些区别,初始化一个接口时并不要求其父接口也被初始化,只有被使用到的接口才会被加载;
类的加载过程:
加载:加载阶段主要做三件事儿:1.通过类的全限定名获取此类的二进制字节流;2将二进制字节流确定的静态存储结构转化为方法区的运行时数据结构;3在内存中生成这个类的Class对象(对于HotSpot虚拟机,这个对象生成在方法区中)作为方法区中这个类的各种数据的访问入口;
验证:验证是连接的第一部分,主要是确保二进制流符合当前虚拟机的要求,同时确保安全性;验证过程包括四个动作:1.文件格式验证,比如魔数,版本,常量池常量的类型等;2元数据的验证,该动作是对类进行语意分析,如这个类是否有父类,是否继承了final修饰的类,非抽象类是否继承了抽象父类的所有方法等,确保这些描述符合java虚拟机规范;3字节码验证,通过控制流和数据流确定程序语意是合法的,符合逻辑的;4.符号引用验证,最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验下列内容:符号引用中通过字符串描述的全限定名是否能找到对应的类。在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问等;
准备:该阶段正式为类变量分配空间并设置初始值,该空间在方法区中,这个初始值指的是数据类型对应的零值,但如果变量同时被static和final赋值,则会在该阶段显示的赋值;
解析:该阶段将符号引用转换为直接引用;符号引用:用一组符号来描述所引用的目标,符号可以是任意的字面量,只要能无歧义的确定目标即可;直接引用:是目标在内存中的地址,如果有直接引用,则目标肯定已经存在于内存中;大多数情况下,解析一次后会将结果缓存起来,下一次需要时直接去缓存中的直接引用即可;解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行;
初始化:这是类加载的最后一个步骤,这一阶段,类中的静态变量将被显示的赋值;
类加载器简述:
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果;
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现 [1] ,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
对于程序员来说,类加载器可以分为三类:1.启动类加载器(Bootstrap ClassLoader),这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可;2.扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。3.应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher $App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
类加载器之间的层次关系称为类加载器的双亲委派模型(ParentsDelegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派模型的最大的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。