Java基础:类文件结构及类加载

Class文件结构

  • 魔数 4bits 确定该文件是否是可接受的Class文件(0xCAFEBABE)
  • 版本号 4bits 包括次版本号和主版本号
  • 常量池 包括字面量(文本字符串,声明为final的常量值)和符号引用(类和接口的全限定名,字段的名称和描述符,方法的名称和描述符)
  • 访问标志 2bits 标志识别类或者接口层次的访问信息,如类是否为public,是否为abstract,是否为final,是Class还是Interface。
  • 类索引 父索引与接口索引集合 用于确定这个类的继承关系
  • 字段表集合 包括用于描述接口或类中声明得变量,包括public private protected static final volatile transient synthetic(字段是否为编译器自动产生) enum,以及变量简要名称和类型描述符(基本类型,数组)。
  • 方法表集合 类似字段表集合,包括前缀,简要名称,类型描述符,不包含方法中的代码。
  • 属性表 存放多种属性,如方法中的代码存在Code属性中,操作栈深度最大值,exception等。

类加载机制

类加载的整个生命周期: 加载,验证,准备,解析,初始化,使用,卸载

对类初始化的情况

  • new关键字实例化对象,读取设置类的静态字段(非final修饰),调用一个类的静态方法
  • 反射调用
  • 初始化一个类时,若父类没有初始化,则先初始化父类
  • 虚拟机启动时,用户指定的主类
  • java.lang.invoke.MethodHandle的解析结果是一个静态方法,则需要先初始化方法对应的类

加载

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将字节流代表的静态存储结构转化为方法区的运行时结构。
  • 内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

验证

  • 文件格式验证:对Class文件进行格式校验。是否以魔数开头,主次版本号是否可处理,常量池中常量是否全支持...
  • 元数据验证:对字节码描述的信息进行语义分析。这个类是否有父类,父类是否不允许继承,字段是否与父类矛盾...
  • 字节码验证:对数据流和控制流进行分析。对方法体进行校验分析,如类型校验等。
  • 符号引用验证:对符号引用进行校验保证其可转换为直接引用。

准备

  • 为变量分配内存。(仅包含类变量,不包括实例变量)
  • 为变量设置初始值。(仅设置类型对应的默认值,不赋值)

解析

解析阶段虚拟机将常量池内的符号引用替换为直接引用。

符号引用用一组符号描述所引用的目标,使用时可以无歧义的定位到目标。

直接引用可以是直接指向目标的指针,相对偏移量,或能够间接定位到目标的句柄。不通虚拟机中翻译出的直接引用一般不同。

初始化

初始化是类加载过程中最后一步。初始化接端是执行类构造器<clinit>()方法的过程。该方法是编译器自动收集类中所有类变量的赋值动作和静态语块(static{})中的语句合并而成的,其收集顺序取决于源文件中的出现顺序。虚拟机会保证子类的<clinit>()执行前,父类的<clinit>()已经执行过,因此不需要显示的调用。多线程环境中,<clinit>()方法使用悲观锁阻塞,并保证只被运行一次。

类加载器

上述过程中,第一步加载中,通过一个类的全限定名来获取描述此类的二进制字节流这个机制放在Java虚拟机外部实现,实现这个动作的代码模块称为类加载器。任意一个类,都需要由它的加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类命名空间。两个类是否相等,只在这两个类是由同一个类加载器加载的前提下才有意义,否则必顶步等。(相等指equals()方法,instanceof关键字等)

双亲委派模型

从JVM角度,只存在两种不同的类加载器:启动类加载器,C++实现,JVM一部分;其他类加载器,Java实现,独立于类,继承java.lang.ClassLoader。

启动类加载器负责将<JAVA_HOME>\lib中JVM识别的类库加到JVM内存中(如名字为rt.jar),启动类加载器无法被Java程序直接引用,用户自定义加载器时,把加载请求委派给引导类加载器只需要使用null替换。

其他类加载器包括扩展类加载器和应用程序类加载器,前者负责加载<JAVA_HOME>\lib\ext目录中的类库,后者是ClassLoader::getSystemClassLoader()的返回值,负责加载用户类路径上指定的类库,没有自定义过类加载器,这个加载器就是程序中默认的加载器。

这些加载器的关系如图所示

双亲委派模式要求除了顶层的启动类加载器,其余的类加载器都应该有自己的父加载器。其工作过程中,如果一个类加载器收到了类加载的请求,它首先不回去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层级的加载器都是如此,因此所有的加载请求最终都应该传到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。该模式保证了Java程序中的稳定性,如,重写rt.jar类中的类,不会有编译错误,但无法被加载。

双亲委派模型不是强制性约束模型,由于它的某些缺陷(如基础类要调用回用户的代码),某些标准服务打破了它,如JNDI服务,用于对资源进行集中管理和查找,JNDI使用了线程上下文类加载器。Java中所有涉及SPI的加载动作都采用这种方式,如JDBC。另外,模块热部署也破坏了双亲委派模型。

posted @ 2019-09-18 17:47  CieloSun  阅读(357)  评论(0编辑  收藏  举报