假设这是你接触 Java 的第一天。你照着教程敲下这样一段代码:
1public class HelloWorld {
2 public static void main(String[] args) {
3 System.out.println("你好,Java!");
4 }
5}把它存成 HelloWorld.java,然后在命令行里敲两个命令:
1javac HelloWorld.java
2java HelloWorld屏幕就打印出“你好,Java!”。大多数初学者在这里就满足了,但这一瞬间其实发生了很多事:你的代码先是变成了一种你看不见的中间形态,再被一台“软件造出来的计算机”逐条执行,最终才变成 CPU 上的指令。本篇就把这趟旅程完整走一遍。至于 public、static、main 这些词为什么必须这么写,是下一篇的事,今天只回答一个问题:程序是怎么跑起来的。
一趟需要“两次翻译”的旅程
先说一个绕不开的事实:你的 CPU 听不懂 Java。CPU 只认识机器指令——由 0 和 1 组成的、针对特定芯片型号(如 Intel、ARM)的指令。不同平台的指令体系还互不相同。
所以任何高级语言写出的程序,想被 CPU 执行,都得经过“翻译”。C 语言的做法是一次翻译到底:编译器直接产出针对某个平台的机器码,换一个平台就得重新编译一次。Java 选择了另一条路:翻译两次。
第一次翻译由 javac 完成,把源码变成字节码;第二次翻译发生在你运行程序的那一刻,由 JVM 完成,把字节码变成当前这台机器、这块 CPU 真正执行的指令。整个 Java 技术体系,就是围绕“中间那层字节码”设计的。下面一站一站看。
第一站:javac 把源码变成字节码
你敲下的第一个命令 javac HelloWorld.java,调用的是 JDK 里自带的编译器 javac。它逐行检查你的源码,如果没有语法错误,就生成一个新的文件 HelloWorld.class。
这个 .class 文件里装的不是机器码,而是 字节码(bytecode):一套给 JVM 看的指令。JVM 是 Java 设计者定义的一台“想象中的计算机”,它有自己的指令集——就像真实的 CPU 有它的指令集一样,只是这套指令不属于任何一家芯片厂商,也不依赖任何操作系统。指令之所以叫“字节码”,是因为每条指令的操作码只占一个字节 4。
你可以用 JDK 自带的 javap 工具偷看一眼 .class 内部长什么样:
1javap -c HelloWorld输出大致是:
1public static void main(java.lang.String[]);
2 Code:
3 0: getstatic #7 // Field System.out
4 3: ldc #13 // String 你好,Java!
5 5: invokevirtual #14 // Method PrintStream.printlngetstatic(取一个静态字段)、ldc(把一个常量装入栈)、invokevirtual(调用一个方法)——这就是 JVM 的“语言”。你完全不需要读懂它,只要记住一件事:.class 文件不是给 CPU 的,是给 JVM 的。
第二站:JVM,一台软件造出来的计算机
第二个命令 java HelloWorld 启动了一个叫 JVM 的程序。JVM 本身是运行在你操作系统上的一个普通程序,但它模拟了一台完整的计算机:有自己的指令集,有自己的内存管理方式。Oracle 官方的定义是:JVM 是一台“抽象的计算机”,有指令集,并在运行时操作内存 2。
JVM 拿到 HelloWorld.class 后做三件事。首先是 类加载:JVM 里的类加载器把 .class 文件读进内存,并检查字节码是否合法、安全——乱写的字节码会被当场拒绝,这是 Java 安全模型的一道闸门 4。
然后是 执行。执行引擎读一条字节码指令、执行一条。但这里有个精妙之处:JVM 并不是从头到尾傻乎乎地逐条翻译。Java 的官方实现叫 HotSpot,意思是它会一边运行一边统计哪些方法被反复调用——这些是“热点”。刚开始,它用解释器把字节码一条条翻译成机器码执行,好处是启动快;一旦某个方法被调用足够多次,JIT(Just-In-Time,即时编译)编译器就把这段字节码整个编译成针对当前 CPU 的机器码缓存起来,下次直接运行优化过的机器码 3。
这就是为什么你常听到“Java 程序越跑越快”:一个长期运行的服务器程序,启动后先“解释”一阵子,随后热点代码被逐段编译成原生指令,性能接近甚至超过直接用 C 写的程序。
“一次编译,处处运行”是怎么做到的
现在可以回答那个著名的口号了。Java 的跨平台,靠的是把“最后一次翻译”推迟到了运行时:
- 编译只做一次:你在 Windows 上用
javac编译出的HelloWorld.class,拷贝到 Mac 或 Linux 上,不用重新编译,字节码是一模一样的。 - JVM 每个平台各有一个:Windows 上的 JVM、Mac 上的 JVM、Linux 上的 JVM,是不同的程序,由不同的人(或公司)针对各自平台开发。它们读的是同一份字节码,但翻译出来的机器码各不相同——那是各自 CPU 的事 12。
打个比方:字节码是一份用“世界语”写成的剧本,原样存档,谁也不用改。每个城市都配了一位口译员(JVM),开演时他把世界语剧本现场翻成当地方言(机器码),本地观众(CPU)这才听得懂。换一座城市,剧本不用重写,换一位口译员即可。
代价也要说清楚:多了一层翻译,程序启动时会比“一次编译成机器码”的语言稍慢;而且没有 JVM 的机器上跑不了 Java 程序。Java 的取舍是——用一点运行时开销,换来“一份代码处处能跑”。
JDK、JRE、JVM:是三个东西还是一件事?
初学 Java 的第一道坎,是安装时撞见三个缩写。它们其实是层层包含的三个“同心圆”,从里到外是:
- JVM:最小的一层,真正执行字节码的那台“虚拟机”。只装它还不够,因为你的程序要用现成的类库(比如
System.out.println里System这个现成类),这些代码不在 JVM 里。 - JRE(Java Runtime Environment,Java 运行环境) = JVM + 核心类库。普通用户要“运行”别人的
.class程序,装 JRE 就够了。 - JDK(Java Development Kit,Java 开发工具包) = JRE + 开发工具,最重要的就是编译器
javac,还有javap、调试器等。你要“开发”程序,必须装 JDK 1。
用厨房来记:JVM 是灶台,JRE 是灶台加齐备的锅碗瓢盆(能做菜了),JDK 还多一套菜刀和菜谱工具(能自己发明新菜了)。普通食客只要吃饭(运行程序),JRE 足够;你是来学做菜的,得装 JDK。如今的发行版也早已把两者打包:直接安装一个 JDK,运行所需的一切就都齐了,不用再单独找 JRE。
走到这里,你已经看到了全貌
把整条链再收一遍:你写的是给人读的 .java 源码;javac 把它编译成给 JVM 读的 .class 字节码;运行 java 命令时,本机安装的那个 JVM 把字节码加载进来,先解释执行、再把热点编译成机器码,最终你的 CPU 执行这些指令,屏幕上才出现那行字。
往后你学的每一个语法细节,都能在这张地图上找到位置:你写方法,是在给 JVM 准备可调用的指令序列;你用对象,是在指挥 JVM 管理内存。而下一篇文章,我们会回到开头那段 HelloWorld,把那句看起来像咒语一样的 public static void main(String[] args) 逐词拆开——你会看到,正是它规定了 JVM 该从哪里开始执行你的程序。