2024年java基础语言编程实现与源码分析

java基础语言编程实现与源码分析java 程序的编译工作通常使用 IDE 或 Maven Gradle 等工具完成 开发过程容易忽视 java 编译期隐藏的技术细节 深入理解 javac 编译等相关概念 javac 是 JDK 的 Java 语言前端编译器工具 将满足 Java 语言规范 JLS Java Language

大家好,我是讯享网,很高兴认识大家。



java 程序的编译工作通常使用 IDE 或 Maven,Gradle 等工具完成,开发过程容易忽视java编译期隐藏的技术细节,深入理解 javac、编译等相关概念。

javac 是 JDK 的Java语言前端编译器工具,将满足 Java 语言规范(JLS, Java Language Specification)的 .java 源文件编译成为满足JVM规范(JVMS, Java Virtual Machine Specification)的 .class 字节码文件。

javac的知识合集 = 编译原理 + JDK( JSR-269 Pluggable Annotation Processing API,JSR 199 the Java Compiler API,语法糖,javac工作原理等)+ JLS + JVMS + bytescode

javac的知识引申 = 生成字节码编译过程,并发编程内存模型、java 关键字实现、字节码增强技术等。

一般而言,javac 的编译过程为:源代码 –(词法分析)–> 符号Token流 –(语法分析)–> 抽象语法树 –(注解处理器)–> 插入式注解语法树 -(语义分析)-> 完整语法树 –(生成代码)–> 字节码。是java的前端编译器。

javac由java语言编写,方便调试学习。

1 hello javac

1.1 javac 经典应用

.java 源文件:

 
讯享网 

编译为 .class 类文件:

讯享网
  1. -d 指定了生成class文件的根目录,并且会根据class的包路径创建子目录。
  2. -cp JRE搜索资源文件的路径指定,默认当前路径,只会影响当前进程,覆盖CLASSPATH。

1.2 javac 实例场景

 

自动化编译脚本

讯享网

1.3 编译 与 javac

1.3.1 编译

编译:将便于人编写、阅读、维护的高级计算机语言写的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序的过程。负责这一过程的处理的工具叫做编译器

简单理解,编译 = 高级语言源代码 —(分析 + 翻译 + 优化)—> 低级语言机器码。

编译描述分析语法、词法、语义翻译java语言规范 -> jvm规范优化改善编码风格并提高效率

编译原理中,根据编译任务不同:

字节码不是机器能识别的语言,还需要 JVM 再将字节码转换成机器码。

编译期任务代表前期编译器.java -> .classSun Javac、Eclipse JDT 的增量式编译器后端运行期编译器.class -> 机器码HotSpot 的 C1、C2 编译器静态前期编译器.java -> 机器码GCJ、Excelsior JET

前端编译为字节码的好处:

  1. 解耦后端JVM编译,减少JVM工作,避免每次执行时词法、语法、语义分析。方便读取,执行速度比直接解析源代码(AOT)快。
  2. 字节码可以由Groovy,Clojure,Scala跨语言生成,供JVM调用。
  3. 字节码有版本信息,可以在编译过程在一些语言层面上擦除版本变化
  4. 字节码格式比源码紧凑、轻量,方便方便网络传输。嵌入式设备不够资源跑起完整的编译器,只需嵌入一个小巧的JVM就可以编译源码。

1.3.2 前端编译 javac

javac 是java前端编译的一种编译器实现。

🗣 Tips :

  1. Javac对代码的运行效率几乎没有优化措施,性能的优化集中在后端的即时编译器中。javac编译器实现一些“语法糖”,例如foreach语法、注解等。字节码是对程序应该如何表现的描述,JVM对Program的行为和硬件有更多了解,JIT时对字节码进行任意优化。在很多情况下,编译时优化阻碍了JIT时更重要的优化。
  2. 《深入理解JAVA虚拟机》第十、十一章 编译运行期优化

1.3.3 反编译

  • javap 将字节码转化为看得懂字节码,底层依赖了标记和、两个指令来实现同步。
  • jad 不支持 Java 8 - lambda 表达式,字符串的 switch 是通过和实现的。
  • IDEA插件 jclasslib 字节码查看器 。

1.3.4 后端编译 Just-In-Time Compiler

本文关注javac的编译过程,对后端编译一笔带过。

