• 类文件结构详解及类加载过程解析


    在Java中,JVM可以理解的代码被称之为字节码,它不面向任何的处理器,只面向JVM虚拟机,所以对于字节码来说它屏蔽了任何和处理器指令相关的实现,可以做到一次编译到处运行的能力,为什么它可以有这种能力呢,本质上是因为虚拟机其实替我们的代码做了很多底层兼容适配的工作,所以不同操作系统的平台上的JVM其实是完全不同的;

    Java的字节码其实还有另外一个好处,那就是可以同时兼顾编译执行和解释执行的优点,对于Java程序来说,先通过Java的编译工具可以做到将源代码编译成机器码,这个层面上看好像确实和编译执行的代码很相似,但是它编译出来的字节码却又不是最终的机器码,真正运行的时候,还需要jvm来进行解释执行,所以说jvm同时具备了编译执行和解释执行的特点,不能说是优点;

    好了,接下来我们看一下一个编译好的类文件到底是什么样子;

    看下图:

    一个编译好的类文件(xxxx.class)由以下几个部分组成:

    • 魔法数
    • class文件版本号
    • 常量池
    • 访问标志
    • 当前类、父类、接口、索引集合
    • 字段表集合
    • 方法表集合
    • 属性表集合

    下面分别介绍一下类文件的这些属性都是什么用途:

    魔法数

    魔法数是用来鉴别一个字节码文件是否损坏的,是固定的0xcafebabe,也就是说如果一旦发现字节码文件不是以cafebabe开头,那么就说明这个字节码文件已经损坏,也就没办法走后续的流程了;

    Class文件版本号

    这里的版本号,指的是编译所用的Java编译器的大小版本号,第五六个字节是次版本号,第七八字节是主版本号!高版本的Java虚拟机可以执行低版本的字节码文件,但是低版本的虚拟机是无法执行高版本的编译器编译的Class文件的

    常量池

    紧接着是常量池,常量池的数量是constant_poo_count - 1

    常量池的作用是存放字面量和符号引用,字面量接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值等,我的理解就是常量属性声明时等号后面的那部分,存在于常量池中称之为字面量;

    符号引用属于编译原理方面的概念,包括:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符;也就是代码中等号前面的部分;

    访问标志

    常量池后面是访问标志,这个标志用于标志类或者接口的访问信息,比如说这个class是类还是接口,是public还是abstract等等,如果是类的话是否声明为final等等信息

    当前类、父类、接口、索引集合

    类索引用于确定这个类的全限定名,父类索引用于确定类的父类的全限定名;

    接口索引描述了这个类的所有实现的接口的集合,implement按照从左往右的顺序排列

    字段表集合(Fields)

    用于描述接口或者类中声明的变量,字段包括类变量和实例变量,但不包括在方法内部声明的局部变量;

     access_flag指的是字段的作用域(public、private、protocted等等,是实例变量还是类变量,被static修饰)

    name_index 对常量池的引用,表示的字段的名称

    descriptor_index:对常量池的引用,表示字段和方法的描述符

    attributes_count:一个字段的额外属性,存放额外属性的个数

    attributes:存放具体的额外属性的内容

    方法表集合(Methods)

     方法表的内容看起来和字段表是类似的就不再赘述了

    类加载的完整过程

    接下来我们来讲类加载的完整过程,我们先看一个整体的类加载的过程图:

     就是说一个类的完整加载过程要经过加载 、连接、初始化三个过程,而连接这个过程又包含:验证、准备、解析三个过程;下面我们就一一来介绍一下这几个过程分别都做了哪些事

     

    加载

    1.通过类的全限定名获取定义 此类的二进制字节流

    2.将字节流所代表的静态存储结构转化为方法区运行时的动态存储结构;

    3.在Java堆中生成一个代表该类的class对象,作为方法区中这些数据的访问入口

    注意,虚拟机规范中上述这3点并不具体,因此对于虚拟机实现来说非常灵活,比如“通过类的全限定名获取定义此类的二进制字节流”并没有指明是从哪里获取,怎样获取;比如比较常见的就是从zip包中读取(日后出现的Jar、Ear、War等格式 的基础)以及其他文件生成(比如从JSP文件中)等等

    一个非数据组类的加载阶段是可控性最强的阶段,这一步我们可以自定义类加载器去控制字节流的获取方式(重写类加载器的loadClass()方法)。数组类型不通过类加载器创建,而是由虚拟机直接创建;

    加载完了之后就进入了连接阶段,连接阶段共分成三部分:验证、准备、解析

    验证

     (图源来自JavaGuide公众号文章,推荐)

    验证这部分主要是进行字节码的相关验证,比如文件格式验证,元数据验证,字节码验证,符号引用验证,所谓的字节码验证就是验证是否以cafebabe开头,主次版本号是否在当前java虚拟机能处理的范围内,常量池中是否有不被支持的常量等等

    而元数据验证主要是进行一个语义验证的阶段,主要是对一些基本的语法信息进行验证,比如是否有父类,是否继承了不允许被继承的父类,访问权限又是否可见等,需要保证基本的语义正确;

    字节码验证进行的是更高阶的能力验证,加入刚刚的元数据验证是判断语义是否正确的话,那么字节码验证主要是进行整体逻辑上的验证,即通过数据流和控制流分析确定程序语义是合理合法的。任意时刻操作数栈和指令代码序列都能配合工作;

    最后是符号引用的验证,即确保解析动作能够正确执行,这句话图中解释的比较简单,我们以字节码的文件格式中的类方法表为例,一个Java类想要调用相关的方法,必须去方法表中根据符号引用来找到它具体对应的内存的就偏移量地址,所以这一步其实就是在验证符号引用是否存在于相关的码表中,方便后续步骤的时候可以直接进行转化;

    准备

    准备阶段的主要作用是正式的对类变量进行内存空间的分配,注意这里只有类变量会进行内存空间的分配,而不包括实例变量,何为类变量,就是被static修饰符修饰的变量称之为类变量,除了为类变量分配内存空间以外还会对类变量进行赋初值的操作,加入我们有一个变量是 public static int count = 755,那么在这个阶段其实只是做到了为count变量准备内存空间,并将初始0值赋给这个count变量而已,不过有一个特殊的点就是如果变量被final修饰,那么在准备阶段其就已经会将给定值赋给类变量了,同样是上面的例子,加入 public static final count = 755,此时,在这个阶段count就会被赋值为755 而不会再发生改变了;

    那么这些类变量是在内存的哪块区域开辟的空间呢,在JDK7之前,也就是用永久代来实现方法区的时候,是存放在永久代中的,但是JDK7及之后,类变量随着Class对象一起放到了Java堆中

    (JDK7之后,HotSpot虚拟机已经把原本存放在永久代中的字符串常量池、静态变量等移动到了堆中)

    解析

    何为解析,其实就是将常量池内的符号引用替换为直接引用的过程,解析动作主要针对的对象是类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符等

    符号引用是一组符号来描述目标,直接引用就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄,在程序运行的过程中,只有符号引用是远远不够的,系统需要明确知道这个方法所在的位置,前面我们说到的Java类的方法表,当Java想要调用一个类的方法的时候,只要知道了这个类中方法在方法表中的偏移量就可以直接调用该方法(知道了这个方法的具体实现存储在了哪块内存区域中)

    初始化

    这个阶段才是真正开始调用我们代码中的初始化方法的过程(我理解应该就是构造函数),是类加载的最后一步,这一步JVM才开始真正的执行类中定义的Java程序代码;

    对于(<clinit>)方法的调用,虚拟机会确保其在多线程环境下调用的完全性,也就是说会通过加锁的方式来保证线程安全,不知道各位还记不记得,单例模式中有一种静态子类加载的方式,其实本质上就是利用了类加载过程会加锁的这个条件来使得实例的初始化阶段是线程安全的,也就说不会存在两个或者两个以上的类可以同时初始化我们的单例实例;

    至于具体哪些初始化阶段需要对类进行初始化这边省略(可以看深入理解JVM虚拟机)

    extra:卸载

    额外补充一个类的卸载;

    方法区其实也是一个GC回收的对象,因为它是线程间共享的一块内存资源,不过在方法区进行内存回收效率太低了,因为对常量池和类的回收本身没什么收益,但是从中我们可以得到的信息就是类也是可以被回收的,可是满足什么条件的时候类可以被回收呢?

    • 类的所有实例都已经被回收了,也就是说在虚拟机堆中找不到任何一个类的实例;
    • 类的类加载器已经被回收了;
    • 类的Class对象已经被回收了(存储于堆中),也就是说你无法通过任何反射的途径来访问到类的方法和属性;

    即使是满足了上述的三个条件,也并不能够保证类一定会被回收,也就是说其实类的回收条件是非常非常非常苛刻的;

    同时需要注意的是,JVM自带的类加载器加载的类在JVM的生命周期中是不可能被回收的,也就是说BootstrapClassLoader(启动类加载器),extClassLoader(扩展类加载器),applicationClassLoader(应用程序类加载器)这三种类加载器加载的类不可以被回收,而我们自己实现的类加载器加载的类在满足了上述条件的时候是有可能发生类的回收的;

    本文参考:

    《深入理解JVM虚拟机》

    《JavaGuide类加载过程》

  • 相关阅读:
    【知乎精选】一个产品经理的成长(转)
    互联网性能与容量评估的方法论和典型案例
    系统吞吐量(TPS)、用户并发量、性能测试概念和公式
    基于Windows运行库的UWP—C#高级编程(第10版)学习笔记3
    .Net Core的重要性(价值)—C#高级编程(第10版)学习笔记2
    本书主要内容—C#高级编程(第10版)学习笔记1
    C#—.net开发首选编程语言—摘自《C#高级编程第10版》
    表格标签
    伪类
    ps小知识点备忘录
  • 原文地址:https://www.cnblogs.com/kerwins-AC/p/15586335.html
Copyright © 2020-2023  润新知