主题
界面
主题
S1 的第一节。开始写语法之前,先搞清一件更重要的事:你写的代码是怎么变成程序跑起来的。
《Java 程序的运行与调试》 提到过"编译期 / 加载期 / 运行期",本节把它讲透。理解了这条链路,后面的类加载、JVM 内存、性能优化才有根基——否则它们全是背下来的名词。
完成本节后,你应该能够:
.java 到程序运行的完整链路,并指出每一步的产物;javap 查看字节码,看懂最基础的几条指令;public static void main(String[] args) 每个修饰符的作用;ClassNotFoundException 与 NoClassDefFoundError 出现的位置。javac 与 java 命令编译运行单文件吗?(《JDK 与 Java 版本》)初学者常有的困惑,答案都在本节:
javac 编译出来的 .class 是什么?和 .java 什么关系?"UnsupportedClassVersionError?"main 方法为什么必须写成那个样子?少一个 static 会怎样?"这些不是 "八股",而是决定你能不能独立排查环境与部署问题的常识。
常见说法是"Java 程序一次编写,到处运行",但更准确的说法是:
Java 源码 → 编译成字节码 → JVM 负责翻译成机器指令
(平台无关) (平台无关) (平台相关,每个系统一套)真正跨平台的是"字节码",不是 JVM。 同一份 .class 文件,在 Windows 的 JVM 和 Linux 的 JVM 上都能执行——因为 JVM 屏蔽了操作系统差异(内存管理、线程调度、文件系统、网络)。
由此推出一个重要结论:只要 JVM 版本不同,行为就可能不同。 这也是为什么本知识库把"编译目标"固定在 Java 21,并用 --release 21 约束。
① 编译 源码 .java ──javac──▶ 字节码 .class
② 加载 类加载器把 .class 读入内存,生成 Class 对象
③ 链接与初始化 验证 · 准备(静态变量分配默认值)· 解析 · 执行静态初始化块
④ 执行 JVM 解释执行字节码,热点代码由 JIT 编译成机器码
⑤ 回收 不再使用的对象由垃圾回收器回收(S6 会深入)对应到常见错误:
| 阶段 | 典型错误 | 含义 |
|---|---|---|
| ① 编译 | 找不到符号、类型不匹配 | 源码问题,编译器能精确定位 |
| ② 加载 | ClassNotFoundException | 运行时主动用 Class.forName 等加载器找类,没找到 |
| ② 加载 | NoClassDefFoundError | JVM 被动要用的类没找到(常因编译时有、运行时缺依赖) |
| ③ 初始化 | ExceptionInInitializerError | 静态初始化块抛了异常 |
| ④ 执行 | 各种运行时异常 | 逻辑问题,配合 《调试》 定位 |
用 javap -c 反编译可以看到 JVM 指令(下面取自本节的实验代码):
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String java.version
5: invokestatic #15 // Method java/lang/System.getProperty:(Ljava/lang/String;)Ljava/lang/String;
8: invokedynamic #19 // InvokeDynamic #0:makeConcatWithConstants:(...)Ljava/lang/String;
13: invokevirtual #22 // Method java/io/PrintStream.println:(Ljava/lang/String;)V逐行读:
getstatic:取一个静态字段(这里是 System.out);ldc:把常量池里的常量压入栈(这里是字符串 "java.version");invokestatic / invokevirtual:调用静态方法与实例方法;invokedynamic:Java 9 之后字符串拼接 "a" + b 的编译结果(Java 8 用的是 StringBuilder)。不需要背指令,只要建立"源码 → 一串栈式指令"的认知即可。这个认知在 S6 讲字节码与 Java Agent 时会派上大用场。
Bootstrap ClassLoader 加载 JDK 核心类(java.lang.*)
↑
Platform ClassLoader 加载 JDK 扩展模块
↑
App ClassLoader 加载你自己写的类与依赖 jar"双亲委派"(子加载器先交给父加载器尝试)保证了核心类不会被自定义类覆盖——这也是为什么你不能自己写一个 java.lang.String 去替换它。
public static void main(String[] args) { }| 部分 | 含义 | 少了它会怎样 |
|---|---|---|
public | JVM 需要从外部调用它 | 找不到主类/方法 |
static | JVM 启动时还没有对象,必须能直接调用 | 需要先 new 才能用,JVM 做不到 |
void | 执行完不返回给 JVM 任何值 | 签名不匹配,无法作为入口 |
main | 约定的方法名 | 找不到入口 |
String[] args | 接收命令行参数 | 签名不匹配 |
注意:String[] args 与 String args[] 等价,但前者是推荐写法;String... args 也可作为入口。
javac --release 21 -d target/classes RuntimeMechanism.java
java -cp target/classes cn.kb.learn.syntax.RuntimeMechanismJava 版本: 25.0.2
JVM 名称: OpenJDK 64-Bit Server VM
字节码由 JVM 执行,因此换操作系统不必重新编译
本类的加载器: AppClassLoader
启动参数个数: 0注意两处细节:
java -cp ... 类全名:运行时要写全限定类名(含包名),不是文件名;本类的加载器: AppClassLoader:说明这是你自己写的类,由应用类加载器加载。java -cp target/classes cn.kb.learn.syntax.RuntimeMechanism hello world输出变成 启动参数个数: 2。args 就是命令行参数的数组——这是写命令行工具的基础(《数组》 会用到)。
.class 当成万能文件 Exception in thread "main" java.lang.UnsupportedClassVersionError:
cn/kb/learn/syntax/RuntimeMechanism has been compiled by a more recent
version of the Java Runtime (class file version 69.0), this version of
the Java Runtime only recognizes class file versions up to 65.0.class 是平台无关的,但不是版本无关的:69 对应 Java 25,65 对应 Java 21。高版本编译的类无法在低版本 JVM 上运行。
修复方式就是本知识库一直在用的:--release 21 或统一运行环境。
文件:labs/java-syntax/src/main/java/cn/kb/learn/syntax/RuntimeMechanism.java
package cn.kb.learn.syntax;
public class RuntimeMechanism {
public static void main(String[] args) {
System.out.println("Java 版本: " + System.getProperty("java.version"));
System.out.println("JVM 名称: " + System.getProperty("java.vm.name"));
System.out.println("字节码由 JVM 执行,因此换操作系统不必重新编译");
System.out.println("本类的加载器: " + RuntimeMechanism.class.getClassLoader().getClass().getSimpleName());
System.out.println("启动参数个数: " + args.length);
}
}运行命令:
cd labs
./mvnw -pl java-syntax compile
java -cp java-syntax/target/classes cn.kb.learn.syntax.RuntimeMechanism
# 查看字节码
javap -c -cp java-syntax/target/classes cn.kb.learn.syntax.RuntimeMechanism预期输出:
Java 版本: 25.0.2
JVM 名称: OpenJDK 64-Bit Server VM
字节码由 JVM 执行,因此换操作系统不必重新编译
本类的加载器: AppClassLoader
启动参数个数: 0javap -c ...,找到 main 方法,对照源码数一数:五行 println 对应五组 getstatic → ldc → invokevirtual 的指令模式;getSimpleName() 去掉,观察输出变成 jdk.internal.loader.ClassLoaders$AppClassLoader@...——这证明"类"在运行时是内存里的一个对象;java -cp ... RuntimeMechanism 参数1 参数2 运行,在 args.length 处打断点,观察数组内容;static { ... })里打印一行,会发现它只在首次使用该类时执行一次,而不是程序启动时。ClassNotFoundException 与 NoClassDefFoundError 分不清 ClassNotFoundException:你主动让 JVM 去找一个类(Class.forName("...")),字符串写错了;NoClassDefFoundError:JVM 被动要用的类在 classpath 里找不到,通常是依赖没打进去或版本冲突。排查后者用 ./mvnw -pl <模块> dependency:tree(《Maven 基础》)。
main 方法签名写错 public void main(String[] args) // 少了 static
public static void main(String args) // 参数类型错了
private static void main(String[] args) // 可见性不对这三种都会让程序报"找不到主类"。IDE 的补全能避免 90% 的签名错误:输入 psvm 回车。
java RuntimeMechanism # 错:少了包名
java -cp target/classes cn.kb.learn.syntax.RuntimeMechanism # 对修复:cn.kb.learn.syntax.RuntimeMechanism 的 .class 文件必须位于 target/classes/cn/kb/learn/syntax/ 下——包名与目录结构必须一致。
目标:亲手走一遍"源码 → 字节码 → 运行"的链路。
步骤:
labs/java-syntax 下新增一个类 MyFirst.java,输出三行内容;target/classes/cn/kb/learn/syntax/MyFirst.class 存在;javap -c 查看它的字节码,找出 main 方法对应的指令;args.length 的变化;java -cp java-syntax/target/classes MyFirst 运行(故意不带包名),记录报错并解释原因。验收方式:能解释第 5 步报错里的"找不到或无法加载主类"与包名的关系。
扩展任务:删除 target/classes 下的 .class 文件后重新运行,观察报错——这模拟了"部署时漏打包"的线上事故。
基础题
main 方法为什么必须是 static 的?代码阅读题
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String java.version
5: invokestatic #15 // Method java/lang/System.getProperty:(Ljava/lang/String;)Ljava/lang/String;
8: invokedynamic #19 // InvokeDynamic ...invokedynamic 在处理什么?编程题
SystemInfo,输出:Java 版本、Java 供应商、JVM 名称、操作系统名称与架构。要求用命令行参数控制是否输出详细信息(传入 -v 时输出全部,否则只输出 Java 版本)。综合任务
javap -v(注意是 -v,输出更详细)查看 RuntimeMechanism 的类文件结构,找到: major version 是多少?它对应哪个 Java 版本?major version 会变成多少?提示 1(方向):第 1 题的关键是分清"谁跨平台"——字节码与 JVM 的角色不同。
提示 2(关键点):第 5 题的参数判断用 args.length > 0 && "-v".equals(args[0]),注意把常量写在前面避免空指针(《读懂 Java 错误信息》 讲过这个技巧)。
提示 3(接近答案):第 6 题的 major version 与 Java 版本的关系是:major version = Java 主版本 + 44(Java 8 → 52,Java 21 → 65,Java 25 → 69)。
static 的意义。对应源码:
System.out.println("Java 版本: " + System.getProperty("java.version"));逐步解释:
getstatic 取 System.out(打印流对象);ldc 压入常量 "java.version";invokestatic 调用 System.getProperty 拿版本号;invokedynamic 执行字符串拼接——Java 9+ 用 makeConcatWithConstants 取代了 Java 8 的 StringBuilder 方案,运行时由 JVM 生成拼接逻辑,性能更好;invokevirtual 调用 println 输出。package cn.kb.learn.syntax;
public class SystemInfo {
public static void main(String[] args) {
boolean verbose = args.length > 0 && "-v".equals(args[0]);
System.out.println("Java 版本: " + System.getProperty("java.version"));
if (verbose) {
System.out.println("Java 供应商: " + System.getProperty("java.vendor"));
System.out.println("JVM 名称: " + System.getProperty("java.vm.name"));
System.out.println("操作系统: " + System.getProperty("os.name"));
System.out.println("架构: " + System.getProperty("os.arch"));
}
}
}运行:
java -cp java-syntax/target/classes cn.kb.learn.syntax.SystemInfo
java -cp java-syntax/target/classes cn.kb.learn.syntax.SystemInfo -vjavap -v 输出的开头会包含:
public class cn.kb.learn.syntax.RuntimeMechanism
minor version: 0
major version: 69
flags: (0x0021) ACC_PUBLIC, ACC_SUPERmajor version: 69 对应 Java 25(本机 JDK 编译,未指定 release 时会用本机版本);--release 21 编译,此时 major version 会是 65;major version = Java 主版本 + 44。Java 8 → 52、Java 11 → 55、Java 17 → 61、Java 21 → 65。命令:
javap -v -cp java-syntax/target/classes cn.kb.learn.syntax.RuntimeMechanism | Select-String "major version|minor version"javap -c 看懂最基础的几条指令;main 方法的签名。invokedynamic 字符串拼接的由来《变量与数据类型》:从最基本的"存一个数据"开始写 Java。