传统 JVM 通过解释字节码java基础语言编程实现与源码分析将其翻译成对应的机器指令执行。为了解决效率问题,小部分热点代码消耗大部分的资源,引入 JIT 技术。

  1. 进行热点探测(Hot Spot Detection)识别热点代码(Hot Spot Code)翻译成机器码后缓存。HotSpot 使用基于计数器的热点探测(Counter Based Hot Spot Detection)设计方法计数器(方法、代码块)、回计数器(for/while)统计方法的执行次数,超过阀值就认为是热点方法,触发JIT编译。
  2. HotSpot 内置  (C1 更好的编译速度)和(C2 更好的编译质量)两种JIT编译模式分层编译。
  3. 编译优化,如逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除。

JVM 实际采用解释器和JIT混用模式 Java HotSpot(TM) 64-Bit Server VM (build 13+33, mixed mode, sharing)

2 javac 编译过程源码分析

从 Sun Javac 的源码来看,编译main方法位于

 

编译过程大致可以分为3个过程

分别对应Token流,语法树,注解语法树,字节码输出。

2.1 Parse and Enter

两个重要的接口与实现

接口作用实现com.sun.tools.javac.parser.Lexer词法分析com.sun.tools.javac.parser.JavacParsercom.sun.tools.javac.parser.Parser构建抽象语法树com.sun.tools.javac.parser.Scanner

该阶段将源码文件解析构建抽象语法树( Abstract Syntax Tree,AST )。从功能上分为 词法分析 和 语法分析,实际上同时进行。编译时,通过 ParserFactory 与 ScannerFactory 工厂类管理 JavacParser 与 Scanner 对象。JavacParser 解析时,Scanner 读取源文件字符流,逐个读入 Token,构建抽象语法树。之后,编译器就基本不会再对源码文件进行操作,后续操作都建立在抽象语法树之上。(添加默认无参构造方法等)

 

2.1.1 词法分析

将Java源代码按照Java关键字、自定义关键字、符号等按顺序分解为了可识别的Token流。

输入输出描述源代码的字符流标记(Token)集合关键字、变量名、字面量、运算符

字符char是程序编写过程中的的最小元素,标记Token是编译过程的最小元素。

主要实现类功能com.sun.tools.javac.parser.JavacParser规定哪些词符合Java语言规范,具体读取和归类不同词法的操作由scanner完成。com.sun.tools.javac.parser.Scanner负责逐个读取源代码的单个字符,然后解析符合Java语言规范的Token序列,调用一次nextToken()都构造一个Tokencom.sun.tools.javac.parser.Tokens$Token规定了所有Java语言的合法关键词,包含了开始/结束位置,类型。com.sun.tools.javac.parser.Tokens$TokenKind描述一个Token的类型,如IDENTIFIER(自定义标识)、BOOLEAN、BREAK、BYTE、CASE。com.sun.tools.javac.util.Names用来存储和表示解析后的词法,每个字符集合都会是一个Name对象,所有的对象都存储在Name.Table内部类中。com.sun.tools.javac.parser.KeyWords *负责将字符集合对应到token集合中。JDK9后由Tokens完成

🗣 Tips :

  1. java命名规范指出声明变量的时候必须以字母、下划线或者美元符开头,包括字母、数字、下划线或者美元符。由JavacParser规定并识别 int y=x+1; package per.rsf.javac; 的Token流。
  2. Tokens根据Token.name先转化成Name对象,建立Name和Token的对应关系,保存在key数组中。这个key数组只保存了在Token类中定义的所有Token到Name对象的关系,而其他所有字符集合Tokens都会将它对应到TokenKind.IDENTIFIER类型
  3. Javac中每个与文件相关的实现类都直接或间接实现了JavaFileObject接口,这个接口专门为操作.java文件及.class文件而定义的。每个RegularFileObject类对象可以代表一个Java源文件。调用getCharContent()方法获取字符流输入。
  4. 关键代码nextToken的主要逻辑:处理特殊字符、标识符、16进制、数字、分隔符、斜杠开头、反斜杠开头、双引号开头、默认处理。

例:package的Token读取流程

2.1.2 语法分析

输入输出描述标记(Token)集合抽象语法树(AST)包、类型、运算符、修饰符、接口、返回值、代码注释

将Token流组装成更结构化的语法树,描述程序代码语法结构,检查是否符合Java语言规范。每个语法树上的节点都是的一个实例,继承自接口,代表着程序代码中的一个语法结构(Construct),如包、类型、修饰符、运算符、接口、返回值甚至代码注释。

