美文网首页
类加载机制

类加载机制

作者: 永远的太阳0123 | 来源:发表于2019-01-30 20:13 被阅读0次
    1 JVM整体的运行原理

    (1)首先将“.java”代码文件,编译成“.class”字节码文件。
    (2)类加载器把“.class”字节码文件中的类加载到JVM中。
    (3)JVM来执行我们写好的那些类中的代码。

    2 类加载的时机

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。

    类的生命周期
    加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。注意,这里写的是按部就班地“开始”,而不是按部就班地“进行”或“完成”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。
    什么情况下需要开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
    (1)使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候、以及调用一个类的静态方法的时候。
    (2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    (3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    (4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
    (5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
    注意:
    (1)对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类来引用父类中定义的静态字段,不会导致子类的初始化。
    public class SuperClass {
    
        static {
            System.out.println("SuperClass init!");
        }
    
        public static int value = 123;
    
    }
    
    public class SubClass extends SuperClass {
        
        static {
            System.out.println("SubClass init!");
        }
        
    }
    
    public class NotInitialization {
        
        public static void main(String[] args) {
            // 输出SuperClass init!
            // 输出123
            System.out.println(SubClass.value);
        }
    
    }
    

    (2)通过数组定义来引用类,不会触发此类的初始化。

    public class SuperClass {
    
        static {
            System.out.println("SuperClass init!");
        }
    
        public static int value = 123;
    
    }
    
    public class NotInitialization {
            
        public static void main(String[] args) {
            // 没有输出
            SuperClass[] sca = new SuperClass[10];
        }
    
    }
    

    (3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

    public class ConstClass {
        
        static {
            System.out.println("ConstClass init!");
        }
        
        public static final String HELLOWORLD = "hello world";
    
    }
    
    public class NotInitialization {
            
        public static void main(String[] args) {
            // 只输出hello world
            System.out.println(ConstClass.HELLOWORLD);
        }
    
    }
    

    接口也有初始化过程,这点与类是一致的。接口与类真正有所区别的是前面讲述的5种“有且仅有”需要开始初始化场景中的第3种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

    3 类加载的过程
    3.1 加载

    加载是类加载过程的一个阶段。在加载阶段,虚拟机需要完成一下3件事情:
    (1)通过一个类的全限定名来获取定义此类的二进制字节流。
    (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    (3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    3.2 验证

    验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    验证阶段大致上会完成4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

    3.3 准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
    这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆内存中。
    假设一个类变量的定义为“public static int value = 123;”,那变量value在准备阶段过后的初始值为0而不是123;假设上面类变量value的定义变为“public static final int value = 123;”,在准备阶段虚拟机就会将value赋值为123。

    3.4 解析

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

    3.5 初始化

    初始化阶段是执行类构造器<clinit>()方法的过程。
    (1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
    (2)虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
    (3)由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
    (4)<clinit>()方法对于类或接口来说并不是必需的。
    (5)接口中不能使用静态语句块,但仍然有变量初始化的赋值操作。接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
    (6)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类地<clinit>()方法,其它线程都需要阻塞等待,直到活动现场执行<clinit>()方法完毕。需要注意的是,其它线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其它线程唤醒之后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。

    4 类加载器

    类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗地说,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类必定不相等。

    4.1 类加载器的种类

    (1)启动类加载器(Bootstrap ClassLoader):C++编写。加载$JAVAHOME/jre/lib中的所有类库。
    (2)扩展类加载器(Extension ClassLoader):Java编写。加载$JAVAHOME/jre/lib/ext或者系统变量java.ext.dirs指定的目录中的所有类库。
    (3)应用程序类加载器(Application ClassLoader):Java编写。加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    (4)自定义类加载器:Java编写。

    4.2 双亲委派模型
    类加载器双亲委派模型
    双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
    使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果读者有兴趣的话,可以尝试去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
    5 loadClass和forName的区别

    Class.forName得到的class是已经初始化完成的,ClassLoader.loadClass得到的class是还没有连接的。

    public class Robot {
    
        static {
            System.out.println("Hello Robot");
        }
    
    }
    
    public class LoadDifference {
    
        public static void main(String[] args) throws ClassNotFoundException {
            ClassLoader loader = Robot.class.getClassLoader();
            Class c1 = loader.loadClass("load.Robot");// 不打印
            Class c2 = Class.forName("load.Robot");// 打印
        }
    
    }
    

    相关文章

      网友评论

          本文标题:类加载机制

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