深入理解java虚拟机-----类结构与类加载机制

JVM(Java虚拟机)

6.1 Class文件格式

6.1.1 Class文件格式的数据结构
  • “无符号数”:无符号数属于基本的数据类型,u1,u2,u4,u8分别代表1,2,4,8个字节的无符号数,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成的字符串值。
  • “表”:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有的表的命名习惯上都以"_info"结尾。
6.1.2 Class文件格式
类型名称数量
u4magic (值为0xCAFEBABE,唯一作用是确定这个文件是否是一个能被虚拟机接受的Class文件)1
u2minor_version (次版本号)1
u2major_version (主版本号)1
u2constant_pool_count (常量池容量计数值,容量计数从1开始,0位空出)1
cp_infoconstant_pool (主要存放两大类常量:字面量和符号引用)constant_pool_count - 1
u2access_flags (一共定义了9个标志位,用来标识类或者接口层面的访问信息)1
u2this_class (确定该类的全限定名称)1
u2super_class (确定该类的父类的全限定名称)1
u2interfaces_count (当前类或者接口实现或者继承的接口的数量)1
u2interfacesinterfaces_count
u2fields_count1
field-infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attributes_infoattributesattributes_count

7 虚拟机类加载机制

7.1 类加载过程

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期将会经历如下的七个阶段。

加载(Loading)------->验证(Verification)-------->准备(Preparation)-------->解析(Resolution)-------->初始化(Initialization)-------->使用(Using)--------->卸载(Unloading)

其中加载,验证,准备,初始化和卸载这五个阶段的顺序是确定的。

  • 六种类型初始化的情况(有且仅有这六种情况,均为对类型的主动引用)

    • 遇到new,getstatic,putstatic或invokestatic这四条字节码指令时,如果有类型没有进行过初始化,则需要触发其初始化阶段。生成这四条指令的场景:

      • 使用new关键字实例化对象的时候
      • 读取或设置一个类型的静态字段的时候(被final修饰,在编译阶段就放入常量池的静态字段除外)
      • 调用一个类型的静态方法的时候
    • 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。

    • 当初始化类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。如果接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

    • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。

    • 当使用JDK 7加入的动态语言支持时,如果一个java.lang.invoke.MethedHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化

    • 当一个接口中定义了JDK 8新加入的默认方法时,如果这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。

  • 三种不会触发初始化的被动引用

    • 通过子类引用父类的静态字段,不会导致子类初始化。

      package com.lzx.init;
      
      public class SuperClass {
          static{
              System.out.println("我是父类,我初始化了");
          }
      
          public static int age = 80;
      
          public static int getAge(){
              return 1;
          }
      }
      
      public class SubClass extends SuperClass {
          static {
              System.out.println("我是子类,我初始化了");
          }
      }
      
      public class TestInitialization {
          public static void main(String[] args) {
              System.out.println(SubClass.getAge()); //调用父类的静态方法
              System.out.println(SubClass.age);//调用父类的静态字段
          }
      }
      //以上两种情况均不会初始化子类
      
    • 通过数组定义来引用类,不会出发此类的初始化。

      public class TestInitialization {
          public static void main(String[] args) {
              SuperClass[] sc = new SuperClass[10];
          }
      }
      
    • 常量在编译阶段就会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

      package com.lzx.init;
      
      public class ConstantClass {
          static {
              System.out.println("我初始化了");
          }
      
          public static final double PI = 3.1415926;
      }
      public class Test {
          public static void main(String[] args) {
              System.out.println(ConstantClass.PI);
          }
      }
      
7.1.1 加载
  1. 对一般的类,加载阶段,虚拟机需要完成如下的三件事情:

    • 通过一个类的全限定名称来获取定义此类的二进制字节流

    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    • 在java堆内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

  2. 数组类本身不通过类加载器创建,它是由虚拟机直接在内存中动态构造出来的,但是数组的元素类型(去掉所有的维度)最终还是要依赖类加载器加载。一个数组类A的创建过程遵循以下的规则

    • 若数组的组件类型(去掉一个维度)是一个引用类型,则递归采用如上定义的加载过程去加载这个组件类型,数组A会被标示在加载该组件类型的类加载器的类名称空间上。
    • 若数组的组件类型不是引用类型,java虚拟机将会把数组A标记为与启动类加载器关联。
    • 数组类的可访问性与它的组件类型的可访问性一样,若组件类型不是引用类型,则数组类的可访问性将默认为public。

加载阶段和连接阶段的部分动作是交叉进行的(比如部分字节码文件格式的验证动作),