实现类功能com.sun.tools.javac.tree.TreeMaker生成语法节点,根据Name对象构建一个语法节点com.sun.tools.javac.tree.JCTree生成的语法节点都会继承jctree和实现(如根节点JCCompilationUnit)com.sun.tools.javac.tree.JCTree#Tagenum类,区分语法树的类型。类型的数值是上一个节点类型的数值+1com.sun.tools.javac.tree.JCTree#pos语法节点在源文件中的起始位置,-1表示不存在com.sun.tools.javac.tree.JCTree#typeJava类型(int、float、String)

🗣 Tips :

  1. JCCompilationUnit表示一个编译单元,一般是一个源文件(可以是多个类)内容对应一个编译单元,同时这也是顶层的树节点。包含包注解 List、包名 JCExpression、和树 List。
  2. 在遍历像抽象语法树这样由各种类的实例所组成的树形结构时,通常会借助Visitors访问者模式来完成。

2.1.3 填充符号表

该步骤实际发生在语义分析中,在处理注解前。

一个类中的符号变量,除了类本身定义,其他类定义。如调用其他类方法、变量,继承或实现父类和接口等。

调用其他类的符号变量时,就需要通过符号表来进行查找。(符号引用)

这些类的符号也需解析到符号表中。按照递归向下的顺序解析语法树,

  1. 将所有类中出现的符号输入到自身的符号表,并将类符号、类的参数类型符号(泛型参数类型)、超类符号,继承类型符号和继承的接口类型符号都存储到一个未处理列表中。
  2. 将这个未处理列表中的所有类都解析到各自的符号列表中。

输入输出描述当前范围的定义域(definitions)待处理列表,包含需要分析并生成类文件的树.一组符号地址和符号信息构成的表格

符号表是由一组符号地址和符号信息构成的表格。在语义分析中,符号表所登记的内容将用于语义检查和产生中间代码。在目标生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。

实现类功能com.sun.tools.javac.comp.Enter内容填充,符号表(Symbol Table)填充的出口是一个待处理列表,包含了每一个编译单元的抽象语法树的顶级节点以及 package-info.java 的顶级节点。com.sun.tools.javac.comp.MemberEnter使类变得完整,确定类的泛型参数、父类、接口,该类的所有符号输入到它所对应的scopeVarSymbol预定义符号的输入,对操作符的处理

2.2 Annotation Processing

JDK 6 实现了插入式注解处理API(JSR-269),位于和包。

通过声明一个注解,实现一个注解处理器。注册服务后

在编译期由处理注解。读取、修改、添加抽象语法树中的任意元素。像反射一样访问类、字段、方法和注解等元素,创建新的源文件。每一个插入式注解处理器操作语法树,编译器将回到解析及填充符号表的过程循环。

作用:减少编写配置文件的劳动量,提高代码可读性。

测试使用时,测试类和实现类写在不同子模块下!!!否则编译不通过!!!一般需要插件工具完成,如 Lombok 插件

2.2.1 javac 命令编译过程

  1. 声明一个注解
  2. 创建一个注解处理器,注解处理器需实现  接口或继承  类。重写方法。
  3. 为注解处理器注册服务。在文件夹下创建 文件。写入注解处理器的全称。
  4. 为了能够让项目能够通过编译,我们需要为Java编译器添加一个不进行注解处理的参数
 

也可使用 指定注解处理器

主要源码:

 
元素含义Element程序元素(源代码)VariableElement代表一个字段,枚举常量,方法或者构造方法的参数,局部变量及异常参数等元素PackageElement包元素TypeElement类或接口元素ExecutableElement代码方法,构造函数,类或接口的初始化代码块等元素,也包括注解类型元素TypeMirror声明类型(类类型和接口类型),数组,类型变量和空类型。也代表通配类型参数,可执行文件的签名和返回类型等。TypeMirror = Element.asType()DeclaredType声明类型:类类型还是接口类型

2.2.2 编译器 API 实例过程

使用  的  方法可以传入注解处理器。

 

2.2.3 实例

仿照findbugs实现一个简单的类编写规范检查

注解类 CHECK
 
注解处理类 CLASSCHECKER
 
注册服务

在文件夹下创建文件,内容如下:

 
测试类
 
结果

将项目打成jar包使用

 

🗣 Tips :

  1. 模拟Lombok实现get set方法,依赖  包

2.3 Analyse and Generate

2.3.1 语义分析

输入输出描述语法树字节码文件标注检查,数据及控制流分析

语法分析将源文件抽象成结构正确的抽象语法树,但无法保证符合逻辑的。还需添加默认的构造器,检查变量使用前是否已经初始化等。

