Skip to content

Java 语言与运行机制 ​

S1-031必学入门约 60 分钟JDK 21编程语言基础 · 语法基础

1. 本节定位 ​

S1 的第一节。开始写语法之前,先搞清一件更重要的事:你写的代码是怎么变成程序跑起来的。

《Java 程序的运行与调试》 提到过"编译期 / 加载期 / 运行期",本节把它讲透。理解了这条链路,后面的类加载、JVM 内存、性能优化才有根基——否则它们全是背下来的名词。

2. 学习目标 ​

完成本节后,你应该能够:

  • 用自己的话说清"Java 跨平台"到底跨的是什么;
  • 说出从 .java 到程序运行的完整链路,并指出每一步的产物;
  • 用 javap 查看字节码,看懂最基础的几条指令;
  • 逐字解释 public static void main(String[] args) 每个修饰符的作用;
  • 区分 ClassNotFoundException 与 NoClassDefFoundError 出现的位置。

3. 前置知识检查 ​

  • 能用 javac 与 java 命令编译运行单文件吗?(《JDK 与 Java 版本》)
  • 知道 JDK、JRE、JVM 的包含关系吗?

4. 为什么需要这个知识 ​

初学者常有的困惑,答案都在本节:

  • "为什么装了 JDK 就能跑 Java,换台电脑还要装?"
  • "javac 编译出来的 .class 是什么?和 .java 什么关系?"
  • "Java 说跨平台,为什么我换到另一台机器就报 UnsupportedClassVersionError?"
  • "main 方法为什么必须写成那个样子?少一个 static 会怎样?"

这些不是 "八股",而是决定你能不能独立排查环境与部署问题的常识。

5. 核心概念 ​

5.1 "跨平台"跨的到底是什么 ​

常见说法是"Java 程序一次编写,到处运行",但更准确的说法是:

text
Java 源码      →  编译成字节码    →  JVM 负责翻译成机器指令
(平台无关)        (平台无关)         (平台相关,每个系统一套)

真正跨平台的是"字节码",不是 JVM。 同一份 .class 文件,在 Windows 的 JVM 和 Linux 的 JVM 上都能执行——因为 JVM 屏蔽了操作系统差异(内存管理、线程调度、文件系统、网络)。

由此推出一个重要结论:只要 JVM 版本不同,行为就可能不同。 这也是为什么本知识库把"编译目标"固定在 Java 21,并用 --release 21 约束。

5.2 从源码到运行:完整链路 ​

text
① 编译        源码 .java  ──javac──▶  字节码 .class
② 加载        类加载器把 .class 读入内存,生成 Class 对象
③ 链接与初始化 验证 · 准备(静态变量分配默认值)· 解析 · 执行静态初始化块
④ 执行        JVM 解释执行字节码,热点代码由 JIT 编译成机器码
⑤ 回收        不再使用的对象由垃圾回收器回收(S6 会深入)

对应到常见错误:

阶段典型错误含义
① 编译找不到符号、类型不匹配源码问题,编译器能精确定位
② 加载ClassNotFoundException运行时主动用 Class.forName 等加载器找类,没找到
② 加载NoClassDefFoundErrorJVM 被动要用的类没找到(常因编译时有、运行时缺依赖)
③ 初始化ExceptionInInitializerError静态初始化块抛了异常
④ 执行各种运行时异常逻辑问题,配合 《调试》 定位

5.3 字节码长什么样 ​

用 javap -c 反编译可以看到 JVM 指令(下面取自本节的实验代码):

text
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 时会派上大用场。

5.4 类加载器:三个层次 ​

text
Bootstrap ClassLoader      加载 JDK 核心类(java.lang.*)
      ↑
Platform ClassLoader       加载 JDK 扩展模块
      ↑
App ClassLoader            加载你自己写的类与依赖 jar

"双亲委派"(子加载器先交给父加载器尝试)保证了核心类不会被自定义类覆盖——这也是为什么你不能自己写一个 java.lang.String 去替换它。

5.5 逐字拆解 main 方法 ​

java
public static void main(String[] args) { }
部分含义少了它会怎样
publicJVM 需要从外部调用它找不到主类/方法
staticJVM 启动时还没有对象,必须能直接调用需要先 new 才能用,JVM 做不到
void执行完不返回给 JVM 任何值签名不匹配,无法作为入口
main约定的方法名找不到入口
String[] args接收命令行参数签名不匹配

注意:String[] args 与 String args[] 等价,但前者是推荐写法;String... args 也可作为入口。

6. 递进示例 ​

6.1 最小示例:一个类从编译到运行 ​

bash
javac --release 21 -d target/classes RuntimeMechanism.java
java -cp target/classes cn.kb.learn.syntax.RuntimeMechanism
text
Java 版本: 25.0.2
JVM 名称: OpenJDK 64-Bit Server VM
字节码由 JVM 执行,因此换操作系统不必重新编译
本类的加载器: AppClassLoader
启动参数个数: 0

注意两处细节:

  1. java -cp ... 类全名:运行时要写全限定类名(含包名),不是文件名;
  2. 本类的加载器: AppClassLoader:说明这是你自己写的类,由应用类加载器加载。

6.2 业务示例:命令行参数 ​

bash
java -cp target/classes cn.kb.learn.syntax.RuntimeMechanism hello world

输出变成 启动参数个数: 2。args 就是命令行参数的数组——这是写命令行工具的基础(《数组》 会用到)。

6.3 反例:把 .class 当成万能文件 ​

text
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 或统一运行环境。

7. 完整可运行代码 ​