7.1.2 验证
  1. 文件格式验证:
    • 主要目的是保证输入的字节流能够正确的解析并存储与方法区之内,格式上符合一个java类型信息的要求
    • 该阶段是基于二进制字节流进行的,该阶段通过后二进制字节流才会进入方法区中进行存储,后面的三个阶段都是基于方法区的存储结构进行的。
  2. 元数据验证
    • 主要目的是对元数据信息进行语义校验
      • 该类是否有父类
      • 该类的父类是否继承了不允许继承的类
      • 非抽象类是否实现了父类或者接口中的所有抽象方法
  3. 字节码验证
    • 通过数据流分析和控制流分析,确定语义是合法的,符合逻辑的。
      • 保证操作数栈的数据类型与指令代码序列能正确配合工作
      • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
  4. 符号引用验证
    • 该校验行为发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),用来验证该类是否缺少某些资源或者禁止访问某些资源。
      • 符号引用中通过字符串描述的全限定名称是否能找到对应的类
      • 符号应用中的类,字段,方法的可访问性是否可以被当前类访问
7.1.3 准备
  • 该阶段正式为类中的静态变量(被static修饰)分配内存并设置类变量初始值(类变量会随着Class对象一起放在java堆内存中)

  • 初始值一般为该静态变量类型的零值,常量例外

    public static int value = 123;
    //准备阶段会赋值为0
    public static final int value = 123;
    //准备阶段会赋值为123,字段的属性表中ConstantValue属性对应的值
    
7.1.4 解析
  • 解析阶段是java虚拟机将常量池中的符号引用替换为直接引用的过程

    • 符号引用:用来描述所引用的目标的一组符号,可以是任何形式的字面量,只要在使用时能够唯一的定义到目标即可。
    • 直接引用:直接引用是可以直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
  • 在执行以下17个用于操作符号引用的字节码指令之前,对它们所使用的符号引用进行解析

    anewarray,checkcast,
    getfield,getstatic,
    instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,
    ldc,ldc_w,ldc2_w,
    multianewarray,new,putfield,putstatic
    
  • 以上指令除了invokedynamic以外,虚拟机可以对第一次的解析结果进行缓存,避免重复解析。并且要保证在用一个实体中,多次解析的结果应当保持一致性。

  • 解析动作主要针对如下7类符号引用进行

    • 类或者接口-----------CONSTANT_Class_info
    • 字段--------------------CONSTANT_Fieldref_info
    • 类方法-----------------CONSTANT_Methodref_info
    • 接口方法--------------CONSTANT_InterfaceMethodref_info
    • 方法类型--------------CONSTANT_MethodType_info
    • 方法句柄--------------CONSTANT_MethodHandle_info
    • 调用点限定符--------CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic_info
  1. 类或者接口解析

    假设当前类为D,要将未解析的符号引用N解析为一个类或者接口C的直接引用,完整的解析过程需要包含以下3个步骤:

    • 若C不是数组类型,虚拟机会把代表N的全限定名称传给D的类加载器去加载这个类C
    • 若C是数组类型,并且数组的元素类型为对象,则按照上述规则加载数组的元素类型,接着由虚拟机生成一个代表该数组维度和元素的数组类型
    • 若以上两步没有异常,则C在虚拟机中已经是一个有效类或者接口,在解析完成之前还要进行符号引用验证,确定D是否有对C的访问权限。
  2. 字段解析

    对一个未被解析过的字段符号进行解析,首先要先对该字段所属的类或接口C进行解析,解析成功后再按照后续步骤进行解析。

    • 首先会对该字段的字段表内的class_index项中索引的CONSTANT_Class_info符号引用进行解析,解析成功后进行下面的步骤。
    • C本身包含了简单名称和字段描述符都与目标相匹配的字段,则返回该字段的直接引用,查找结束
    • 否则,在C实现的接口列表中,通过递归从下往上搜索父接口
    • 否则,按照继承关系,通过递归从下往上搜索父类
    • 否则,查找失败,抛出java.lang.NoSuchFieldError异常
    • 成功返回引用之后还要进行权限验证,若没有权限将抛出java.lang.IllegalAccessError异常
  3. 方法解析

    对一个未被解析过的方法符号进行解析,首先要先对该方法所属的类C进行解析,解析成功后再按照后续步骤进行解析。

    • 首先会对该方法的方法表内的class_index项中索引的C进行解析,如果发现C是接口则抛出java.lang.IncompatbleClassChangeError异常,解析成功后进行下面的步骤。
    • C本身包含了简单名称和描述符都与目标相匹配的方法,则返回该方法的直接引用,查找结束
    • 否则,按照继承关系,通过递归从下往上搜索父类
    • 否则,在C实现的接口列表中,通过递归从下往上搜索父接口,若存在说明C是抽象类,抛出java.lang.AbstractMethodError异常
    • 否则,查找失败,抛出java.lang.NoSuchMethodError异常
    • 成功返回引用之后还要进行权限验证,若没有权限将抛出java.lang.IllegalAccessError异常
  4. 接口方法解析

    对一个未被解析过的方法符号进行解析,首先要先对该方法所属的类或者接口C进行解析,解析成功后再按照后续步骤进行解析。

    • 首先会对该方法的方法表内的class_index项中索引的C进行解析,如果发现C是类不是接口则抛出java.lang.IncompatbleClassChangeError异常,解析成功后进行下面的步骤。
    • C本身包含了简单名称和描述符都与目标相匹配的方法,则返回该方法的直接引用,查找结束
    • 否则,通过递归从下往上搜索C的父接口,直到Object类(接口方法的查找会包括Object类中的方法)为止。有可能有多个匹配的方法,则会返回其中一个。
    • 否则,查找失败,抛出java.lang.NoSuchMethodError异常
    • JDK9之前不存在权限访的问题。JDK 9之后,成功返回引用之后还要进行权限验证,若没有权限将抛出java.lang.IllegalAccessError异常
