当我们编写一个Java程序时,看起来再简单不过:定义一个Main类,写下main方法,输出一行文字。保存文件,运行javac,再运行java,屏幕上就会出现Hello, World。但在这两个命令之间,到底发生了什么?Java并没有直接运行“.java”源码文件,而是经历了一整套复杂机制。这篇文章深入拆解从源码到JVM执行的完整过程,带你看见Java程序的真实旅程。
第一步,编写Java源代码。一个典型的Java源文件看起来像是人类可读的文本。例如,public class Main { public static void main(String[] args) { int a = 10; int b = 20; System.out.println(a + b); } }。这个文件被保存为Main.java。此时,它只是由字符组成的源代码。尽管语法上看起来很清晰,但计算机的CPU并不能直接执行这些文本。计算机只理解二进制机器码,而人类写的Java语言只是高层次的抽象指令。
第二步,Java编译器javac介入。当我们执行javac Main.java,编译器会逐个检查源代码中的结构。javac会做语法分析、类型检查、变量声明校验、方法调用解析、类结构确认、访问修饰符判断,以及各种编译期规则校验。如果发现错误,编译会失败并给出提示;如果一切正常,编译器就会生成一个新的文件Main.class。很多人误以为编译后就是机器码,但Java的编译并没有走到最后一步。这时的Main.class里装的是字节码,也就是一种面向Java虚拟机的中间指令集。
第三步,弄清楚为什么Java不生成机器码,而是字节码。C或C++等语言,通常会直接把源代码编译成当前CPU架构相关的原生机器码。比如在x86机器上编译出来的程序不能在ARM机器上直接运行,必须针对不同平台重新编译。Java采用了完全不同的思路:javac只把源码翻译成与CPU无关的字节码。字节码大致包含iload、iadd、invokevirtual、return这样的抽象指令。这些指令不直接代表底层电路的开关,而是描述一种可移植的虚拟机操作。这个设计是Java实现“一次编写,到处运行”的基石。假如你从Windows平台编译出一个.class文件,在Linux或macOS上只要安装了对应的JVM,就能直接运行同一个文件,而不需要重新编译。这正是“相同字节码,不同JVM”带来的跨平台红利。
第四步,类加载器出场。当我们执行java Main,JVM会启动,并找到Main.class文件,将其读入内存。这个读取过程并不是简单地将字节码一次性塞进内存,而是由类加载器来负责定位、验证、准备和初始化类。类加载器先加载字节码,解析类的结构,验证字节码的安全性,然后分配静态变量内存,并执行静态初始化逻辑。经过这个流程,Main类才真正成为一个可以被JVM使用的运行时对象。
第五步,JVM执行引擎真正开始运行字节码。执行引擎由解释器和JIT编译器(即时编译器)共同组成。JVM刚启动时,通常使用解释器逐条翻译字节码,速度相对较慢。但是当某段代码被频繁执行时,JIT编译器会把它编译成本地机器码,直接交由CPU执行,从而大幅提升运行速度。这就是Java程序往往需要“热身”的原因。与此同时,JVM还必须管理好运行时数据区。堆里存放new出来的对象,栈里存放局部变量和方法调用帧,方法区保存类元信息,还有程序计数器和本地方法栈。一段简单的a+b运算,背后也要经过方法调用、操作数栈、局部变量表等多个环节的配合。最终,System.out.println输出结果,方法执行完,main函数退出,JVM开始销毁相关对象,并完成程序的结束流程。
所以,当你在命令行中敲下java Main时,看到的绝不仅仅是一次简单的方法调用。从Main.java到字节码,再从字节码到可执行机器码,Java用层层抽象换来了跨平台能力。理解这5个阶段,你才能真正理解Java程序的运行本质。下次再有人告诉你“Java比较慢”时,你可以告诉对方:Java的慢只是启动阶段的解释执行,而JIT会逐渐把热点代码编译成机器级优化,现代JVM的运行时性能早已不可同日而语。从源码到虚拟机的这段旅程,正是Java长盛不衰的秘密之一。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.