美文网首页
JVM-004-类加载机制

JVM-004-类加载机制

作者: 井易安 | 来源:发表于2018-07-10 21:23 被阅读0次

    虚拟机类加载机制

    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的Java类型。

    在Java里面,类型的加载、连接和初始化过程都是在程序运行期间完成,不同于在编译时需要进行连接工作的语言。

    java支持动态扩展就是依赖运行时动态加载和动态连接实现的

    1. 什么时候进行类加载

    类从被加载到虚拟机内存中到卸载出内存为止,整个生命周期包括:

    image

    除了解析之外其他步骤的顺序是确定的,解析在某些情况下可以在初始化阶段之后在开始,目的是为了支持JAVA语言的运行时绑定(动态绑定)

    1.1 加载
    并没有进行强制性约束,交给虚拟机根据具体实现在自有把握

    1.2 初始化
    严格规定了5种情况必须对类进行初始化,在这之前验证 准备 解析已经完成

    • 遇到 new getstatic putstatic invokestatic 4条字节码指令时,生成这四条指令的java代码场景有:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(final修饰并且已在编译器把结果放入常量池静态字段中的除外),调用类的静态方法的时候

    • 使用java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有初始化则先要触发初始化。

    • 初始化一个类的时候其父类还没有进行初始化,则先要触发对其父类的初始化。

    • 启动虚拟机后,包含main方法的类要先被初始化

    • 当使用jdk7动态语言支持时,invoke.methodhandle,使java也可以像C语言那样将方法作为参数传递

      有且仅有以上5种场景中的行为被称为对一个类的主动引用,除此之外所有引用类的方式都不会触发初始化,被称为被动引用。

    1.2.1 一些被动引用的例子

    • 对于静态字段只有直接定义这个字段的类才会被初始化,通过子类来引用父类中定义的静态字段,只会触发父类的初始化。
    • 通过定义数组来引用类,并不会对类进行初始化。
    • 常量在编译阶段会存入调用类的常量池中,本质并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

    关于接口与类不同的地方在:初始化一个类的时候要求其父类全部都已经初始化过了,但是一个接口在初始化的时候不要求其父接口全部完成初始化,只有在真正使用到父接口的时候例如引用了父接口中定义的常量才会被初始化。

    2. 类加载的过程

    2.1加载
    加载主要完成三件事情

    1. 通过一个类的全限定名来获取定义此类的二进制字节流
    2. 将这个字节流的静态存储结构转化为方法区的运行时数据结构
    3. 在内存中生成一个代表这个累的java.lang.Class对象作为方法区这个类的各种数据的访问入口

    加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,然后再内存中实例化一个java.lang.Class类的对象 这个对象存储在方法区里而不是存放普通对象的堆中。

    加载还未完成,连接阶段可能已经开始了,这两个阶段的开始时间仍然保持着固定的先后顺序。

    类 和 数组加载过程的区别?

    数组也有类型,称为“数组类型”。如:

    String[] str = new String[10]; 
    

    这个数组的数组类型是Ljava.lang.String,而String只是这个数组中元素的类型。

    当程序在运行过程中遇到new关键字创建一个数组时,由JVM直接创建数组类,再由类加载器创建数组中的元素类。

    而普通类的加载由类加载器完成。既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器。

    2.2验证
    主要目的是确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全 包括以下验证

    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证

    2.3准备

    • 为已经在方法区中的类中的静态成员变量分配内存 类的静态成员变量也存储在方法区中。
    • 为静态成员变量设置初始值 初始值为0、false、null等。

    需要注意的是 类变量(被static修饰的变量)而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。

    并且这里的初始值通常情况下是数据类型的零值,赋值操作是在编译后,初始化阶段才会执行。

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

    2.5初始化
    初始化阶段就是执行类构造器clinit()的过程。
    clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。

    在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。

    初始化过程的注意点:

    • clinit()方法中静态成员变量的赋值顺序是根据Java代码中成员变量的出现的顺序决定的。
    • 静态代码块能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量。
    • 静态代码块能给出现在静态代码块之后的静态成员变量赋值。
    • 构造函数init()需要显示调用父类构造函数,而类的构造函数clinit()不需要调用父类的类构造函数,因为虚拟机会确保子类的clinit()方法执行前已经执行了父类的clinit()方法。
    • 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会生成clinit()方法。
    • 接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化。
    • 接口中不能使用静态代码块。
    • 接口在执行clinit()方法前,虚拟机不会确保其父接口的clinit()方- 法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接- 口的clinit()方法。
    • 虚拟机会给clinit()方法加锁,因此当多条线程同时执行某一个类的clinit()方法时,只有一个方法会被执行,其它的方法都被阻塞。并且,只要有一个clinit()方法执行完,其它的clinit()方法就不会再被执行。因此,在同一个类加载器下,同一个类只会被初始化一次。

    类加载器

    把类加载过程的第一个阶段--加载阶段中的 通过一个类的全限定名来获取描述此类的二进制字节流 这个动作放到jvm外部去实现,以便让应用程序自己决定如何去获取所需要的类。

    一个类需要由加载它的类加载器和这个类本身来确定它在java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。

    比较两个类是否相等,只有在这两个类是否由同一个类加载器加载的前提下才有意义。

    双亲委派模型

    对于JVM来说类加载器分为两种

    1. 启动类加载器,使用C++实现,是虚拟机自身的一部分
    2. 其他的类加载器,Jva语言实现,独立于虚拟机外部,继承与抽象类 java.lang.ClassLoader

    对于开发人员来说分为三种

    1. 启动类加载器,负责将放在lib目录中或者-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的 类库 加载到虚拟机内存中。
    2. 扩展类加载器,负责将lib\ext目录中的,或者java.ext.dirs系统变量所指定的路径中的所有类库。
    3. 应用程序类加载器, 这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库
    双亲委派模型

    类加载器之间的这种层次关系被称为双亲委派模型。
    双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。类加载器之间的父子关系一般不会以继承的方式实现,而是都使用组合的关系来复用父加载器的代码。

    双亲委派模型的工作过程

     protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
        {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
            } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
        }
    
    1. 加载器比较依赖它的父类,收到类加载的请求时首先会去把这个请求委派给父类加载器去完成,所以所有的加载器请求最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(在它的搜索范围内没有找到这个类)时,自加载器才会尝试自己加载。
    2. 使用双亲委派模型,java类随着类加载器一起具备了一种带有优先级的层次关系。比如Object类只会在顶层的启动类加载器中加载,这样系统中就不会出现多个不同的Object类,如果执意要写一个重名的java类,那么这个类永远都不会被加载。
    3. 对于基础类又要调用回用户代码怎么解决?
      线程上下文类加载器,通过这个加载器,福类加载器可以请求自类加载器去完成类加载活动。这种行为违背了双亲委派模型的一般性原则。

    相关文章

      网友评论

          本文标题:JVM-004-类加载机制

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