Java 编译与运行过程
理解 Java 的编译和运行过程,有助于你更好地理解 Java 的工作原理,以及如何解决编译和运行中的问题。本章将详细介绍从源代码到程序执行的完整过程。
Java 源码 → 字节码 → JVM 执行
完整流程
Java 源代码 (.java)
↓
javac 编译器
↓
字节码 (.class)
↓
JVM(Java 虚拟机)
↓
机器码(JIT 编译)
↓
CPU 执行
↓
程序运行结果
详细步骤
1. 编写源代码
创建 .java 文件,编写 Java 代码:
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
2. 编译(Compilation)
使用 javac 编译器将源代码编译为字节码:
javac HelloWorld.java
编译过程:
- 词法分析:将源代码分解为标记(tokens)
- 语法分析:检查语法是否正确
- 语义分析:检查类型、作用域等
- 代码生成:生成字节码文件
结果:生成 HelloWorld.class 文件(字节码)
3. 加载(Loading)
JVM 加载 .class 文件:
- 类加载器:读取字节码文件
- 验证:检查字节码的正确性
- 准备:为类变量分配内存
- 解析:将符号引用转换为直接引用
4. 执行(Execution)
JVM 执行字节码:
- 解释执行:逐条解释字节码指令
- JIT 编译:将热点代码编译为机器码
- 运行:CPU 执行机器码
javac 与 java 命令使用
javac 命令
作用:编译 Java 源代码为字节码
基本用法
# 编译单个文件
javac HelloWorld.java
# 编译多个文件
javac File1.java File2.java File3.java
# 编译目录下所有文件
javac *.java
# 编译带包的文件
javac com/example/HelloWorld.java
常用选项
# -d:指定输出目录
javac -d out src/HelloWorld.java
# -cp 或 -classpath:指定类路径
javac -cp lib/*.jar src/HelloWorld.java
# -encoding:指定源文件编码
javac -encoding UTF-8 HelloWorld.java
# -source:指定源代码版本
javac -source 17 HelloWorld.java
# -target:指定目标字节码版本
javac -target 17 HelloWorld.java
# -verbose:显示详细信息
javac -verbose HelloWorld.java
编译示例
# 示例 1:基本编译
javac HelloWorld.java
# 生成:HelloWorld.class
# 示例 2:指定输出目录
javac -d classes src/HelloWorld.java
# 生成:classes/HelloWorld.class
# 示例 3:编译多个文件
javac -d out src/**/*.java
# 编译 src 目录下所有 .java 文件到 out 目录
java 命令
作用:运行编译后的 Java 程序
基本用法
# 运行类(无包)
java HelloWorld
# 运行带包的类
java com.example.HelloWorld
# 传递参数
java HelloWorld arg1 arg2
常用选项
# -cp 或 -classpath:指定类路径
java -cp .:lib/* com.example.HelloWorld
# -Xmx:设置最大堆内存
java -Xmx512m HelloWorld
# -Xms:设置初始堆内存
java -Xms256m HelloWorld
# -version:显示版本信息
java -version
# -verbose:显示详细信息
java -verbose:class HelloWorld
# -D:设置系统属性
java -Dkey=value HelloWorld
运行示例
# 示例 1:基本运行
java HelloWorld
# 输出:Hello, World!
# 示例 2:传递参数
java HelloWorld hello world
# args[0] = "hello", args[1] = "world"
# 示例 3:指定类路径
java -cp .:lib/*:out com.example.HelloWorld
类路径(classpath)概念
什么是类路径
类路径(classpath) 是 JVM 查找类文件的路径。当 JVM 需要加载一个类时,会在类路径中搜索对应的 .class 文件。
类路径的组成
类路径可以包含:
- 目录:包含
.class文件的目录 - JAR 文件:Java 归档文件
- ZIP 文件:压缩文件
设置类路径
方法 1:使用 -cp 选项
# 单个路径
java -cp /path/to/classes HelloWorld
# 多个路径(Windows 用分号,Linux/macOS 用冒号)
java -cp .:lib/*:out HelloWorld
# Windows 示例
java -cp .;lib\*;out HelloWorld
方法 2:使用环境变量
Windows:
set CLASSPATH=.;lib\*;out
java HelloWorld
Linux/macOS:
export CLASSPATH=.:lib/*:out
java HelloWorld
方法 3:使用清单文件(MANIFEST.MF)
在 JAR 文件中设置:
Manifest-Version: 1.0
Class-Path: lib/dependency1.jar lib/dependency2.jar
Main-Class: com.example.HelloWorld
类路径的查找顺序
JVM 按以下顺序查找类:
- Bootstrap 类路径:JVM 核心类(rt.jar 等)
- Extension 类路径:扩展类
- 用户类路径:通过 -cp 或 CLASSPATH 指定的路径
类路径示例
示例 1:单文件程序
project/
└── HelloWorld.java
javac HelloWorld.java
java HelloWorld
# 类路径:当前目录(.)
示例 2:带包的程序
project/
└── src/
└── com/
└── example/
└── HelloWorld.java
# 编译
javac -d out src/com/example/HelloWorld.java
# 运行(从 out 目录运行)
cd out
java com.example.HelloWorld
# 或指定类路径
java -cp out com.example.HelloWorld
示例 3:使用外部库
project/
├── src/
│ └── Main.java
├── lib/
│ ├── library1.jar
│ └── library2.jar
└── out/
└── Main.class
# 编译
javac -cp lib/* -d out src/Main.java
# 运行
java -cp out:lib/* Main
常见编译与运行问题
编译问题
问题 1:找不到符号(Cannot find symbol)
错误信息:
error: cannot find symbol
symbol: variable x
location: class HelloWorld
原因:
- 变量未声明
- 类未导入
- 拼写错误
解决:
// ❌ 错误
public class Test {
public static void main(String[] args) {
System.out.println(x); // x 未声明
}
}
// ✅ 正确
public class Test {
public static void main(String[] args) {
int x = 10; // 先声明
System.out.println(x);
}
}
问题 2:类找不到(Class not found)
错误信息:
error: cannot find symbol
symbol: class Scanner
location: class HelloWorld
原因:未导入类
解决:
// ❌ 错误
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
}
}
// ✅ 正确
import java.util.Scanner; // 添加导入
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
}
}
问题 3:编码问题
错误信息:
error: unmappable character for encoding ASCII
原因:源文件编码与编译器编码不匹配
解决:
# 指定编码编译
javac -encoding UTF-8 HelloWorld.java
运行问题
问题 1:找不到或无法加载主类
错误信息:
Error: Could not find or load main class HelloWorld
原因:
- 类路径不正确
- 类名拼写错误
- 包名问题
解决:
# ❌ 错误:包含 .class
java HelloWorld.class
# ✅ 正确:不包含扩展名
java HelloWorld
# ❌ 错误:包名问题
java HelloWorld # 如果类在包中
# ✅ 正确:使用完全限定名
java com.example.HelloWorld
问题 2:类路径问题
错误信息:
Exception in thread "main" java.lang.NoClassDefFoundError: com/example/HelloWorld
原因:类路径设置不正确
解决:
# 检查类文件是否存在
ls -R out/
# 使用正确的类路径运行
java -cp out com.example.HelloWorld
# 或进入正确的目录
cd out
java com.example.HelloWorld
问题 3:主方法找不到
错误信息:
Error: Main method not found in class HelloWorld
原因:
- main 方法签名错误
- main 方法不存在
解决:
// ❌ 错误:缺少 static
public void main(String[] args) { }
// ✅ 正确:完整的 main 方法
public static void main(String[] args) { }
调试技巧
1. 检查文件结构
# 查看文件结构
tree
# 或
find . -name "*.java" -o -name "*.class"
2. 使用 -verbose 选项
# 编译时显示详细信息
javac -verbose HelloWorld.java
# 运行时显示类加载信息
java -verbose:class HelloWorld
3. 检查类文件
# 使用 javap 反编译查看类信 息
javap HelloWorld
javap -c HelloWorld # 显示字节码
实际示例
示例 1:单文件程序
// Calculator.java
public class Calculator {
public static void main(String[] args) {
int a = 10;
int b = 20;
int sum = a + b;
System.out.println("和:" + sum);
}
}
编译和运行:
javac Calculator.java
java Calculator
# 输出:和:30
示例 2:多文件程序
文件 1:Student.java
package com.example;
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
public void introduce() {
System.out.println("我是 " + name);
}
}
文件 2:Main.java
package com.example;
public class Main {
public static void main(String[] args) {
Student student = new Student("张三");
student.introduce();
}
}
编译和运行:
# 编译(需要编译所有依赖的类)
javac com/example/*.java
# 或分别编译
javac com/example/Student.java
javac com/example/Main.java
# 运行
java com.example.Main
# 输出:我是 张三