类文件结构和类加载过程执行引擎简述

.Java文件执行产生结果的过程简述

Java源程序通过编译器,根据字节码和Java语法对应的指令,对文件进行编译生成一定结构的class文件即语法分析生成虚拟机认识的字节流,这里涉及到类的结构知识点,每个类或者接口生成一个单独的class文件,之后虚拟机把class类加载到虚拟机方法区内存中这个过程中还包括语法分析验证正确性初始化等步骤,即类加载,如果一个类继承于父类则会先加载父类再加载本类,加载完类后找到main方法,从main方法开始执行class文件里有一个字节码属性,目前只是在方法区内存里,当遇到对象的创建时在堆中进行内存分配保存实例专有的属性值,当执行实例方法时找到已经加载的实例对应的类找到相应的方法执行该方法的字节码,之前的方法入栈,直到执行结束产生结果,卸载类结束。

.class文件的结构简述

.class文件结构顺序如下

  • 魔数和版本号
    每个class文件的头四个字节为魔数,是文件类型的标识,代表这个文件是虚拟机可以识别的class文件,因为扩展名是随意改变的不可靠的,这里class文件类型魔数为0xCOFEBABE,接下来四个字节是保存的是版本号的,虚拟机必须拒绝执行超过其版本号的class文件。

  • 常量池
    常量池保存着整个文件的所有常量,主要是些字面量和符号引用,字符串、final修饰的常量、类接口的全限定名、字段方法的名称和描述等,比如程序中定义的字段名称都会以一个字符串的形似保存在常量池里面,然后其他地方直接应用即可,还要类的全限定名也是这样的保存一个约定格式的字符串,在类加载阶段才把符号引用变成真正的直接引用。这里的第0项表示不引用任何常量,比如父类那里用到

  • 访问标志
    接下来就是两个字节访问标志,这个也比较简单,保存了类接口层次的信息,比如是否为public是否为final修饰的等,每种信息都对应一个比特,0表示无1表示有。目前就用了8位,也就是保存了8种信息。

  • 类父类接口的索引
    接下来就是继承关系信息,类索引和父类索引都是一个u2类型也就是两个字节,都指向常量池的全限定名,到类加载的时候才搞成直接引用,所有类都有父类所以除了Object外所有的类父类索引都不为0,当Object时就是0,而在常量池中0表示什么也不是。接口不止一个接口所以前面有一个u2类型作为计数,表示接口数量,后面和其他一样是个常量池的引用。

  • 字段表集合
    这里保存所有字段的信息,不包含方法内的变量方法内的局部变量运行时才需要,字段变也是保存了标志位,名称,描述符,还有一些额外得属性表。标志位都是什么是否为静态,是否为final修饰,是否为,作用域等和类得访问标志一样就是用一个u2类型表示,一个比特位表示一种状态得有无,名称就引用常量池里的常量,还有描述就是属于什么类型信息引用常量池里一个特殊格式得字符串表示比如int[]类型得记录位[I。属性表集合稍后

  • 方法表集合
    接下来就是方法表集合,和属性表得组织方式几乎完全一样保存了标志位,名称,描述符,还有一些额外得属性表。

  • 属性表集合
    属性表得所有属性名称都是引用常量池得常量,属性表保存些字节码,可能抛出的异常,还有static修饰的字段的值,还有其他好多额外的信息,重点时字节码和异常,字节码保存在属性位code的属性里。

.class文件加载简述

类加载分为加载、验证、准备、解析、初始化、使用、卸载七个阶段,加载、验证、准备、初始化、卸载着五个过程是确定关系来的,解析就不一定,有时候会在初始化后开始的,说的确定顺序是说先加载才能能验证而不是说加载所有了才进行验证,因为这些阶段总是交叉运行的,但是会保证比如要验证这段总得先加载。

  • 加载
    通过类得全限定名将类从静态内存加载到虚拟机运行时得数据区也就是方法区,这里想到了class文件得常量池里保存的全限定名,可以是它,这个加载的途径可以从网络也可以从数据库中也可以其他好多途径可以获取到。然后实例化一个class类作为外部这个数据的外部接口,反射啥的就用到。注意在hotspot这个类实例保存在方法区

  • 验证
    这个阶段就是为了保证字节流信息是否符合当前虚拟机的要求,不会危害到虚拟机的安全,每一个阶段的验证失败都会直接抛出异常,比如文件格式验证验证是否输入的字节流是否符合class文件的规范,元数据验证对字节码描述的信息进行词义分析保证信息符合Java规范的要求比如实现了接口是否对所有的方法进行实现,字节码验证验证确定程序合法和合逻辑比如字节码要操作的是int却出现操作long的情况,或者调转指令调到方法区外区验证等等,这步验证时最复杂的和耗时的,最后时符号引用的验证对类自身以外的信息进行匹配验证比如是否能根据用到的符号引用的全限定名找到该类等。虚拟机提供一个配置进行关闭验证,在一个类已经经过了多次验证时可以关闭节约加载时间。

  • 准备
    这个阶段对类变量进行分配内存和进行初始赋值,内存分配在方法区分配,这里只是类变量static修饰的,不包括实例变量,还有这里的初始化不是在程序中的赋值初始化一般赋值初始化位0,比如static a = 9,这里只赋值为0,到初始化阶段才赋值为9.或者当字段属性表中存在constantValue属性的话用该属性进行赋值通常final修饰的变量会放到constantValue属性里面。比如static final a = 9这个阶段将从属性表中取出9进行赋值。

  • 解析
    解析为将常量池中的符号引用变为直接引用的过程,举例子,比如一个字段为a类,在常量池中保存的只是这个这个a类的全限定名字符串,这一步就转为直接指向该类的指针。

  • 初始化
    目前为止,前面的各个阶段都没有执行过类程序都是于虚拟机控制完成的,这一步就执行的字节码,前面准备阶段已经对类变量进行了一次初始化,但准备阶段都是由虚拟机决定的,不由程序员写的程序决定的初始值,这一步才是按程序设计的意愿进行初始化。这一阶段的实现时通过执行一个类构造器方法<clinit》(),这个方法不是程序员写的,是编译器根据条件收集生成的,收集变量的赋值也就是程序里的初始值,还有静态语块组成static{}的方法,注意这里收集的顺序和源程序的顺序一样,也就是说静态语块只能访问定义在该语块之前的变量,对于之后的变量只能赋值不能访问,而且子类的类构造器方法执行前父类的构造方法已经执行完成也就是说Object的构造方法最先执行,所以父类的静态语块优先于子类的静态语块,而且多线程同时对一个类进行初始化那么只会有一个线程执行类构造方法其他线程堵塞等待

  • 类加载器
    对于任意一个类,都需要类本身和加载它的加载器确定在虚拟机中的唯一性,也就是说同一个源文件同一个虚拟机如果不同的加载器加载那么也就会不想等,表现在equals()方法的返回值,还有instanceof()等。
    类加载器分有启动类加载器(负责加载\lib目录的类),扩展类加载器(负责加载\lib\ext目录下的类),引用程序类加载器(加载用户类库的加载器如果没有自定义加载的话为默认的加载器)和自定义类加载器,程序中的类都由这三种加载器配合进行加载,这几类加载的关系为顶层启动类加载器——>扩展类加载器——>引用程序类加载器——>自定义类加载器,这种继承关系叫做双亲委派模型,这一模型执行过程为,当一个加载器收到加载请求时不会自己加载,先委托给上一层进行加载,当上一层反馈无法完成加载时才尝试自己加载。也就是说Object类都是由启动类加载器加载的,也就是说保证了程序中所有用到Object类作为的父类都是同一个Object类,即使自己定义一个Object类程序遇到要加载Object类时会先加载\lib目录下的Object类。当然这个双亲委派模型是可以破外的。

字节码执行引擎

Java虚拟机是基于栈的解释执行的,优点就是可移植性强,因为不像基于寄存器一样过于依赖寄存器容易受到硬件的约束,相对基于寄存器来说会相对慢一些。栈帧是支持虚拟机进行方法的调用和方法的执行的数据结构它是虚拟机栈的栈元素,运行时从main()方法开始,每一个方法的调用都是一个入栈和出栈的过程。一个栈需要多大的空间和栈的最高深度已经写入code属性中去了所以不受运行时数据的约束。执行的时候程序计数器指的是加载后的class文件的code属性里面的字节码。

  • 局部变量表
    保存了方法的参数和在方法内定义的局部变量,最大值在code属性中由说明,为了节约栈帧空间,这快内存是可以重用的,不是每个变量的作用都是整个方法,当某个变量的作用域到期时这块内存就可以重用。还有第0 个索引默认保存方法所属实例的引用,可以通过this访问这个参数

  • 操作数栈
    这个也有属性指定深度大小,无论任何时期都会超过这个深度。相当于基于寄存器的寄存器的地位,保存用于运算的操作数。

  • 动态连接
    在常量池重的字符串表示的符号引用,有的在类加载的解析阶段就转化为直接引用,有的则需要在运行期间才可以转换为直接引用,这部分称为动态连接。

  • 返回地址
    无论方法时异常了退出还是正常退出,都需要返回被调用的位置,程序才能正常运行,返回时可能要保存一些信息以帮助上层恢复信息,这些就是返回地址,通常情况下存放的是调用者调用处的PC值,也就是说调用结束后从该程序计数器值继续执行。

  • 附加信息
    还允许增加其他规范没有的信息,由不同的虚拟机产商决定。

  • 方法调用
    方法调用不等同于方法执行,方法调用阶段唯一的作用就是决定调用方法的版本,所有方法在常量池里都有一个符号引用与之对应,在类加载阶段会把部分引用转化为直接引用,并且运行区是不可改变的,也就是说在类加载阶段就已经决定调用了哪个方法版本了,这种类方法的调用称为解析
    多态的实现就是方法的重载重写等,而这种实现的方法调用称为分派调用
    1、静态分派,当方法名一样参数类型不一样时即重载时,决定调用哪个方法呢,根据参数的类型和数量决定,主要是在这个参数类型,当传入的是一个实际上new出来的是一个对象,但是赋值给的变量类型是该对象的父类或者接口时,调用方法是按变量类型的类型参数调用,也就是这部分调用方法的版本是在编译的时候进行决定的,在编译阶段编译器根据传入参数声明时的类型进行方法版本的调用也就是转为对应的直接引用。这种叫做静态分派
    2、动态分派,方法的执行过程是先在实际实例中寻找是否有与常量中描述和名称相对应的方法(和上面的不一样的过程,上面调用后还是会经历这个过程),如果找到则验证权限通过则返回这个方法作为直接引用,权限不通过异常,如果该实例没有方法则继续寻找父类做同样的操作,所有父类都没找到异常,这个过程就是重写的本质,这种方法决定调用方法的版本的方法叫做动态分派

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值