类型的加载、连接和初始化过程都是程序运行期间完成的,这样就导致类加载时增加了一些性能开销,但是为Java应用程序提供了高度的灵活性。
类的加载被分为了七个阶段:
加载–>验证–>准备–>解析–>初始化–>使用–>卸载
验证–>准备–>解析三个阶段在一起被称为连接,加载与连接是会交叉进行的(如加载的后期可能已经开始了验证)
其中解析是可能出现在初始化后面的(为了支持Java的动态绑定)
1 加载
加载步骤:
-
通过一个类的全限定名来获取定义此类的二进制字节流(用名字找到字节流)
- 通过全限定名来获得字节流,不指明从哪里获得,怎么获得,极大的提高了灵活度,这是例如WAR,JAR格式基础,Applet(从网络获取),动态代理技术,JSP等技术的基础
-
将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构(字节流–>方法区)
- 方法区中的数据格式由虚拟机自定义
-
内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类各种数据的访问入口(创建对应Class对象)
- 虽然是对象,但对于像HotSpot而言,把它放在方法区中。
数组类的加载:
对于非数组类的加载,使用系统提供的引导类加载器去完成。但数组类由Java虚拟机直接创建(但数组类的元素类型(数组中存放类的类型)还是要考系统提供的引导类加载器完成)
- 若元素类型为引用类型,用非数组类的方法去加载它的元素类型对应的类,数组接下来还会被这个加载器的类名称空间中被标记
- 元素类型不是引用类型(如基本数据类型int),虚拟机会把数组标记与引导类加载器关联
- 数组类与其元素类型的可见性一样,元素类型不是引用类的话默认为public。
2 验证
验证阶段主要作用是作为安全保证,确保代码中没有违规操作或其他有危害的部分。
验证阶段工作很多,简诉四个过程。
- 文件格式验证
确保字节流文件是否符合class文件的结构,未缺省或添加,其次是各部分数据是否符合规定(如魔数是否为0xCAFEBABE,版本号是否超过处理范围,常量池中的常量是否有问题(常量类型不支持、索引对应常量不存在,编码规则等)等…) - 元数据验证
进行语义分析,检查描述信息是否符合Java规范,如是否有父类(除了Object都有),是否继承了不可继承的类,不是抽象类的话是否实现了接口等中的方法,字段方法是否与父类中存在冲突等… - 字节码验证
最复杂的验证。是对类的方法体进行校验分析,确保指令不会存在超范围跳转,操作了错误类型的数据,类型转换是否有效等…
由于工作量巨大,JDK1.6以后进行了优化,为Code属性加了一个StackMapTable属性来记录方法体中所有基本块应有的状态,这样就能直接对应检查,不需要分析程序了。 - 符号引用验证(发生在第三阶段解析中)
使用的符号引用所表述的全限定名是否能找到对应的类,其内部的方法和字段是否符号字段的描述,类,方法,字段的访问权限是否够等…
验证阶段非常重要,但不是必要的操作。
3 准备
准备阶段的工作十分简单,为类变量(static修饰的变量,不是所有的变量哦)分配内存,并设初值(设初值,全部是零值,并不是初始化哦),但对于像存在final修饰的类变量会带有属性表附加表示其变量的值,这个时候赋的初值就不是零值了,而是属性表附带的值。
4 解析
解析的工作就是把符号引用转换为直接引用。主要转换的就是类,接口,类方法,接口方法,方法类型,方法句柄,调用点限定符。
- 符号引用:描述所引用目标的字面量(任何形式都行,只要能无歧义的定位目标)
- 直接引用:直接指向目标的指针,相对便宜或者能间接定位的句柄。
解析过程,不管是前面提到的那种引用,到需要先定义到具体的类或接口(当然还要区分是数组类还是非数组类)。
- 如果是类引用,到这里确定解析类或接口有效且访问权限没问题就直接返回了。
- 如果是方法或字段的引用,先找本类会接口的简单名称或字段描述符和方法就会找从下往上找其所有父类接口简单名称或字段描述符和方法,再找所有父类中简单名称或字段描述符和方法,若找到且有访问权限就成功返回,不然返回异常。(需要注意的是如果找类方法结果解析出来是接口或者找接口方法解析出来是类同样会返回异常)
5 初始化
类初始化是类加载的最后一步,初始化中最先执行的就是<clinit>()
方法
<clinit>()
:编译器自动收集类中所有类变量的赋值动作和静态语句块合并产生,收集顺序按照赋值语句在源文件中出现顺序决定(静态块前面的变量,静态块可以赋值,不能访问(可写不可读)),<clinit>()
方法执行时与类的构造函数<init>()
不同,<init>()
执行是需要显示的调用其父类的构造方法,而<clinit>()
则是按照先父类后子类的顺序来的,所以最先执行永远是Object类的<clinit>()
方法(所以父类的赋值操作是先于子类的)
<clinit>()
对于类与接口不是必须的,若没有类变量的赋值动作和静态语句块,那么编译器不会产生<clinit>()
方法。- 接口没有静态块,在有类变量的赋值动作才会生成
<clinit>()
方法,且执行时不会先执行父接口的<clinit>()
方法,同理实现类也不会先执行接口的<clinit>()
方法。 - 多线程中,若多个线程同时初始化一个类,那只有一个线程可以去初始化,剩下的阻塞等待,且唤醒完后同样不会去初始化(因为已经被其他线程初始化了)。
- 类实例创建过程:首先执行父类的初始化块部分,然后是父类的构造方法,再执行子类的初始化块,最后是子类的构造方法
- 类实例销毁时,先销毁子类部分,再销毁父类部分。
虚拟机规定**有且仅有5种情况(其他情况都不会触发)**会触发类的初始化。
- 遇到new(使用new关键字),getstatic,putstatic(读写一个类的静态字段,除开被final修饰静态字段),invokestatic(调用一个类的静态方法时)这四条字节码指令时,对应操作的类若没有初始化会触发这个类的初始化。(要使用一个类,要确保这个类是被初始化了的)
- 通过java.lang.reflect包的方法对类进行反射调用时,若类没有初始化,需要进行初始化(使用反射,要确保反射的类是被初始化了的)
- 初始化化一个类时,若其父类没有被初始化,要初始化父类(初始化顺序是先父后子,从高到低)
- 虚拟机启动时,需要先初始化主类(带main()方法的那个类)(主类优先要被初始化)
- JDK1.7及以后,若java.lang.invoke.MethodHandle实例最后的解析结构是REF_getstatic,REF_putstatic,REF_invokestatic的方法句柄,且此方法句柄对应的类没有被初始化,那类会被初始化(java.lang.invoke.MethodHandle实例产生的解析结构的方法会产生第一种情况的那种指令,所以会涉及初始化)
除了上述五种情况,其他情况均不会触发初始化,称为被动引用,三个典型例子
- 若通过子类去访问父类的静态字段,只会初始化父类,不会初始化子类
- 用数组来定义类(如:String[]),不会触发此类(String)的初始化,而是触发另一个类(虚拟机自定义:[java.lang.String)的初始化
- 访问类中被final修饰的静态字段,不会触发类的初始化,因为编译时会进行常量优化,类中引用另一个类的静态字段时,会把这个静态字段的放入本身类的常量池,所以访问实则不需要去另一个类中,本身的常量池中就能找到值。
前面提到,加载是独立在虚拟机外部实现的动作,由类加载器来实现,这大大提高了Java的灵活性。
6 类与类加载器
类加载器只用于实现类的加载动作,但作用却远不止此,判断两个类是否相等,相等的前提条件是两个类由同一个类加载器加载(不然就算两个类来着与同一个class文件,也会判断不相等),这里相等包含Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果。
7 双亲委派模型
前面提到了,同一个class文件若被不同的类加载器加载会导致两个类不相等,而由于自定义类加载器的存在,这可能导致许多最基本的行为无法保证,应用程序变得混乱(如自定义加载器加载Object类到不同路径下,导致不相等,会导致需要混乱后果)
于是引入了双亲委派模型,首先要说一下三种系统提供的类加载器
-
启动类加载器Bootstrap ClassLoader:
是嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负载加载JAVA_HOME/lib下的类库,启动类加载器无法被应用程序直接使用。 -
扩展类加载器Extension ClassLoader:
该加载器器是用JAVA编写,且它的父类加载器是Bootstrap(但用代码查看父类返回null,因为BootstrapClassLoader是用C++语言写的),是由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext目录中的类库。开发者可以这几使用扩展类加载器。 -
应用程序类加载器(Application ClassLoader),也称为应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件(用户写代码路径下)。它的父加载器为Extension ClassLoader。
自定义加载器的父类加载器是应用程序类加载器(Application ClassLoader),但类加载器之间的父子关系不是使用继承来实现的,而是使用组合关系来复用父加载器的代码。
双亲委派模型:
如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载。
委派的好处:
- 类加载器带有优先级的层次关系,避免有些类被重复加载。
- 保证了Java程序的稳定运行
- 实现简单