深入理解JAVA虚拟机——总结2:虚拟机类文件&类加载

来源:互联网 发布:长春大学教务处软件 编辑:程序博客网 时间:2024/04/30 11:16

所谓类文件

  • Java虚拟机与特定的语言不存在绑定关系
  • Java虚拟机只与class文件相关联
  • Java编译器将Java语言编译成class文件,再将class文件交由虚拟机处理
  • 其他语言如果可以被其相应的编译器编译成class文件,同样可以由Java虚拟机来处理
  • 一个class文件对应唯一的类或接口的定义信息
  • 一个有效的类或接口不一定都以磁盘文件形式存在,也可以由类加载器直接生成
  • class文件是一种特定的二进制文件格式,存储“字节码”

类文件结构

  • class文件中存储的数据,以8位字节为基础单位,顺序严格,无分隔符,无空隙;8位以上的数据项按照高位在前分割存储
  • class文件中数据类型:
    • 无符号数:基本数据类型,u1,u2,u4,u8,数字代表字节数
    • 表:符合数据类型,由多个无符号数或者其他表组合而成,已“_info”结尾,整个class文件是一个表
  • 存储数据的先后顺序:
    • 4个字节魔数(Java -> CAFEBABE)
    • 4个字节版本号
    • 常量池(与class文件的其他数据项关联最多,占用空间最大)
      • 容量计数器(从1开始计数,如:22代表21个常量1~21;第0项常量代表不引用任何一个常量池项目)
      • 常量(与其他数据项关联、与类加载过程关联)
        • 字面量:文本字符串、声明为final的常量值
        • 符号引用:
          • 类和接口的全限定名
          • 字段的名称和描述符
          • 方法的名称和描述符
      • 常量以表的形式存在:
        • u1类型的tag标志位(代表该常量是何种字面量或是何种符号引用)
        • 该常量类型特有的表结构
          • 如,字符串字面量:tag、length(表示字符串占用多少字节)、bytes(字面量内容)
          • 如,类的符号引用:tag、name_index(索引值,指向常量池中一个字符串字面量)
    • 2个字节的访问标志:类or接口,是否public,是否abstract,是否final等
    • 类索引、父类索引、接口索引集合:确定类的继承关系
      • 类/父类(出Object类以外,父类索引均不为0)用u2类型索引值,引用常量池中的类符号引用常量->字符串字面量表示累的全限定名(诸如org/fenixsoft/clazz/TestClass;结尾的“;”用于区分多个全限定名不至于混淆而特别加入)
      • 接口计数器(0表示没有实现任何接口)
      • 接口索引
    • 字段表集合(字段:类&实例级的变量,不包含方法内局部变量)(不会列出从超类和父接口中继承的字段,可能存在代码中不存在的自动添加的字段)(字节码允许存在描述符不同的重名字段)
      • 字段计数器
      • 字段访问标志:public、static、volatile、enum等等
      • name_index:字段的简单名称(不含类型)
      • descriptor_index:字段的描述符(字段的数据类型)
        • int->I, void->V, boolean->Z, 对象类型->Ljava/lang/Object
        • 数据类型,每一维度加一个前置[,如int[]->[I
      • attribute_count
      • 属性表:详情见下文attribute_info(final static变量可能有ConstatntValue属性)
    • 方法表集合(字段:类&实例级的变量,不包含方法内局部变量)(不会列出从超类和父接口中继承的方法信息,可能存在代码中不存在的自动添加的方法)(字节码允许存在描述符不同的重名字段,只要返回值不同,其他都想吐的方法同样可以共存在一个class文件中)
      • 方法计数器
      • 字段访问标志:public、static、synchronized、abstract、native等等
      • name_index:字段的简单名称(不含参数)
      • descriptor_index:字段的描述符(方法的参数列表和返回值,参数列表为()内表示参数的数量、类型、顺序)
        • 先参数列表后返回值:int indexOf(char[] source, int sourceOff)-> ([CI)I
      • attribute_count
      • 属性表:详情见下文attribute_info
    • 属性表集合
      • 属性计数器
      • 属性:不要求具有严格的顺序,只要不重名即可,各个属性的内部结构完全自定义,虚拟机默认忽视不认识的属性。
        • attribute_name_index:引用常量池的字符串字面量
          • Code属性:字节码指令
            • max_stack:操作数栈的最大深度,决定虚拟机运行时分配栈帧的操作栈深度
            • max_locals:局部变量表所需存储空间,单位是slot,与局部变量数量并不绝对关联,存在变量复用
            • code_length: 字节码长度
            • code: 字节流,每个指令是一个u1类型的单字节
            • 显示异常处理表集合:
              • start_pc & end_pc: try代码块
              • catch_type:引用常量池的类符号引用常量,代表异常类型
              • hander_pc: 异常处理代码起始位置
          • Exception属性: 类举出方法中可能抛出的异常(checked exceptions),即方法描述时throws后面列举的异常(与上面的异常处理表集合不是一回事)
          • LocalVariableTable属性: 描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,非必需,取消后arg0等占位符代替参数名
          • LineNumberTable:描述Java源码行号与字节码行号对应关系,非必需,取消后调试程序时无法设置断点,stacktrace中也不会显示出错行号
          • SourceFile:记录生成该class文件的源码文件名称,对于文件名与类名不同名的情况更重要
          • ConstantValue: 通知虚拟机自动为静态变量赋值,只有被static final修饰的基本类型或string变量才有该属性
          • InnerClasses: 记录内部类与宿主类之间的关系
          • Deprecated:类、字段、方法不再推荐使用
          • Synthetic:字段、方法不是由源码直接产生,而是编译器自动添加
          • StackMapTable: 包含0~多个栈映射帧,每个栈映射帧代表一个字节码的偏移量,检查目标方法的局部变量和操作数栈所需要的类型来确定字节码指令是否符合逻辑约束(代替类型推倒验证器),一个方法最多有一个该属性
          • Signature: 记录泛型签名信息,支持Java的反射API获取泛型类型
          • BootstrapMethods: 保存invokedynamic指令引用的引导方法限定符,一个类最多有一个该属性
        • attribute_length:属性值所占位数(整个表长度减去name_index和length所占的6个字节)
        • info:自定义,如上面Code属性和Exception属性的info都不完全一样

虚拟机类加载机制

  • 在程序的运行期间,把描述类的数据从class文件加载到内存,校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型,动态加载、动态连接
  • 类加载过程:
    • 加载
      • 在虚拟机外部,通过类的全限定名获取定义该类的二进制字节流
        • 该二进制字节流不仅仅是java源码编译生成的class文件,还可能来源于网络、运行时计算生成、其他文件生成、甚至是用十六进制编辑器直接编辑生成
      • 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构
        • 非数组类的加载阶段需要类加载器
          • 每个类加载器都有一个独立的类名称空间,不同加载器加载同一个类文件得到的类仍是不同的
          • 类加载器的类别
            • Bootstrap classloader启动类加载器,是虚拟机自身的一部分
              • 加载\lib目录中可被虚拟机识别的类库
              • 无法直接被Java程序引用,可在自定义类加载器中使用null来委派加载请求
            • 其他类加载器,独立于虚拟机外部,继承自抽象类java.lang.ClassLoader
              • Extension ClassLoader扩展类加载器
                • 加载\lib\ext目录中的类库
                • 可直接使用
              • Application ClassLoader应用程序类加载器
                • 加载用户类路径上所指定的类库
                • 程序默认类加载器,可直接使用
              • 自定义类加载器
          • 双亲委派机制
            • 上面四类加载器,从上到下的“父子”关系,以使用组合而不是继承的关系复用父加载器
            • 一个类加载器收到请求,会委派给父加载器加载,父加载器无法加载时才会由子加载器加载
            • 保证如java.lang.Object这样的基础类在程序的各种环境中都由同一个类加载(不同类加载器加载同一个类得到不同的类对象),都得到同一个类
          • 破坏双亲委派模型
            • 处理双亲委派模型诞生之前的自定义类加载器实现代码时
            • 基础类需要调用用户代码时
              • 线程上下文类加载器(thread context classloader):父类请求子类去完成类加载动作,如JDBC等设计SPI(service provider interface)加载动作的情况
            • 对程序动态性的追求
              • 如代码热替换、模块热部署等不需要重启就能更改程序的情况
              • OSGi自定义类加载机制:每一个程序模块(bundle)都有一个自己的类加载器,替换模块时类加载器一起换掉
        • 数组类由Java虚拟机直接创建,而不通过类加载器
          • 数组类的元素类型最终还是由类加载器创建
      • 内存中实例化一个代表该类的java.lang.Class对象
        • 该对象不一定存在Java堆中(Hotspot虚拟机的class对象存在方法区内)
        • 作为方法区该类的各种数据的访问入口
    • 连接(与加载过程的开放时间存在固定的先后关系,但一部分字节码文件格式验证动作与加载过程交叉进行)
      • 验证:确保class文件的字节流中包含的信息符合虚拟机的要求,重要但非必需,一旦class文件不是java源码编译而来,不检验则后患无穷
        • 文件格式验证:参照上文的类文件结构,验证通过后字节流才会流入内存方法区存储
        • 元数据验证:语义分析,验证字节码描述的信息是否符合Java语言规范(该类是否有父类、该类的父类是否允许被继承、该类是否是抽象类、是否实现了要求实现的所有方法、字段和方法是否与父类矛盾等等)
        • 字节码验证:分析数据流和控制流,确定程序语义是否合法、符合逻辑(操作数栈的数据类型与指令代码序列是否相符、跳转语句是否跳出方法体、类型转换是否有效等)——类文件中属性表的StackMapTable将字节码验证的类型推导转换为类型检查,节省时间
        • 符号引用验证:类对自身之外的常量池符号引用信息进行匹配性校验(是否能通过字符串字面量找到对应的类、字段、方法,这些类、字段、方法的访问性是否允许被访问等等)
      • 准备:为类变量(不包含实例变量)在方法区中分配内存并设置初始值(通常为数据类型的零值),只有在类文件属性表存在ConstantValue属性(static final)时,该阶段会设置其初始值为指定值而非零值
      • 解析:常量池内的符号引用(引用的目标不一定已经加载到内存)替换为直接引用(引用的目标一定已经加载到内存)
        • 具体发生时间不确定,可在初始化发生之前或之后,需要保证在执行诸如getstatic、invokestatic、putstatic、new之类的用于操作符号引用的字符指令之前完成符号引用的解析
        • 对同一符号引用可以进行多次解析请求,除invokedynamic指令(动态语言支持,只有等程序实际运行到该指令时才会解析)之外,均缓存第一次解析结果,避免重复解析
        • 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符
          • 类或接口:
            • 类加载器加载代表符号引用的全限定名指定的类,
            • 加载器父类或接口
            • 验证访问权限
            • 完成解析
          • 字段:
            • 加载字段表内class_index(即,字段所属类或接口)中的符号引用所代表的类,
            • 搜索加载的类是否包含该字段,
            • 从下往上递归搜索各个接口和父接口是否包含该字段,
            • 从下往上递归搜索父类是否包含该字段,
            • 权限验证,
            • 完成解析
          • 类方法:
            • 加载类方法表内class_index(即,方法所属类——不能是接口)中的符号引用所代表的类,
            • 搜索加载的类是否包含该方法,
            • 从下往上递归搜索父类是否包含该方法,
            • 从下往上递归搜索该类实现的各个接口和父接口是否包含该方法,
            • 权限验证,
            • 完成解析
          • 接口方法:
            • 加载接口方法表内class_index(即,方法所属接口——不能是类)中的符号引用所代表的接口,
            • 从下往上递归搜索该接口的父接口是否包含该方法,
            • 完成解析(不需要权限验证)
    • 初始化:执行类构造器clinit()方法的过程
      • 在加载、验证、准备之后,与解析无明显顺序关系
      • 真正开始执行类中定义的Java程序代码
      • **有且仅有**5种情况下必须立即对类进行初始化:
        • 构建:遇到new(实例化对象)、getstatic(读取未被final修饰或未放入常量池的静态字段)、putstatic(设置静态字段)、invokestatic(调用静态方法)指令时,未初始化的类需先进行初始化
        • 反射:使用java.lang.reflect报方法对类进行反射调用
        • 继承:初始化父类
        • 程序入口:包含main()方法的执行主类
        • 动态语言支持时解析出的方法句柄对应的类
      • clinit()方法的生成:
        • 编译器自动收集类中所有的类变量赋值动作和静态语句块中的语句合并而成
        • 顺序与源文件顺序一致
        • 静态语句块能访问定义在之前的变量,对之后定义的变量,可以赋值不能访问
        • 不需要显示调用父类构造器,父类clinit()方法先行加载完毕,第一个执行clinit()方法的是Object类
        • 虚拟器保证多线程时clinit()方法会被正确的加锁、同步,clinit()方法耗时较长时可能造成进程阻塞
        • 接口的clinit()方法不需要先执行父接口的clinit()方法,只有当使用父接口定义的变量时才会初始化
        • 接口的实现类在初始化时不会执行接口的clinit()方法

对象的创建

  • 虚拟机遇到new指令时
    • 检查指令参数是否能定位到常量池中的类的符号引用
      • 检查该符号引用所代表的类是否已经被加载、解析和初始化
        • 类进行加载、验证通过后,为对象在Java堆中分配内存空间
          • 内存分配方法
            • 指针碰撞(内存绝对规整):已用过的空间和空闲空间分隔两边,中间指针作为临界点,挪动指针分配空间
            • 空闲列表(内存不规整):已用和空闲空间交错,维护列表记录可用内存块,分配内存后及时更新记录
            • 根据垃圾收集器是否带有压缩整理功能决定Java堆是否规整
              • serial、parNew:具有compact过程,规整,指针碰撞
              • CMS: 空闲列表
          • 线程安全保障
            • CAS(compare and swap)+失败重试,保证更新操作的原子性
            • TLAB(thread local allocation buffer)
        • 将对象空间中示例数据部分初始化为零值
          • 示例数据分配策略:相同宽度的字段放在一起、父类变量在子类之前、子类较窄变量可插入父类变量空隙
        • 设置对象头信息
          • 对象自身的运行时数据(mark word):hashcode、GC分代年龄、锁状态、偏向线程ID等,32bit或64bit,存储空间可复用
          • 类型指针(可有可无):指向类元数据
            • 根据虚拟机栈的reference数据访问Java堆上的对象的方式
              • 句柄:Java堆中划分一块内存作为句柄池,reference(存储对象的句柄地址)->句柄池(存储对象的实例数据和类型数据信息)->java堆实例池中的对象实例数据 & 方法区对象类型数据
              • 直接指针:reference(存储对象地址) -> java堆对象(对象头中包含类型指针)-> 方法区对象类型数据
              • 优缺点:
                • 句柄稳定,对象移动(垃圾回收)不需要修改reference,只需要修改句柄,更常见
                • 指针快速,节省一次指针定位时间开销
          • 数据长度(如果对象是Java数组)
        • 虚拟机部分对象已经产生
        • 执行Java程序对应的init方法,根据程序员意愿初始化对象数据
0 0
原创粉丝点击