实现类功能com.sun.tools.javac.comp.Attr标注检查:名称消解、变量使用声明、类型检查、常量折叠、推导泛型方法的参数类型com.sun.tools.javac.comp.Flow数据及控制流分析:局部变量在使用前是否被正确赋值、final变量不被重复修饰、确定方法返回值类型、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理com.sun.tools.javac.comp.Check用来辅助Attr类检查语法树中变量类型是否正确,如二元操作符两边的操作数的类型是否匹配,方法返回值是否和接收的引用值类型匹配com.sun.tools.javac.comp.Resolve检查变量,方法或者类的访问是否合法,变量是否是静态变量com.sun.tools.javac.comp.ConstFold将一个字符串常量中的多个字符合并成一个字符串com.sun.tools.javac.comp.Infer帮助推导泛型方法的参数类型

🗣 Tips :

  1. 常量折叠:a=1+2 -> a=3
  2. 如果代码中没有提供任何构造函数,自动添加一个没有参数、访问权限与当前类一致的默认构造函数。如果提供了构造函数,则在代码生成阶段添加。
语法糖(SYNTACTIC SUGAR)

Java中最常用的 语法糖 主要是前面提到过的泛型、变长参数、自动装箱/拆箱等。JVM运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。解语法糖的过程由JavaCompiler#desugar()方法触发。

实现类功能com.sun.tools.javac.comp.TransTypesGeneric Java to conventional Javacom.sun.tools.javac.comp.Lowerinner classes, class literals, assertions, foreach loops, etc.

2.3.2 字节码生成

输入输出描述语法树、符号表字节码文件编译器添加和转换少量的代码,生成字节码文件。

把前面各个步骤所生成的信息转化成字节码,

  1. 代码收敛,将方法块转成符合JVM语法的命令形式,jvm的所有操作都是基于栈的,所有操作都必须经过进出栈来完成。
  2. 按照jvm的文件组织格式将字节码输出到以class文扩展名的文件中。

在收敛代码的过程中,将实例构造器方法和类重载构造器方法添加到语法树之中。

实现类功能com.sun.tools.javac.jvm.Gen遍历语法树,生成JVM操作码序列结合om.sun.tools.javac.jvm.Items表示任何可寻址的操作项,这些操作项都可以作为一个单位出现在操作栈上,不同的Item对应不同JVM操作码com.sun.tools.javac.jvm.Code存储生成的字节码,并提供一些能够映射操作码的方法com.sun.tools.javac.jvm.ClassWriter输出字节码,生成最终的Class文件

方法编译器自动收集static代码块,收敛顺序为先父后子,先单后块,父接口“随遇而安”。final static 静态不可变常量提前被编译器放入常量池,无需初始化。

方法编译器自动收集非static代码块,收敛顺序为先父后子,先单后块最后构造函数

方法(实例化阶段)永远在方法(类初始化阶段)执行后执行。🗣 Tips :

  1. 类构造器,在jvm进行类加载—验证—解析—初始化中的初始化阶段(类实例化 调用静态字段或方法)调用,对静态变量,静态代码块进行初始化。多线程时,方法阻塞,一个类只会在一个JVM进程运行期间执行一次方法。
  2. 实例构造器,类实例化阶段(new 反射 克隆 反序列化),对非静态变量解析初始化。若实例时类没有初始化,先执行方法。
 

3 javac API


JDK 6 增加了规范 JSR-199 和 JSR-296,提供相关的 API

绿色标注的包是官方 API(Official API), JSR-199 和 JSR-296.

黄色标注的包为(Supported API).

紫色标注的包代码全部在  包下,为内部 API(Internal API)和编译器的实现类。

类名注释javax.annotation.processing注解处理 (JSR-296)javax.lang.model注解处理和编译器 Tree API 使用的语言模型 (JSR-296)javax.lang.model.element语言元素javax.lang.model.type类型javax.lang.model.util语言模型工具javax.toolsJava 编译器 API (JSR-199)com.sun.source.*编译器 Tree API,提供 javac 工具使用的抽象语法树 AST 的 只读访问com.sun.tools.javac.*内部 API 和编译器的实现类

4 javac 调试

4.1 修改 idea.vmoptions

 

4.2 配置远程debug

4.3 启用idea编译调试

IDEA默认会禁用编译调试,这里需要开启一个开关,以此让IDEA在编译之前等待调试程序的链接。并且,这个配置在IDEA重启后会失效

双击,打开平常搜索类的界面,输入

小讯
上一篇 2024-12-29 14:58
下一篇 2024-12-23 21:24

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/10038.html