一个JAVA文件的使命

764 阅读8分钟

自我介绍:

​ 大家好,我是一个java文件.我的梦想是能够进入一个叫做jvm的世界,听说我可以在那里实现自我价值,走向人生巅峰.

javac-前端编译:

​ 这是我前往jvm大世界的第一站,要从java经由javac工具编译为class的二进制文件(当然也可以是其他形态,取决于是否自己实现classload),编译过程如下:

0.准备过程:

​ 初始化插入式注解处理器.

1.解析与填充符号表:

  • 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树(AST)。

  • 填充符号表。产生符号地址和符号信息。

    符号表是由一组符号地址和符号信息构成的表格。符号表中所登记的信息在编译的不同阶段都要用到,在语义分析(后面的步骤)中,符号表所登记的内容将用于语义检查和产生中间代码,在目标代码生成阶段,党对符号名进行地址分配时,符号表是地址分配的依据。

2.注解处理:

  • 我们可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有 再对语法树进行修改为止,每一次循环过程称为一个轮次.

  • 实现方式:继承AbstractProcessor接口.并实现process方法.例如:lombok可以在编译过程中添加getter,setter,tostring到语法树当.如下(lombok具体细节后续研究)

3.分析与字节码生成:

前几步处理让我们得到了一个结构正确的语法树,但无法保证源程序的语义是符合逻辑的.而语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查,常见的idea爆红问题是在下面前两项检查中未通过触发的提示

  • 标注检查:诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配,等等

  • 数据流分析和控制流分析:检查出诸如程序局部变量 在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题

  • 解语法糖

    • java范型:java的范型其实是比较敷衍的实现方式,仅仅是在编译期间实现的一种方式.
    • 自动装箱、自动拆箱与遍历循环(for-each循环):对一些基础类型的赋值,比较进行自动填充.
    • 条件编译:根据布尔常量值的真假,编译器将会把 分支中不成立的代码块消除掉.

    如下举例,对比即可查看差距:

    • Demo.java
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * @author wuzhentao
     */
    public class Demo {
        public static void main(String[] args) {
            System.out.println("-------------------foreach语法糖---------------------");
            List<Integer> list = Arrays.asList(1, 2, 3, 4);
            int sum = 0;
            for (int i : list) {
                sum += i;
            }
            System.out.println(sum);
    
    
            System.out.println("-------------------自动装箱拆箱---------------------");
            Integer a = 1;
            Integer b = 2;
            Long c = 3L;
            System.out.println(c == (a + b));
    
    
            System.out.println("-------------------条件编译---------------------");
            if (true) {
                System.out.println("yes");
            } else {
                System.out.println("no");
            }
        }
    }
    
    • Demo.class(借用idea查看的class文件,应该是采用了反编译后的形式,但class本身就是一个字节码文件,如下)
    import java.util.Arrays;
    import java.util.Iterator;
    import java.util.List;
    
    public class Demo {
        public Demo() {
        }
    
        public static void main(String[] args) {
            System.out.println("-------------------foreach语法糖---------------------");
            List<Integer> list = Arrays.asList(1, 2, 3, 4);
            int sum = 0;
    
            int i;
            for(Iterator var3 = list.iterator(); var3.hasNext(); sum += i) {
                i = (Integer)var3.next();
            }
    
            System.out.println(sum);
            System.out.println("-------------------自动装箱拆箱---------------------");
            Integer a = 1;
            Integer b = 2;
            Long c = 3L;
            System.out.println(c == (long)(a + b));
            System.out.println("-------------------条件编译---------------------");
            System.out.println("yes");
        }
    }
    
    • Demo.class(借用idea jclasslib插件进行查看 开头是cafebabe常量,如下信息详细用法请查阅深入理解java虚拟机第6章)

    • 大版本号
    • 小版本号
    • 常量池
    • 访问标志
    • 类索引
    • 父类索引
    • 接口索引集合
    • 字段表集合
    • 方法表集合
    • 属性表集合

守门员-类加载器

现在脱去凡胎,我已被练就成为class体,可是想要进入到jvm大世界中要需要一名称为类加载器的组织来接引

1.成员-类加载器组成

  • 大长老:启动类加载器
  • 二长老:扩展类加载器
  • 三长老:应用类加载器
  • 帮主,帮众:通过继承应用类加载器(三长老)成为的帮组,并依次继承的帮众

2.帮规-双亲委派

​ 该组织采用一级一级上报的形式来进行任务的执行,最终都需要将由大长老来决定,只有大长老表示处理不了的才会给下一级放权,最终将任务给到级别最高能处理这个任务的人员,这项规定可以保证jvm世界中类的唯一性.

3.怎么证明我是我-类的唯一性

  • jvm世界中,需要由接引人跟我的名字一起才能确定我的唯一性.

接化发-后端编译器

经过类加载器的提携,我进入了jvm世界当中,现在我需要考虑的问题则是如何将我的class体转换为这个世界的机器码以及如何提升自己的功力.现成的功法大致分为一下1.2.两种

1.解释器与即时编译器

  • 解释器的作用是当程序需要迅速启动和执行的时候,解释器可以首先发挥作用省去编译的时间,立即运行.解释器还可以节省编译花费的内存.

  • 编译器可以把代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率

    • Hotspot虚拟机的即时编译器分为C1(客户端编译器,稳定优化),C2(服务端编译器,激进优化)两种(1.8jdk).
  • 解释器与编译器搭配使用的方式

    • 混合模式:也是hotspot的默认模式,解释器跟编译器都会执行.

    • 解释模式:只有解释器会执行.

    • 编译模式:优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程.

  • 针对性优化-分层编译

    • 由于即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化程度越高的代码,所花 费的时间便会越长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信 息,这对解释执行阶段的速度也有所影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机在编译子系统中加入了分层编译的功能.层数越高消耗的优化时间,占用内存越高,反之越低,动态的改变层数可以均衡代码的启动速度以及多次运行后的执行效率.
  • 触发编译的热点代码:1.被多次调用的方法,2.被多次执行的循环体.

  • 优点(对应于提前编译器):

    • 可以进行性能分析制导优化:不断收集性能监控信息,检查代码的偏好性,例如如果一个条件分支的某一条路径执行特别频繁,而其他路径鲜有问津,那就可以把热的代码集中放到 一起,集中优化和分配更好的资源(分支预测、寄存器、缓存等)给它
    • 激进预测性优化:相对于提前编译来说,即时编译的策略就可以不必这样保守,如果性能监控信息 能够支持它做出一些正确的可能性很大但无法保证绝对正确的预测判断,就已经可以大胆地按照高概 率的假设进行优化,万一真的走到罕见分支上,大不了退回到低级编译器甚至解释器上去执行,并不 会出现无法挽救的后果
    • 链接时优化:Class文件在运行期被加载到虚拟机内存当中,然后在即时编译器里产生优化后的本地代码.因为java的对象引用可以是动态的,所以只有即时编译这种才能进行动态的引用链接优化.

2.提前编译器

提前对代码进行编译

  • 优点:
    • 可以一定程度避免即时编译器带来的性能消耗
    • 可以为编译的程度做缓存加速,也就是省去一部分的即时编译过程,让代码从启动到编译完成的时间.

最后

​ 经过前端编译将我从java体转换为class体,并经由类加载器把我传送到了jvm世界,再加上后端编译器的优化提升了我的功力.相信我现在离走上人生巅峰只差一步;嘿嘿想想还有点小激动呢.