美文网首页
01-JVM原理

01-JVM原理

作者: XAbo | 来源:发表于2022-09-05 22:17 被阅读0次

    通过编译器将源代码编译称为字节码文件(包含程序运行各类信息的数据文件),字节码文件被类加载器加载到内存中,在内存中将各类数据分类整理,最后在解释器的作用下,将程序运行起来。

    1.静态变量应在逻辑的方法区上(即类变量) 2.方法区中的“字节码”修改为“指令码”比较合适

    一、字节码规范

    JVM不关心类文件的来源,可源于java 、JRuby、Groovy程序等;并且类文件并非依赖于磁盘文件,可以只存在内存中。

    class文件跟xml一样是用来存储数据的。类文件由“无符号数”、”表“组成。类文件规范如下:

    ClassFile {
        u4             magic; //Class 文件的标志
        u2             minor_version;//Class 的小版本号
        u2             major_version;//Class 的大版本号
        u2             constant_pool_count;//常量池的数量
        cp_info        constant_pool[constant_pool_count-1];//常量池
        u2             access_flags;//Class 的访问标记
        u2             this_class;//当前类
        u2             super_class;//父类
        u2             interfaces_count;//接口
        u2             interfaces[interfaces_count];//一个类可以实现多个接口
        u2             fields_count;//Class 文件的字段属性
        field_info     fields[fields_count];//一个类可以有多个字段
        u2             methods_count;//Class 文件的方法数量
        method_info    methods[methods_count];//一个类可以有个多个方法
        u2             attributes_count;//此类的属性表中的属性数
        attribute_info attributes[attributes_count];//属性表集合
    }
    

    存放的数据和位置:

    • 字面量和符号引用存放在常量池中:cp_info;
    • 类的访问标志(private、public等)存放在:access_flags;
    • 类信息(全限定名)存放在:this_class、super_class
    • 字段信息(字段作用域、是否static、final、volatile、transient、数据类型、字段名称)存放在field_info;
    • 方法信息存放在:method_info。注意方法的具体代码(字节码指令)是存放在attribute_info的Code属性中。

    常量池:17种常量类型且每种类型有完整独立的数据结构,每一项都是一个表,互相间没有共性无联系,每个数据结构的第一位都是标识位。

    属性表集合:虚拟机规范一共制定了29项;与常量池类似,每个属性都有自己的存储结构。

    根据虚拟机的字节码文件引出以下问题:
    1. 虚拟机如何将源代码编译为字节码?
    2. 虚拟机如何字节码文件加载到内存中?
    3. 字节码加载到内存后,如何让程序运行起来呢?

    二、编译阶段

    1. 虚拟机如何将源代码编译为字节码文件?

    将源代码编译为字节码是由编译器完成的; 根据编译期的不同分为:前端(javac)、即时(C1和C2)和提前(Jaotc)编译器。提前编译器是直接把程序编译成与目标机器指令集相关的二进制代码。

    前端编译器

    需要注意的是前端编译器如Javac,对代码的运行效率几乎没有任何优化措施,因为虚拟机设计团队选择把对性能的优化集中到运行期,这样可以让那些不是由javac产生的字节码文件(如JRuby、Groovy程序)也能享受到编译器优化所带来的性能提升。但是javac这类编译器可以通过“语法糖”提高编码效率。

    “语法糖”:目的是提高代码开发效率。

    以下是Java支持的语法糖:

    • 泛型:Java采用的是“类型擦除式泛型”,即泛型只存在于源码中,编译后的字节码中全部泛型被替换成为原来的裸类型,并在相应的地方插入了强制转型代码。在运行期间ArrayList<Integer>和ArrayList<String>是一个类型(方法重载时这两个类型属于同一个类型,编译报错)。这样做的缺点有两个:1.运行期间无法得到泛型类型信息,会让代码变得繁琐。2.泛型擦除后,导致ArrayList<int>这种类型不支持,因为基础数据类型不能与Object互转。
    • lambda表达式:不算纯粹的语法糖,但是在前端编译器中做了大量的转换工作。
    • 其他语法糖:自动装箱、拆箱;循环遍历;变长参数;条件编译;内部类;枚举类;断言语句;数值字面量;对枚举和字符串的switch支持;try语句中定义和关闭资源。

    以Javac为例的前端编译器:虚拟机规范对编译的约束相当宽松,极端情况下会出现在javac中可以编译但是在IDE中不能编译。编译主要分为:

    • 1 解析与填充符号表:
      1.1 词法分析:是将源代码的字符(程序编写的最小单位)流转变为标记(程序编译的最小单位)集合的过程。
      1.2 语法分析:根据标记序列构造抽象语法树,抽象语法树每个节点都代表一个语法结构,例如包、类型、运算符、接口、注释都可以是一种特定的语法结构。
      1.3 完成词法和语法分析后,需要对符号表进行填充;符号表是由一组符号地址和符号信息构成的数据结构,类似于哈希表中键值对的存储形式。

    • 2 注解处理器执行:注解在设计上原本与代码一样只会在运行期间起作用,但在JDK6后提出“插入式注解处理器”的API,可以将注解提前在编译期对代码中的特定注解进行处理。可以将“插入式注解处理器”看作是一组编译器的插件,这些插件工作时允许读取、修改和添加抽象语法树的任意元素。如插件Lombok就是利用“插入式注解处理器”API干预编译器的行为。

    • 3 语义分析和字节码生成:
      3.1 语义分析:抽象树能够表示一个正确的源程序,但无法保证程序的语义符合逻辑。语义分析就是对结构上正确源程序进行上下文相关性质的检查。
      3.2 语法糖解析:语法糖避免的代码的“啰嗦”和减少语法“错误”,但是在编译期间还是需要将“语法糖”恢复原来的语法。javac中通过desugar()完成解析。
      3.3 字节码生成:字节码生成不见将前面步骤所生成的信息转化成字节码写道磁盘,同时还进行少量的代码添加和转化工作。例如实例的构造器<init>()。

    即时编译器

    按照虚拟机规范,即时编译器和提前编译器都是非必需的。

    即时编译器:用到即时编译器的时候,虚拟机已经完成类的加载过程,当字节码文件进入内存后,Java程序最初先是通过解释器执行的,当虚拟机发现某个方法或代码块运行的特别频繁,就认定这些代码是“热点代码”(多次被调用和多次被执行的循环体),为了提高热点代码的效率,虚拟机通过即时编译器进行代码优化(会改变字节码文件)。这种编译方式因为发生在方法执行过程中,被形象的称为“栈上编译”。

    即时编译器在HotSpot中内置了三个,其中“客户端编译器(C1)”和“服务端编译器(C2)”存在很久,JDK10后出现了Graal编译器。

    在分层编译模式之前,虚拟机都是采用一个解释器一个编译器搭配的工作(混合)模式;但是也可以通过设置是否采用混合模式。分层模式之后解释器、客户端编译器(C1)和服务端编译器(C2)会同时工作,热点代码被多次编译,用客户端编译器获取更高的编译速度,用服务端编译器获取更好的编译质量。

    代码被标记为热点代码的方式有两种:

    • 基于采样的热点探测:虚拟机周期性的检查各个线程的调用栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是热点代码。
    • 基于计数器的热点探测:虚拟机会为每个方法建立计数器,如果某个方法执行次数超过阈值,那么这个方法就是热点代码。HotSpot为每个方法准备“方法调用计数器”和“回边计数器”,当到达阈值后就会触发编译器进行优化。

    编译器如何进行热点代码的优化?

    在后台执行编译的过程,前端编译器利用简单快速的三段式编译器,进行局部(如常量传播、空值检查消除)优化。而后端编译器会执行大部分经典的优化动作:无用代码消除、循环展开、循环表达式外移、消除公共子表达式、常量传播、基本块重排序、范围检查消除、空值检查消除等。

    提前编译器

    按照虚拟机规范,即时编译器和提前编译器都是非必需的。
    提前编译在JDK1.0时候就可以使用外挂的提前编译。但是提前编译在很长时间内没有进展和应用;直到2013,在Android世界中出现了ART使用提前编译,把使用即时编译的Dalvik彻底终结。提前编译器才大展身手。

    三、加载阶段

    1. 虚拟机如何字节码文件加载到内存中?

    通过类加载将字节码的数据加载到内存中。类的加载分为:加载、连接(验证、准备、解析)、初识化、使用、和卸载。其中解析可不按照该顺序。

    类的加载过程:

    • 加载:通过类的全限定名来获取定义该类的二进制流;然后将字节码中所代表的静态存储结构转换为方法区运行时的数据结构。注意:数据已经从字节码转移到内存中的方法区了。
    • 验证:验证文件格式、验证元数据、字节码验证(Code属性的数据)、最后是符号引用验证。
    • 准备:将类中定义的变量分配到方法区并初始化;这里分配仅仅包括类变量(static修饰的),而不包括实例变量,实例变量会在对象实例化的时候随着对象一起分配到堆内存中。
    • 解析:将字节码中常量池中的符号引用替换直接引用。
    • 初始化:在准备阶段变量被赋〇值,在初始化阶段中会根据程序员通过程序编码制定主观计划去初始化类变量和其他资源。

    类的加载器

    对于任意一个类,都必须由加载它的加载器+类本身确定在虚拟机中的唯一性。
    对于虚拟机来说只有两类加载器:启动类加载器和其他类加载器。在程序员角度Java一直保持着三层类加载器、双亲委派的类加载架构。

    三层类加载器、双亲委派

    启动类加载器:负责加载JAVA_HOME\lib下的类。
    扩展类加载器:负责加载JAVA_HOME\lib\ext下的类。
    应用程序加载器:负责加载用户类路径下的所有类。
    双亲委派:一个类加载器收到加载类的请求后,首先将请求委派给父类加载器,每个层次的类加载器都是如此;最终请求会被传给启动类加载器,只有当父类加载器反馈无法完成这个加载请求时,子加载器才会尝试自己去加载该类。这样的好处是Object在程序的各类中都能保证是同一个类。

    四、运行阶段

    1. 字节码加载到内存后,如何让程序运行起来呢?
    • 将字节码中的数据分类放入内存中的不同区域。
    • 虚拟机解释器解释栈中的指令码,利用栈中的“操作数栈”执行的指令(入栈进行计算,出栈则存入局部变量表)。
    栈为主的引擎

    相关文章

      网友评论

          本文标题:01-JVM原理

          本文链接:https://www.haomeiwen.com/subject/yhswnrtx.html