zoukankan      html  css  js  c++  java
  • 【JVM】(二) :类加载机制(Class Loader)

    简介

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

     

                    类的整个生命周期分为以上七个阶段,验证、准备、解析统称为连接阶段。

    一、加载

    加载阶段虚拟机需要完成如下事情。
      1. 通过类的全限定名来获取此类的二进制字节流。
      2. 将二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构
      3. 创建一个代表带该的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
      用户可以自定义类加载器(重写loadClass方法)获取二进制字节流。对于数组类而言,数组类由java虚拟机直接创建,不通过类加载器创建。数组类的创建过程如下:
      ①如果数组元素类型是引用类型,就采用双亲委派模型进行加载(之后会介绍),数组类将在加载该元素类型的类名称空间上被标识。
      ②如果数组元素类型为基本类型,数组类被标记为与引导类加载器关联。
      ③数组类的可见性与其元素类型可见性一致,如果元素类型不是引用类型,那数组类的可见性默认为public。
      创建的Class对象在方法区中。对象绝大多数放在堆中,Class对象是一个例外。

    二、连接

    2.1 验证

      此阶段是确保class文件的字节流包含的信息符合虚拟机的要求。主要会进行如下的验证。
      1. 文件格式验证
      验证字节流是否符合Class文件格式规范。如是否以魔数开头、主次版本号是否能被虚拟机处理、常量池的常量中是否有不被支持的常量类型等等。
      2. 元数据验证
      对字节码描述的信息进行语义分析。如这个类是否有父类、这个类是否继承了不允许被继承的类、非抽象类是否实现了父类或接口中要求实现的所有方法等等。
      3. 字节码验证
      分析数据流和控制流,确定程序语义是否合法,符合逻辑。如任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证跳转指令不会跳转到方法体以外的字节码指令上等等。
      4. 符号引用验证
      符号引用转化为直接引用的时候进行验证。如符号引用中通过字符串描述的全限定名是否能够找到对应的类、指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段等等。

    2.2 准备

       准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值(基本类型特定值,引用类型为null),假设一个类变量的定义为:

    public static int value=100;

    变量value在准备阶段过后的初始值为0而不是100.因为这时候尚未开始执行任何java方法,而把value赋值为100的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为100的动作将在初始化阶段才会执行
    至于“特殊情况”是指:public static final int value=100,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为100而非0.

    讲到这里让我们来看一个直观的例子,当一个类静态属性没有赋值时,也就是准备阶段的初始值不会被初始化阶段覆盖,我们可以看看准备阶段的具体赋值情况

    package net.riking.springframework.packTest;
    
    public class Test {
        static  int count;
        static  boolean flag;
        static  double aDouble;
        static  String string;
        static  Object obj;
    
        public static void main(String[] args) {
            System.out.println(count);
            System.out.println(flag);
            System.out.println(aDouble);
            System.out.println(string);
            System.out.println(obj);
        }
    }

    输出结果:

    0
    false
    0.0
    null
    null

    点击进入更直观示例

    2.3 解析

      将常量池中的符号引用(包括类或接口的全限定名、字段名和描述符、方法名和描述符)替换为直接引用(内存地址)。符号引用与最终class文件载入内存的布局无关,直接引用与内存的布局有关。为了加快解析效率,可以对解析结果进行缓存,之后再解析符号引用时直接返回即可,但是对于invokedynamic则不能进行缓存。解析主要是针对CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info七种常量类型。
      指向类型、类变量、类方法的直接引用可能是指向方法区的本地指针。类型的直接引用可能简单地指向保存类型数据的方法区中的与实现相关的数据结构。类变量的直接引用可以指向方法区中保存的类变量的值。类方法的直接引用可以指向方法区中的一段数据结构方法区中包含调用方法的必要数据。
      指向实例变量和实例方法的直接引用都是偏移量。实例变量的直接引用可能是从对象的映像开始算起到这个实例变量位置的偏移量。实例方法的直接引用可能是到方法表的偏移量。
      1. 类或接口的解析
      将符号引用替换为直接引用包括如下几步。假设符号引用记为S,当前类记为C,S对应的类或接口记为I。
      ① 若S不是数组类型,则把S传递给当前类C的类加载器进行加载,这个过程可能会触发其他的加载,这个过程一旦出现异常,则解析失败。
      ② 若S是数组类型,并且数组元素类型为对象,则S的描述符会形如[java/lang/String,按照第一条去加载该类型,如果S的描述符符合,则需要加载的类型就是java.lang.String,接着有虚拟机生成一个代表此数组唯独和元素的数组对象。
      ③ 若以上两个步骤没有出现异常,即I已经存在于内存中了,但是解析完成时还需要进行符号引用验证,确认C是否具备对I的访问权限。若不具备,则抛出java.lang.IllegalAccessError异常。

      2. 字段解析
      首先将CONSTANT_Fieldref_info中的class_index索引的CONSTANT_Class_info符号引用进行解析,即解析字段所在类或接口,若解析出现异常,则字段解析失败。如解析成功,则进行下面的解析步骤。假设该字段所属的类或接口标记为C。
      ① 如果C包含了字段的简单名和描述符与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
      ② 否则,如果C实现了接口,按照继承关系从下往上递归搜索各个接口和它的父接口,看是否存在相匹配的字段。存在,则返回直接引用,查找结束。
      ③ 否则,如果C不是Object对象,按照继承关系从下往上递归搜索父类,看是否存在相匹配的字段。存在,则返回直接引用,查找结束。
      ④ 否则,查找失败,抛出java.lang.NoSuchFieldError异常。
      ⑤ 如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常.
      说明:字段解析对接口优先搜索。

           在实际应用中,虚拟机的编译器实现可能会比上述规范要求的更加严格一些,如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。在下面代码示例中,如果注释了C类中的 “public  int a=3; ”,接口与父类同时存在字段 a,那编译器将提示“Error:(13, 29) java: 对a的引用不明确 ,com.kly.admin.test.B 中的变量 a 和 com.kly.admin.test.A 中的变量 a 都匹配”,并且拒绝编译这段代码。

    public interface A {
           int a = 1;
    }
    public class B {
        public   int a = 2;
    }
    public class C extends B implements A {
        //public   int a = 3;
    
        public static void main(String[] args) {
            C c = new C();
            System.out.println(c.a);
        }
    }


      3. 类方法解析
      首先将CONSTANT_Methodref_info中的class_index索引的CONSTANT_Class_info符号引用进行解析,即解析方法所在的类或接口,若解析出现异常,则方法解析失败;如解析成功,则进行下面解析步骤。假设该方法所属的类标记为C。
      ① 如果在方法表中发现CONSTANT_Class_info中索引的C是一个接口而不是一个类,则抛出java.lang.IncompatibleClassChangeError异常。
      ② 否则,如果C中包含了方法的简单名和描述符与目标相匹配的字段,则返回这个方法的直接引用,查找结束。
      ③ 否则,在C的父类中递归搜索,看是否存在相匹配的方法,存在,则返回直接引用,查找结束。
      ④ 否则,在C实现的接口列表及父接口中递归搜索,看是否存在相匹配的方法,存在,说明C是一个抽象类(没有实现该方法,否则,在第一步就查找成功),抛出java.lang.AbstractMethodError异常。
      ⑤ 否则,查找失败,抛出java.lang.NoSuchMethodError异常。
      ⑥ 若查找过程成功,则对方法进行权限验证,如果发现不具备对此方法的访问权限,则抛出java.lang.lllegalAccessError异常。
      说明:方法解析对父类优先搜索。

      4. 接口方法解析
      首先将CONSTANT_InterfaceMethodref_info中的class_index索引的CONSTANT_Class_info符号引用进行解析,即解析方法所在的类或接口,若解析出现异常,则方法解析失败;如解析成功,则进行下面解析步骤。假设该方法所属的类标记为C。
      ① 如果在方法表中发现CONSTANT_Class_info中索引的C是一个类而不是接口,则抛出java.lang.IncompatibleClassChangeError异常。
      ② 否则,如果C中包含了方法的简单名和描述符与目标相匹配的字段,则返回这个方法的直接引用,查找结束。
      ③ 否则,在C的父接口中递归搜索,直到Object类,看是否存在相匹配的方法,存在,则返回直接引用,查找结束。
      ④ 否则,查找失败,抛出java.lang.NoSuchMethodError异常。
      ⑤ 若查找过程成功,不需要进行权限验证,因为接口方法为public,不会抛出java.lang.IllegalAccessError异常。

    三、初始化

      在此阶段,才开始真正执行用户自定的java代码。在准备阶段,类变量已经被赋予了系统默认值,而在初始化阶段,会赋予用户自定义的值。初始化由编译器收集类中的所有类变量的赋值动作(如果仅仅只是声明,不会被收集)和静态语句块中的语句合并产生的,收集顺序按照语句在源文件中出现的顺序所决定而初始化阶段是在<clinit>()方法中执行的。

         对于初始化我分为类初始化和对象初始化(对象实例化),类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段,而类的实例化是指创建一个类的实例(对象)的过程,这个过程中也进行了(非static)成员变量赋值:这两种分类也仅只是我在测试过程中个人理解,我翻阅了很多文章,对于初始化的概念讲的很模糊,大多都讲一下初始化的特性 。所以下面我会用一些示例验证一下我的观点(如果本人理解错了请留言,谢谢)。在验证之前首先我也先讲解一下关于初始化的特性,如下:

    3.1 主动引用类时,会初始化类

    如下几种情况就必须要进行初始化。

      ①. 遇到new、getstatic、putstatic、invokestatic指令时,对应到程序中就是使用到new实例化对象时、读取或设置类静态字段时(非final)、调用静态方法时。需要进行初始化。

          ②.使用java.lang.reflect包的方法对类进行反射调用时,需要进行初始化。

          ③.使用一个类时,若其父类还未初始化,则需先初始化其父类(不需要显示调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕)。

          ④.虚拟机启动时,包含main方法的类,虚拟机会将其初始化。

          ⑤.使用Class.forName(String className)加载类时

    3.2 被动引用类时,不会初始化类

         ①.对于类而言,使用父类的静态字段(非final)不会导致子类的初始化,父类会被初始化,但是不会调用构造函数。
         ②.对于类或接口而言,使用其常量字段(final static)不会导致其初始化。ps 根据主动使用第三条可知,继承或实现此类或接口的子类或接口,使用其常量字段(final static)不会导致其初始化
         ③.对于数组而言,创建类的引用数组,该类不会被初始化。eg Main[] list = new Main[10];

    3.3 初始化调用顺序

    父类静态块-->子类静态块-->父类构造块-->父类构造函数-->子类构造块-->子类构造函数

    3.4 验证

    父类SuperClass

    package net.riking.springframework.packTest;
    
    public class SuperClass {
        public  static  int value = 100;
        public static final int finalValue = 110;
        static
        {
            System.out.println("SuperClass 静态代码块");
        }
        public SuperClass(){
              System.out.println("SuperClass 构造函数");
         }
        {
            System.out.println("SuperClass 成员代码块");
        }
    }

     子类SubClass

    package net.riking.springframework.packTest;
    
    public class SubClass extends SuperClass{
        static
        {
            System.out.println("SubClass 静态代码块");
        }
        public SubClass(){
            System.out.println("SubClass 构造函数");
        }
        {
            System.out.println("SubClass 成员代码块");
        }
    }

    主类Test

    package net.riking.springframework.packTest;
    
    public class Test {
    
          static
            {
                System.out.println("Main方法  静态代码块");
            }
            public Test(){
                  System.out.println("Main方法 构造函数");
             }
            {
                System.out.println("Main方法 成员代码块");
            }
        
        public static void main(String[] args)  {
    
        }
    }

    验证一:初始化调用顺序以及主动引用③④

        public static void main(String[] args)  {
           new SubClass();
        }

    输出结果:

    Main方法  静态代码块
    SuperClass 静态代码块
    SubClass 静态代码块
    SuperClass 成员代码块
    SuperClass 构造函数
    SubClass 成员代码块
    SubClass 构造函数

    验证二:被动引用

     public static void main(String[] args)  {    
             System.out.println(SubClass.finalStaticValue);
             System.out.println("---------分割线---------");
             System.out.println(SubClass.staticValue);
    }

    输出结果:

    Main方法  静态代码块
    110
    -------分割线---------
    SuperClass 静态代码块
    100

    验证三:类初始化和对象初始化

      public static void main(String[] args)  {
           new SubClass();
    System.out.println("---------分割线---------");
    new SubClass(); }

    输出结果:

    Main方法  静态代码块
    SuperClass 静态代码块
    SubClass 静态代码块
    SuperClass 成员代码块
    SuperClass 构造函数
    SubClass 成员代码块
    SubClass 构造函数
    ---------分割线---------
    SuperClass 成员代码块
    SuperClass 构造函数
    SubClass 成员代码块
    SubClass 构造函数

    现象表明调用第一次new SubClass()时,初始化了静态代码块以及成员代码块和构造函数,第二次调用new SubClass()时仅仅只初始化了成员代码块和构造函数。

    像静态代码块初始化一生只有一次这种现象我称为类初始化,而成员代码块和构造函数初始化一生多次我称为对象初始化(对象实例化)。

    验证四:在静态语句块中只能访问定义在静态语句之前的变量;而对于定义在静态语句块之后的变量,可以进行赋值,但是不能够访问。

    package net.riking.springframework.packTest;
    
    public class Test {
        static int counter1=1;
        static {
            System.out.println("静态代码块一:");
            System.out.println("counter1="+counter1++);//访问定义在静态语句之前的变量
           //System.out.println(counter2);//编译器报错:Illegal forward reference----->定义在静态语句块之后的变量,可以进行赋值,但是不能够访问
            counter2=2;
        }
        static int counter2=1;
        static {
            System.out.println("静态代码块二:");
            System.out.println("counter1="+counter1);
        }
        public static void main(String[] args) {
            Test test = new Test();
            System.out.println("main方法:");
            System.out.println("counter1="+counter1);
            System.out.println("counter2="+counter2);
        }
    }

    输出结果:

    静态代码块一:
    counter1=1
    静态代码块二:
    counter1=2
    main方法:
    counter1=2
    counter2=1

    四、使用

      完成了初始化阶段后,我们就可以使用对象了,在程序中可以随意进行访问,只要类还没有被卸载。

    五、卸载

      对类型进行卸载,在之前的垃圾回收中我们已经讲解了如何才能对类型进行卸载,即回收操作。启动类加载的类型永远是可触及的,回收的是由用户自定义加加载器加载的类。

    六、类加载器

      

    类加载器用于加载类,任何类都需要由加载它的类加载器和这个类一同确立其在Java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间,由不同类加载的类不可能相等。

    6.1. 双亲委派模型

      从虚拟机角度看,只存在两种类加载器:1. 启动类加载器。2. 其他类加载器。从开发人员角度看,包括如下类加载器:1. 启动类加载器。2. 扩展类加载器。3. 应用程序类加载器。4. 自定义类加载器。
      ① 启动类加载器,用于加载Java API,加载<JAVA_HOME>lib目录下的类库。
      ② 扩展类加载类,由sun.misc.Launcher$ExtClassLoader实现,用于加载<JAVA_HOME>libext目录下或者被java.ext.dirs系统变量指定路径下的类库。
      ③ 应用程序类加载器,也成为系统类加载器,由sun.misc.Launcher$AppClassLoader实现,用于加载用户类路径(ClassPath)上所指定的类库。
      ④ 自定义类加载器,继承系统类加载器,实现用户自定义加载逻辑。
      各个类加载器之间是组合关系,并非继承关系。
      当一个类加载器收到类加载的请求,它将这个加载请求委派给父类加载器进行加载,每一层加载器都是如此,最终,所有的请求都会传送到启动类加载器中。只有当父类加载器自己无法完成加载请求时,子类加载器才会尝试自己加载。
      双亲委派模型可以确保安全性,可以保证所有的Java类库都是由启动类加载器加载。如用户编写的java.lang.Object,加载请求传递到启动类加载器,启动类加载的是系统中的Object对象,而用户编写的java.lang.Object不会被加载。如用户编写的java.lang.virus类,加载请求传递到启动类加载器,启动类加载器发现virus类并不是核心Java类,无法进行加载,将会由具体的子类加载器进行加载,而经过不同加载器进行加载的类是无法访问彼此的。由不同加载器加载的类处于不同的运行时包。所有的访问权限都是基于同一个运行时包而言的。

    七、总结

    package net.riking.springframework.packTest;
    
    public class Singleton {
        private static Singleton sin = new Singleton();
        public static int counter1;
        public static int counter2 = 10;
    
        private Singleton() {
            counter1++;
            counter2++;
        }
    
        public static Singleton getInstance() {
            return sin;
        }
    }
    
    package net.riking.springframework.packTest;
    
    public class Singleton2 {
    
        public static int counter1;
        public static int counter2 = 10;
        private static Singleton2 sin = new Singleton2();
        private Singleton2() {
            counter1++;
            counter2++;
        }
    
        public static Singleton2 getInstance() {
            return sin;
        }
    }
    
    package net.riking.springframework.packTest;
    
    public class Test {
    
    
        public static void main(String[] args) {
            Singleton sin = Singleton.getInstance();
            System.out.println("一、counter1:"+sin.counter1);
            System.out.println("一、counter2:"+sin.counter2);
            Singleton2 sin2 = Singleton2.getInstance();
            System.out.println("二、counter1:"+sin2.counter1);
            System.out.println("二、counter2:"+sin2.counter2);
        }
    }

    输出结果:

    结果:
        一、counter1:1
        一、counter2:10
        二、counter1:1
        二、counter2:11
  • 相关阅读:
    java中金钱计算BigDecimal
    SpringBoot的学习二:整合Redis,JPA,Mybatis
    SpringBoot的学习一:入门篇
    Java基础回顾一
    golang 实现冒泡排序
    Go统计键盘输入随机字母的个数
    破解点触码的识别之第三方平台超级鹰的SDK(python3版本)
    RuntimeError: Failed to init API, possibly an invalid tessdata path: E:python36报错
    Django项目部署
    Django REST framework 的功能
  • 原文地址:https://www.cnblogs.com/kongliuyi/p/10706330.html
Copyright © 2011-2022 走看看