类加载

    xiaoxiao2022-07-02  86

    虚拟机把描述类的数据从Class文件加载到内存(方法区),并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制.

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用和卸载阶段.其中验证,准备,解析3个部分称为连接,发生顺序如下图所示:

    加载,验证,准备,初始化,卸载这5个阶段是按部就班开始的,但是不一定是顺序执行的,有可能是交叉进行的.

    解析阶段有可能在初始化之后,也有可能在初始化之前,这是为了支持java的运行时绑定的特性.

     

    什么情况下需要开始类加载过程的第一个阶段:加载? java虚拟机规范并没有进行强制约束,交给虚拟机具体实现来自由把握.但是对于初始化阶段,虚拟机规范严格规定有且只有以下5种情况必须立即进行对类进行初始化操作(而加载,验证,准备自然需要在此之前完成):

    使用new关键字实例对象的时候get或者set类的静态成员字段的时候(被final修饰的,或者在编译期间放入常量池的静态字段字段除外)调用类的静态方法的时候对类进行反射调用的时候,如果类没有初始化,则进行初始化初始化父类(如果父类没有初始化)当虚拟机启动的时候,指定要执行的类当使用JDK7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例后的解析结果REF_getstatic,REF_putstatic,REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有初始化

     举例如下:

    1.第一种情况

    public class Parent { static int value = 1; static { System.out.println("parent init"); } } public class Sub extends Parent{ static { System.out.println("Sub init"); } } public static void main(String []args) { System.out.println(Sub.value); }

    执行完main方法之后,最终输出的只有"parent init",首先get了Parent的静态字段,所以Parent类会进行初始化,但是value字段是属于Parent类的,所以Sub类是不会进行初始化的.

    2.第二种情况

    //依然使用上述的Parent和Sub类 public static void main(String []args) { Sub []arr = new Sub[]{}; }

    这种情况下,只是初始化了一个数组,并不会初始化上面2个之中的任何一个类.

    3.第三种情况

    public class Parent { final static int value = 1; static { System.out.println("parent init"); } } public class Sub extends Parent{ static { System.out.println("Sub init"); } } public class InitMain { public static void main(String []args) { System.out.println(Sub.value); } }

    这种情况下两个类中的static代码段都不会执行,因为修饰value的是final修饰符,final修饰的字段叫做静态常量,会直接放到方法区常量池,get的时候,不会触发类的加载.

    每个类编译后,都会生成一个class文件,在加载阶段,通过类的全限定名获取定义此类的二进制流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构.然后在内存中生成一个Class对象,作为方法区(类的信息都会存储到方法区里面)这个类的各种数据的访问入口.

    加载来源总结如下:

    从ZIP包中读取,常见的如JAR包,WAR包等等从网络中读取,这种场景最典型的应用就是Applet运行时计算生成,这种场景用的最多的就是java动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGeneretor.generateProxyClass类为特定接口生成形式为"$Proxy"的代理类的二进制字节流由其他文件生成,典型的就是JSP文件从数据库中读取

    类的验证阶段暂时不详细介绍,感兴趣的可以搜索了解一下.

    类的准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的的内存都将在方法区中进行分配.注意这个地方初始化的只是类的静态成员变量,实例变量此时不处理,实例变量会随着对象的初始化一起进行初始化,还有一点就是这个初始化仅是指对静态成员变量进行零值操作,不会执行赋值操作的.如下图就是一些基本类型的零值:

    相对初始零值来说,也有一些特殊情况,如果类的字段属性表中存在Constantvalue属性,那么这个变量的值就不会初始化为零值,而是直接设置成需要所赋的值,Constantvalue属性是指这个字段是有final修饰符修饰的.

    解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程. 符号引用是用一组符号来描述所引用的目标,可以是任何形式的字面量,只要能定位到目标即可.符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中. 直接引用可以是直接指向目标的指针,相对偏移量,或者是一个能间接定位到目标的句柄.如果有了直接引用,那引用的目标必定已经存在内存中过了.

    初始化

    初始化阶段,除了用户自定义的类加载器参与外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的java程序. 准备阶段已经进行了一次初始化(不是赋值操作),初始化阶段就是主观计划的去初始化变量和其他资源,初始化阶段是执行类构造器<cinit()>方法的过程. <cinit()>是所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句执行访问到定义在静态语句块 之前的变量,后面的静态变量是引用不到的. 虚拟机会保证子类的<cinit()>方法执行之前,先执行父类的<cinit()>方法,所以说父类的静态块优先于子类的静态变量执行. 如果一个类或者接口中无静态变量 静态代码块,也不会产生<cinit()>方法. 接口中的<cinit()>方法执行的时候,不需要先执行父类的接口,只有使用父类定义的变量的时候,才会初始化,实现类初始化的时候,也不会执行接口中的<cinit()>方法,只有用的时候才会初始化. 同一个类加载器下,一个类型只会初始化一次.

    类加载器

    类加载器通过类的全限定名获取二进制流来进行加载的.

    加载器大致分为3种:

        1.启动类加载器

            这个加载器负责将放在<JAVA_HOME>/lib目录下,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机能识别的类库加载到内存中.启动类加载器无法被java程序直接引用.

        2.扩展类加载器

           这个加载器负责将放在<JAVA_HOME>\lib\ext目录下,或者被java.ext.dirs系统变量所指定路径中的所有类库.

       3.应用程序类加载器

           这个类加载器由ClassLoader实现,负责加载用户类路径上的所指定类库,开发者可以使用这个类加载器,一般情况下默认的就是这个类加载器.当然,用户也可以自己定义类加载器,如果没有自定义的加载器就会使用应用程序类加载器.

    上面这些类加载器,有一种层次的关系,自上而下,启动类加载器->扩展类加载器->应用程序类加载器->自定义类加载器,这个层次关系也称之为双亲委托模型.当类库被加载的时候,在这种模式下,会优先给自己的上层类加载器去加载,一直向上抛,如果上层无法加载,就给下层类加载器加载.

     

    最新回复(0)