7.1.5 初始化
  • 初始化阶段就是执行类构造器()方法的过程
    • ()方法是由编译器收集类中所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,收集的顺序是按照语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在它之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
    • ()方法与类的构造函数不同(实例构造器()方法),它不需要显示调用父类的构造器,java虚拟机保证子类的()方法执行前父类的()方法已经指向完毕。java虚拟机中第一个被执行()方法的类型肯定是java.lang.Object
    • 父类的静态语句块优于子类的变量赋值操作
    • ()方法对类和接口不是必须的,若类中没有静态语句块也没有对变量的赋值操作,则编译器可以不为这个类生成()方法
    • 接口中不能有静态语句块,但是可以有静态变量的赋值,因此接口也可以生成()方法。但是执行接口的()方法不需要先执行其父接口的()方法,因为只有当父接口中定义的变量被使用时父接口才会实例化,接口的实现类在初始化时也不会执行接口的()方法。
    • java虚拟机需要保证一个类的()方法在多线程环境下被正确的加锁同步。

7.2 类加载器

7.2.1 类与类加载器
  • 每一个类加载器都有一个独立的类名称空间,也就是说比较两个类是否“相等”,只有在这两个类是同一个类加载器加载的情况下才有意义。
  • 三层类加载器:
    • 启动类加载器(Bootstrap Class Loader):负责加载存放在 <JAVA_HOME>/lib目录或者被-Xbootclasspath参数指定的路径中存放的能被java虚拟机识别的类库到虚拟机的内存中。(按照文件名识别,如rt.jar,tools.jar,名字不符合的类库放到lib目录也不会加载) 用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器去处理,直接使用null值代替即可。
    • 扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以java代码的形式实现的。他负责加载<JAVA_HOME>/lib/ext目录中或者被Java.ext.dirs系统变量指定的路径中的所有的类库。
    • 应用程序类加载器(Application Class Loader):这个类加载器是在类sun.misc.Launcher$AppClassLoader中以java代码的形式实现的。该类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”,它负责加载用户类路径(ClassPath)上所有的类库。
7.2.2 双亲委派模型
  • 双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。这样的好处是java中的类随着它的加载器一起具备了一种带有优先级的层次关系,如java.lang.Object类总是被顶端的启动类加载器加载,能够保证在任何加载器环境下加载的都是同一个Object类。

  • image-20211226153341466

  • 双亲委派模型的代码片段,java.lang.ClassLoader的loadClass()方法

     protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    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.
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        // this is the defining class loader; record the stats
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    
  • 破坏双亲委派模型

    • 重写loadClass()方法,JDK 1.2之后在java.lang.ClassLoader中添加了一个protected方法findClass(),引导大家重写该方法实现类加载逻辑
    • 引入线程上下文类加载器,通过Java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它会从父线程中继承,如果全局范围内都没有设置的话,默认该加载器为应用类加载器。例如:JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器请求子类加载器去完成类加载的行为。
    • OSGi实现模块化热部署时使用自定义的类加载器实现,每一个模块都有自己的一个类加载器。网状的委派机制,出现了平级之间的委派。
      sun.misc.PerfCounter.getFindClasses().increment();
      }
      }
      if (resolve) {
      resolveClass©;
      }
      return c;
      }
      }
    
    
  • 破坏双亲委派模型

    • 重写loadClass()方法,JDK 1.2之后在java.lang.ClassLoader中添加了一个protected方法findClass(),引导大家重写该方法实现类加载逻辑
    • 引入线程上下文类加载器,通过Java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它会从父线程中继承,如果全局范围内都没有设置的话,默认该加载器为应用类加载器。例如:JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器请求子类加载器去完成类加载的行为。
    • OSGi实现模块化热部署时使用自定义的类加载器实现,每一个模块都有自己的一个类加载器。网状的委派机制,出现了平级之间的委派。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值