概览(一句话)
把 .java 源文件变成 .class 字节码文件的过程,主要包括:词法/语法分析 → 符号表建立 → 注解处理(可生成新源) → 类型检查与数据流分析 → 语法糖转换/泛型擦除 → 字节码生成 → 写出 class 文件(可再打包为 jar)。
详细编译阶段(按执行顺序)
下面按典型 javac(OpenJDK)内部流程把各阶段讲清楚:
-
源文件输入(.java)
-
编译器从磁盘读取
.java文件,按编码解析字符(可通过-encoding指定)。
-
-
词法分析(Lexical analysis / Tokenize)
-
把字符流拆成 token(关键字、标识符、字面量、符号等)。
-
-
语法解析(Parsing) → 抽象语法树(AST)
-
根据 Java 语法把 token 解析成 AST(编译单元、类/方法/表达式的树形结构)。
-
-
Enter(符号表收集)
-
收集声明(类、接口、字段、方法、包等),建立符号表(symbol table)。此阶段主要识别“名字在哪儿声明”。
-
-
注解处理(Annotation Processing)
-
编译器调用注册的注解处理器(javax.annotation.processing)——比如 Lombok、编译期生成代码的处理器。
-
注解处理可以生成新的源文件或其它资源,生成后会触发新的编译轮次(处理器可能被执行多轮直到没有新文件生成)。
-
可通过
-processor、-proc:only等控制。
-
-
类型属性与语义分析(Attribution / Type checking)
-
名称解析(将变量/方法名绑定到符号表中的具体声明)、重载解析、类型推断、泛型边界检查等。
-
检查方法签名、可访问性(public/private)、接口实现是否完整等。
-
-
数据流分析(Flow analysis)
-
包括“definite assignment”(确定性赋值)检查、不可达代码检测、局部变量的初始化性检查等。
-
-
泛型擦除 & 桥方法(TransTypes)
-
Java 的泛型在编译时进行 类型擦除(type erasure):泛型信息不会保留在运行时(大多数场景)。
-
编译器为保持运行时多态,可能生成 bridge methods(桥方法)(例如子类泛型导致方法签名不同,需要桥方法来保持父子兼容)。
-
-
语法糖转换 / 降低(Desugaring / Lowering)
-
把高级语言结构转换为 JVM 能理解的形式,例如:
-
内部类 / 匿名类 → 生成
Outer$Inner.class或Outer$1.class等,并可能添加 synthetic 字段/方法以访问外部变量。 -
lambda 表达式(Java 8+) → 主要通过
invokedynamic+引导方法(bootstrap)实现(编译期生成 invokedynamic 指令与必要的“目标方法”引用;不一定每次都生成独立类)。 -
枚举(enum) → 编译成类,静态 final 实例和
values()/valueOf()方法等。 -
switch on String → 编译器将字符串转换为
hashCode()+ equals 的处理逻辑或辅助表。 -
assert → 转换成条件判断语句(如果开启断言)。
-
-
-
字节码生成(Code generation)
-
根据处理过的 AST 生成 JVM 字节码(方法的字节码、常量池、字段、属性等)。
-
同时生成 class file 的各种 attributes(
LineNumberTable、LocalVariableTable、SourceFile等,取决于-g等选项)。
-
-
写出 .class 文件
-
把生成的字节码与常量池、类文件头(magic number、版本号)等写入磁盘
.class文件。
-
-
后处理 / 打包(可选)
-
使用
jar打包、使用字节码工具做混淆(ProGuard/Obfuscator)、AOT 编译或 native-image 等(这些属于构建或运行时工具链外的步骤)。
-
类文件(.class)结构(简要)
.class 文件主要包含:
-
magic(0xCAFEBABE)、minor/major version(类文件版本,与 Java 版本对应)
-
Constant Pool(常量池)(字符串、类名、方法引用、字面量等)
-
访问标志(public/abstract/final)
-
this_class / super_class / interfaces
-
fields(字段表)
-
methods(方法表:包含字节码)
-
attributes(类级属性,比如 SourceFile,RuntimeVisibleAnnotations 等)
可以用 javap -v ClassName 查看这些信息(常用于排查编译后产生的字节码/常量池内容)。
一些关键细节(面试/排错常用点)
-
泛型是编译时检查,运行时类型擦除:
List<String>与List<Integer>在字节码层面是相同的。 -
桥方法:为保持泛型/协变重写的二义性/兼容性,编译器会自动生成桥方法。
-
lambda:不再总是生成匿名内部类,而是采用
invokedynamic(减少类数量、提高性能)。 -
注解处理器:可以生成新源码或资源,务必注意生成的类的包名、类名不冲突,且可能需要多轮编译。
-
内部类 & 匿名类:会产生额外的
.class文件(带$符号),注意在打包/反射时路径变化。 -
调试信息:
-g(默认一般会生成行号/本地变量等),-parameters可把方法参数名保留到 class 文件(用于反射获得真实参数名)。 -
跨版本编译:使用
--release(推荐)或-source/-target来控制生成的字节码兼容性;--release还能为目标平台选择正确的核心库签名。
常见编译命令:
# 编译单个文件并指定输出目录
javac -d out -encoding UTF-8 src/com/example/Main.java
# 指定 classpath 并编译整个源码目录
javac -d out -classpath "lib/*" $(find src -name "*.java")
# 用 --release 指定目标 Java 版本(推荐用于跨版本编译)
javac --release 11 -d out src/com/example/*.java
# 仅运行注解处理器(不生成 class)
javac -proc:only -processor com.example.YourProcessor src/...
# 生成带参数名的 class(反射可见)
javac -parameters src/...
#打包
jar cvf app.jar -C out .
javap -c -p com.example.Main # 查看反汇编字节码
javap -v com.example.Main # 查看详细(含常量池)
1085



被折叠的 条评论
为什么被折叠?



