JVM类加载机制、类加载器、双亲委派

JVM类加载

类加载过程

  • 加载

    这一步骤通常由JVM提供的类加载器来完成,我们也可以通过实现ClassLoder来自定义一个类加载器。
    指将类的class文件读入内存当中,并为之创建一个java.lang.Class的对象。

  • 连接

    1. 验证
       验证是连接阶段的第一步,这一步是为了保证当前Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。
       1. 文件格式验证
           - 是否以魔数(0xCAFEBABE)开头
           - 主次版本号是否在当前虚拟机处理范围内
           - 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
           - 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
           - CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
           - Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
           - ......
       2. 元数据验证
           - 验证这个类是否有父类,除了java.lang.Object外,其他类必须有父类。
           - 这个类是否继承了不允许被继承的类(如被final修饰的类)
           - 如果此类不是抽象类,是否实现了父类或接口的所有要求实现的方法。
           - 类中的属性是否与父类中的属性产生了冲突(如覆盖了父类的final属性、不符合规则的方法重载,例如方法名相同但参数列表一致)
           - ......
       3. 字节码验证
           主要目的是通过数据流和控制流来分析其语义是合法的、符合逻辑的。这个阶段将对方法体进行校验(元数据验证主要是对类进行验证)。
           保证被校验的类在运行时不会对虚拟机产生危害。
           - 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。说白了就是操作数栈中放置了int类型的数据,本地变量表却存放了long类型的数据。
           - 保证跳转指令不会跳转到方法体以外的字节码指令上。
           - 保证方法体中转换是有效的。例如把父类对象赋值给子类类型、把对象赋值给与这个对象毫不相关的类型等。
           - ......
       4. 符号引用验证
           符号引用就是在一个类中,对其他类的引用,在将代码编译成class文件时,编译器并不知道引用类的实际内存地址,这时只能用一个唯一的符号来进行表示。
           在解析阶段会将符号引用替换成直接引用。  
           这一步骤主要时为了保证符号引用类能被正确访问到,不会出现类无法访问、非法访问等情况。  
           - 符号引用通过字符串的全限定名是否能找到对于的类。
           - 在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段。
           - 符号引用中的类、字段、方法的访问性(public、private、protected、default)是否可被当前类访问。
           - ......
      
    2. 准备
      这一阶段主要是将类中的静态变量分配内存,并设置默认初始值。
      注意:设置的默认初始值并不是代码中实际的值,例如int = 1,这时设置的值并不是1,而是0(因为是int,默认0)。
    3. 解析
      这一阶段主要就是将类中的符号引用替换成直接引用。
      直接引用:指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定要加载进来的。
  • 初始化

    这一阶段主要是将静态变量进行赋值,此处赋的值与准备过程中赋的值不同,准备中赋的值为默认初始值,而这里是代码中真实的值。
    注意:初始化是有顺序的,按照写的代码的顺序向下初始化。且不能访问还未被变量初始化的静态变量。

        // 此写法中,初始化完毕后i最终的值为0。因为先将i赋值为1,然后static代码块将i又赋值为0。
        static int i = 1;
        static {
            i = 0;
        }
        
        // 此写法中,初始化完毕后i最终的值为1。因为static代码块将i赋值为0。然后将i又赋值为1。
        static {
            i = 0;
        }
        static int i = 1;
        
         // 此写法中,将在System.out.print(i)这一行报错。
         // 因为不能访问还未被变量初始化的值。但可以赋值(i=0赋的值将被变量赋值那一行覆盖掉,如案例2中结果一样)
        static {
            i = 0;
            System.out.print(i);
        }
        static int i = 1;
    

类加载器

  1. 根类加载器
    根类加载器由C++编写,负责加载$JAVA_HOME中jre/lib里所有的class
  2. 扩展加载器
    它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现。
  3. 系统类加载器
    它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库 加载到内存中。

双亲委派机制

亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,
通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,
通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,
这样便可以防止核心API库被随意篡改。

你可能感兴趣的:(java,类加载机制,类加载,类加载器,双亲委派)