执行引擎(二):虚拟机类加载机制

执行引擎

  • 物理机
    • 概述
      • 执行引擎建立在处理器,硬件,指令集,操作系统层面上
  • 虚拟机
    • 概述
      • 执行引擎由自己实现,可自行制定指令集与执行引擎的结构体系,并且能够直接执行哪些不被硬件直接支持的指令集格式
  • java虚拟机执行引擎:
    • 概述
      • 输入字节码文件,处理过程是字节码解析的等效过程,输出执行结果
      • 在不同的虚拟机实现里,执行引擎在执行java代码的时候
  • 解释执行:通过解释器执行
  • 编译执行:通过即使编译器产生本地代码执行

  • 类加载时机
  • 初始化
  • 类加载过程
  • 加载
  • 实现
  • 1.通过类全限定名来获取此类的二进制字节流
  • 2.将字节流静态存储结构转换为方法区运行时数据
  • 3.内存生成该类的java.lang.Class对象
  • 作为方法区这个类的各种数据的访问入口。
  • (Hotstop虚拟机将Class对象存放到方法区中)
  • 注:虚拟机规范的这3点要求并不及具体,
  • 因此虚拟机实现与具体应用的灵活度很大
  • (如没有指明流的获取地址)
  • 开发人员可以通过自定义的类加载器去控制字节流的获取方式
  • 即:重写一个类加载器的loadClass()方法)
  • 注:数组类本身不通过类加载器创建,
  • 是由java虚拟机直接创建的
  • 但数组类的元素类型最终要靠类加载器去创建。
  • 遵循:
  • 数组元素类型类加载器的类空间名称标识;
  • 1.引用类型,
  • 递归采用加载过程去加载这个组件类型,
  • 数组将在加载该组件类型的类加载器的类名称空间上被标识。
  • 2.非引用类型,数组标记为与引导类加载器关联
  • 可见性:
  • 3.数组类的可见性与他的元素类型可见性一致,
  • 如果不是引用类型,
  • 则数组类的可见性将默认为public
  • 类加载器
  • 概述
  • 虚拟机团队把类加载阶段中
  • "通过一个类的全限定名来获取描述此类的二进制字节流"
  • 放到java虚拟机外部来实现,
  • 以便让应用程序自己决定如何去获取所需要的类。
  • 类与类加载器
  • 启动类加载器
  • 负责将存放在<JAVA_HOME>\lib目录中的
  • 或者被-Xbootclasspath参数所指定的路径中
  • 并且是虚拟机识别的
  • (仅按照文件名识别
  • 如rt.jar,名字不符合的类库即使放在lib中也不会被加载)
  • 类库加载到虚拟机内存中。
  • 无法被java程序直接引用,
  • 编写自定义加载器时,
  • 如需把加载请求委派给引导类加载器,直接使用null代替即可。
  • 扩展类加载器
  • 该加载器由sun.misc.Launcher$ExtClassLoader实现,
  • 负责加载<JAVA_HOME>\lib\ext目录中的,
  • 或者被java.ext.dirs系统变量指定的路径中的所有类库。
  • 可直接使用
  • 应用程序类加载器
  • 这个类加载器由sun.misc.Launcher$App-ClassLoder实现。
  • 称为系统类加载器
  • (由于这个类加载器是ClassLoader中getSyetemClassLoader()方法的返回值)。
  • 负责加载用户类路径(ClassPath)上所指定的类库,
  • 可以直接使用
  • 默认的类加载器
  • 概述
  • 对于任何一个类,都需要由加载它类加载器和这个类的本身一同确立其在java虚拟机中的唯一性,
  • 每一个类加载器,都拥有一个独立的类名称空间。
  • 比较两个类是否相等,只有这两个类是由同一个类加载器加载器的前提下才有意义。
  • 否则即使这两个类源于同一个Class文件,被同一个虚拟机加载,
  • 只要加载他们的类加载器不同,那么这两个类必定不相等。
  • 从java虚拟机的角度:
  • 启动类加载器:C++语言实现,属于虚拟机的一部分
  • 所有其他类加载器:
  • java语言实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader
  • 双亲委派模型
  • 工作过程
  • 如果一个类的加载器收到了类加载的请求
  • 1.首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成
  • 因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。
  • 2.如果父加载器反馈自己无法完成这个请求的时候
  • (搜索范围中没有找到所需的类),
  • 子加载器才会尝试自己去加载。
  • 为什么使用?
  • 一致性
  • java随着他的类加载一起具备了一种带有优先级的层次关系。
  • 对于保证java程序的稳定运作很重要。
  • 如:Object类,最终委派给处于模型最顶端的启动类加载器进行加载,
  • 因此Object类在程序的各种类加载器环境中都是同一个类。
  • 相反,如果由各个类加载器自行加载的话,
  • 那么就会有许多个不同的Object类。
  • 怎么使用?
  • 1.首先检查请求的类是否已经被加载过。
  • 2.若没有加载则调用父类加载器的loadClass()方法
  • 3.如果父类加载器为空则使用默认的启动类加载器作为父类加载器,如果父类加载失败,则抛出ClassNotFoundException异常,在调用自己的findClass()方法进行
  • 加载。
  • 特例:只要有足够意义和理由,突破已有的原则就可以认为是一种创新
  • JDK1.2之前
  • 基础类回调用户代码
  • OSGI:在OSGI环境下,类加载器不再是双亲委派模型,而是进一步发展成更加复杂的网络结构。
  • 搜索顺序
  • 1.将以java.*开头的类委派给父类加载器加载
  • 2.否则,将委派列表名单内的类委派给父类加载器加载
  • 3.否则,将import列表中的类委派给Export类的Bundle的类加载器加载
  • 4.否则,查找当前Bundle的ClassPath,使用自己的类加载器进行加载
  • 5.否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器进行加载
  • 6.否则,查找DYnamic Import列表的Bundle,委派给对应的Bundle的类加载器加载
  • 7.否则,查找失败
  • 概述
  • 注:双亲委派模型要求除了顶级的启动类加载器外,
  • 其余的类加载器都应当有自己的父类加载器。
  • 父子关系一般使用组合关系来复用父加载器的代码。
  • 注:双亲委派模型并不是强制性的约束模型,
  • 而是java设计者推荐给开发者的一种类加载实现方式。
  • 链接
  • 验证
  • 文件格式验证
  • 验证字节流是否符合Class文件的规范,能被当前版本的虚拟机处理
    (魔术,主次版本号,常量池中常量类型)
    目的:保证输入的字节流能正确的解析并存储与方法区之内,
    符合描述一个java类型信息的要求。
    元数据验证
    对字节码描述的信息进行语义分析
    目的:对类的元数据信息进行语义检验,
    保证符合java语言规范的元数据信息。
    字节码验证
    字节码验证:通过数据流和控制流的分析
    通过程序去校验程序逻辑是无法做到绝对准确的
    目的:确定程序语义合法,符合逻辑。
    注:对类的方法体进行校验
    没保证该方法在运行时不会做出危害虚拟机安全的事件。
    DK1.6之后javac编译器和java虚拟机中给方法体的Code属性的属性表中增加StackMapTable的属性蘀ꗗߐĀ鹀芙
    字节码验证:类型推导 => 类型检查
    描述了方法体重所有的基本块开始时的本地变量表和操作栈应有的状态,
    在字节码验证期间,只需检查StackMapTable属性中的记录是否合法即可。
    使用
    -XX:-UseSplitVerifier关闭。
    -XX:+FailOverToOldVerifier:验证失败的时候退回到类型推到方式。
    注:JKD1.7之后(主版本号大于50的Class文件):使用类型检查来完成数据流分析检验是唯一的选择。
    符号引用验证
    发生在虚拟机将符号引用转换为直接引用的时候
    (转换动作在链接的第三阶段,解析阶段发生)。
    即对类自身之外的信息(常量池中的各种符号引用)进行匹配性的校验
    目的:确保解析动作能正常执行。
    无法通过符号引用验证,则抛出
    java.lang.IncompatibleClassChangeError异常的子类:
    java.lang.IllegalAccessError
    java.long.NoSuchFieldError
    java.lang.NoSuchMethodError
    使用:-Xverify:none关闭大部分的类验证措施。
    准备
    正式为变量分配内存并设置类变量初始值的阶段,
    这些变所使用的内存都将在方法区中进行分配
    解析
    类或接口的解析
    符号引用 (N)=> 直接引用(C),类(D):
    1.如果C不是数组类型,
    把代表N的全限定名传递给D的类加载器去加载C
    在加载过程中,由于元数据的验证,字节码的验证可能触发其他相关类的加载动作
    如出现异常,解析过程失败。
    2.如果C是数组类型,并且数组的元素类型为对象,
    将按1的规则加载数组类型的元素。
    随后生成一个代表此数组维度和元素的数组对象
    3.解析完成之前还要进行符号引用的验证,
    确认D是否具备对C的访问权限,
    如不具备,抛出java.lang.IllegalAccessError异常
    字段解析
    对字段表内class_index项索引的CONSTANT_Class_info符号引用进行解析
    查询该字段所属的类或接口C。
    即字段所属的类或接口的符号引用。
    1.查找类本身
    如C本身包含简单名称和字段描述符都与目标相匹配的字段,
    则返回该字段直接引用,查找结束
    2.查找类的父接口
    如C中实现了接口,将按继承关系从下往上递归搜索各个接口,
    如包含简单名称和字段描述符都与目标相匹配,
    则返回这个字段的直接引用。查找结束
    3.查找类的父类
    如C不是java.lang.Object,将会按照继承关系从下往上递归搜索其父类,如包含简单名称和字段描述符与目标相匹配,
    则返回这个字段的直接音乐,查找结束。
    4.查找失败
    抛出java.lang.NoSuchFeldError异常
    注:如成功返回,将对该字段进行权限验证,
    如不具备对该字段的访问权限,
    则抛java.lang.IllegalAccessError异常。
    在编译器中:
    如同名字段同时出现在接口和父类中
    或同时在自己或父类中的多个和接口中出现,
    则编译器拒绝编译。
    类方法解析
    对类方法表内class_index项索引的方法所属的类或接口的符号引用进行解析
    查询到该字段所属的类或接口C。
    即类方法所属的类或接口的符号引用。
    1.类方法
    如果类方法中发现class_index中索引的C是接口,
    则抛java.lang.IncompatibleClassChangeError异常。
    2..查找类本身
    在类C中查找简单名称和描述符都与目标相匹配的方法,
    如成立则返回这个方法的直接引用,查找结束
    3.查找类的父类
    在C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
    4。查找类的父接口
    在C实现的接口列表和父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法
    如果有则返回这个方法的直接引用,查找结束
    5.查找失败
    抛出java.lang.NoSuchMethodError异常
    注:如果查找成功,
    对这个字方法进行权限验证,
    如不具备对字段的访问权限,
    将抛java.lang.IllegalAccessError异常。
    接口方法解析
    对接口方法表内class_index项索引的方法所属的类或接口的符号引用进行解析
    查询到该字段所属的类或接口C。
    即接口方法所属的类或接口的符号引用。
    1.接口方法
    如果在接口方法中发现class_index中索引的C是个类,
    则抛java.lang.IncompatibleClassChangeError异常。
    2..查找接口本身
    在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
    3.查找接口的父接口
    在C的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,
    如果有则返回这个方法的直接引用,查找结束
    4.查找失败
    抛出java.lang.NoSuchMethodError异常
    注:接口中所有方法默认都是public的,
    所以不会抛出java.lang.IllegalAccessError异常。
    概述
    即将常量池内的符号引用替换为直接引用的过程
    符号引用:以一组符号来描述所引用的目标,
    可以是任任何形式的字面量,
    只要使用时无歧义的定位到目标即可。
    与虚拟机实现的内存布局无关。
    符号引用的字面量形式明确定义在java虚拟机规范的Class文件中
    直接引用:直接引用可以是直接指向目标的指针,
    相对偏移量或一个能间接定位到目标的句柄。
    与虚拟机实现的内存布局相关,
    同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同,
    如果有直接引用,那引用的不表必定已经在内存中存在。
    虚拟机规范中并未规定解析阶段发生的具体时间,
    要求了在执行
    anewarry.
    checkast,
    getfield,
    instanceof,
    invokeinterface,
    invokespecial,
    invokestatic,
    invokevirtual.
    ldc,
    ldc_w,
    multianewarry,
    new,
    putfield,
    putstatic
    这16个用于操作符号引用的字节码指令之前。
    除invokedynamic指令
    (动态调用点限定符:必须等到程序实际运行到这条指令的时候,解析动作才能进行)外,
    虚拟机实现可以对第一次解析的结果进行缓存
    (在运行时常量池中记录直接引用,并把常量标识为已解析状态)
    初始化
    初始化阶段是执行类构造器<clinit>()方法的过程。
    除在加载阶段用于应用程序可以通过自定义的加载器参与之外
    其余动作完全由虚拟机主导可控制。
    到了初始化阶段,开始真正执行类中定义的java程序代码(或者字节码)。
    1.<clinit>()方法是由编译器自动收集类中的所有类变量赋值动作,静态语句块(static{})中语句合并产生的。
    收集顺序是由语句在源文件中顺序决定的。
    2.<clinit>()方法与类的构造函数<init>()不同,
    不用显示调用父类构造器,
    虚拟机会保证在子类的<clinit>()之前父类的<clinit>()方法已经执行完毕。
    因此虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
    3.由于父类的<clinit>()方法先执行,所以父类中静态语句块要优先于子类的变量赋值操作。
    4.<clinit>()方法对于类或者接口来说并不是必须的,
    如果一个类中没有静态语句块,
    也没有对变量赋值的操作,
    那么编译器可以不用生成<clinit>()方法。
    5.接口中不能使用静态语句块,
    但仍有变量初始化的赋值操作,
    因此接口与类一样都会生成<clinit>()方法。
    只有当父接口中定义的变量使用是,父接口才会初始化,
    接口的实现类在初始化时也不会执行接口的<clinit>()方法蘀ꗗߐĀ꟰腱
    注:同一个类加载器下,一个类型只会初始化一次。
    虚拟机会保证一个类的<clinit>()方法方法在多线程环境中被正确的加锁,同步,
    如果多个线程同时去初始化一个类,那么只有一个线程会执行这个类的<clinit>()方法,
    其他的线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕,
    如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。
    概述
    虚拟机类加载机制:
    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析,初始化,最终形成可以被虚拟机直接使用的Java类型。
    注:类型的加载,链接和初始化过程都是在程序运行期间完成的,虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性(java语言动态拓展依赖:运行期动态加载和动态链接)

猜你喜欢

转载自www.cnblogs.com/lllllht/p/9177902.html