文件:labs/java-syntax/src/main/java/cn/kb/learn/syntax/RuntimeMechanism.java

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);
    }
}

运行命令:

powershell
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

预期输出:

text
Java 版本: 25.0.2
JVM 名称: OpenJDK 64-Bit Server VM
字节码由 JVM 执行,因此换操作系统不必重新编译
本类的加载器: AppClassLoader
启动参数个数: 0

8. Debug 观察点 ​

  1. 看字节码:执行 javap -c ...,找到 main 方法,对照源码数一数:五行 println 对应五组 getstatic → ldc → invokevirtual 的指令模式;
  2. 看加载器:把 getSimpleName() 去掉,观察输出变成 jdk.internal.loader.ClassLoaders$AppClassLoader@...——这证明"类"在运行时是内存里的一个对象;
  3. 传参数:用 java -cp ... RuntimeMechanism 参数1 参数2 运行,在 args.length 处打断点,观察数组内容;
  4. 观察类加载时机:在类的静态初始化块(static { ... })里打印一行,会发现它只在首次使用该类时执行一次,而不是程序启动时。

9. 常见错误 ​

错误 1:ClassNotFoundException 与 NoClassDefFoundError 分不清 ​

  • ClassNotFoundException:你主动让 JVM 去找一个类(Class.forName("...")),字符串写错了;
  • NoClassDefFoundError:JVM 被动要用的类在 classpath 里找不到,通常是依赖没打进去或版本冲突。

排查后者用 ./mvnw -pl <模块> dependency:tree(《Maven 基础》)。

错误 2:main 方法签名写错 ​

java
public void main(String[] args)          // 少了 static
public static void main(String args)     // 参数类型错了
private static void main(String[] args)  // 可见性不对

这三种都会让程序报"找不到主类"。IDE 的补全能避免 90% 的签名错误:输入 psvm 回车。

错误 3:运行时写错类名 ​

bash
java RuntimeMechanism            # 错:少了包名
java -cp target/classes cn.kb.learn.syntax.RuntimeMechanism   # 对

修复:cn.kb.learn.syntax.RuntimeMechanism 的 .class 文件必须位于 target/classes/cn/kb/learn/syntax/ 下——包名与目录结构必须一致。

10. 动手实验 ​

目标:亲手走一遍"源码 → 字节码 → 运行"的链路。

步骤:

  1. 在 labs/java-syntax 下新增一个类 MyFirst.java,输出三行内容;
  2. 用 Maven 编译,确认 target/classes/cn/kb/learn/syntax/MyFirst.class 存在;
  3. 用 javap -c 查看它的字节码,找出 main 方法对应的指令;
  4. 用命令行参数运行它,观察 args.length 的变化;
  5. 用 java -cp java-syntax/target/classes MyFirst 运行(故意不带包名),记录报错并解释原因。

验收方式:能解释第 5 步报错里的"找不到或无法加载主类"与包名的关系。

扩展任务:删除 target/classes 下的 .class 文件后重新运行,观察报错——这模拟了"部署时漏打包"的线上事故。

11. 练习题 ​

基础题

  1. 判断:JVM 是跨平台的,所以 Java 的字节码也是跨平台的。(对 / 错)
  2. 填空:从源码到运行的四个阶段是 、、、。
  3. 简答:main 方法为什么必须是 static 的?

代码阅读题

text
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 ...
  1. 这段字节码对应哪一行 Java 源码?invokedynamic 在处理什么?

编程题

  1. 写一个程序 SystemInfo,输出:Java 版本、Java 供应商、JVM 名称、操作系统名称与架构。要求用命令行参数控制是否输出详细信息(传入 -v 时输出全部,否则只输出 Java 版本)。

综合任务

  1. 用 javap -v(注意是 -v,输出更详细)查看 RuntimeMechanism 的类文件结构,找到:
    • major version 是多少?它对应哪个 Java 版本?
    • 常量池里有哪些字符串常量?
    • 写一段说明:如果把这个类的编译目标改成 Java 8,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)。

12. 参考答案 ​

基础题
  1. 错。字节码跨平台,JVM 不跨平台——每个操作系统都需要安装对应的 JVM。正是"字节码统一 + JVM 各管各的平台差异"这一组合,实现了跨平台。
  2. 编译、加载、链接与初始化、执行(垃圾回收属于执行期的后台活动)。
  3. 因为 JVM 启动时还没有创建任何对象,只能通过类名直接调用入口方法。实例方法必须先有对象,JVM 无法做到——这就是 static 的意义。
代码阅读题(第 4 题)

对应源码:

java
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 输出。
编程题(第 5 题)
java
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"));
        }
    }
}

运行:

powershell
java -cp java-syntax/target/classes cn.kb.learn.syntax.SystemInfo
java -cp java-syntax/target/classes cn.kb.learn.syntax.SystemInfo -v
综合任务(第 6 题)

javap -v 输出的开头会包含:

text
public class cn.kb.learn.syntax.RuntimeMechanism
  minor version: 0
  major version: 69
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  • major 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。

命令:

powershell
javap -v -cp java-syntax/target/classes cn.kb.learn.syntax.RuntimeMechanism | Select-String "major version|minor version"

13. 掌握标准 ​

  • [ ] 能说清"字节码跨平台、JVM 不跨平台"的含义;
  • [ ] 能指出从源码到运行的每个阶段及其典型错误;
  • [ ] 能用 javap -c 看懂最基础的几条指令;
  • [ ] 能逐字解释 main 方法的签名。

14. 扩展资源 ​

15. 下一步学习 ​

《变量与数据类型》:从最基本的"存一个数据"开始写 Java。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引