目录
Java是将代码编译成一种“字节码”,它类似于抽象的CPU指令,然后针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”的效果。当然,这是针对Java开发者而言。对于虚拟机,需要为每个平台分别开发。为了保证不同平台、不同公司开发的虚拟机都能正确执行Java字节码,SUN公司制定了一系列的Java虚拟机规范。从实践的角度看,JVM的兼容性做得非常好,低版本的Java字节码完全可以正常运行在高版本的JVM上。
Java源码本质上是一个文本文件,我们需要先用javac
把Hello.java
编译成字节码文件Hello.class
,然后,用java
命令执行这个字节码文件:
因此,可执行文件javac
是编译器,而可执行文件java
就是虚拟机。
Java程序的编译和运行涉及到多个阶段和不同的组件,最重要的是Java虚拟机(JVM)的角色,它在程序运行时负责执行字节码。理解Java程序的编译、运行流程及其如何与JVM交互是掌握Java开发的基础。
一.Java编译与执行流程:
- 由开发者编写.java源代码文件。
- Java编译器(javac)将.java源代码文件编译成字节码文件(.class),字节码文件包含平台无关的指令,JVM能够理解并执行这些指令。
- JVM通过类加载器(ClassLoader)加载
.class
文件。 - 字节码被JVM解释执行,或者通过JIT(即时编译器)转化为本地机器代码。(将字节码转换为机器码,从而提高程序的执行效率)
- JVM根据需求进行垃圾回收,自动管理内存以及回收不再使用的对象。
二.编译过程:
当我们编写Java程序并运行javac
命令时,Java编译器将源代码(.java文件)编译成字节码(.class文件)。字节码并不是针对某个特定机器的机器码,而是与平台无关的中间代码。这是Java的跨平台性("Write Once, Run Anywhere")的关键所在。字节码并没有直接针对硬件或操作系统进行优化,因此它不能直接被CPU执行。
Java程序首先由Java编译器(
javac
)编译成字节码(字节码文件(.class
文件)是与平台无关的指令,而JVM可以理解并执行)。这个过程包括:
- 语法分析(Parsing):将源代码(
.java
文件)转换为抽象语法树(AST)。- 符号解析(Symbol Resolution):通过符号表解析类、方法、变量的名称等。
- 字节码生成(Bytecode Generation):将抽象语法树转换为字节码指令,生成
.class
文件。javac HelloWorld.java
编译后会生成一个字节码文件
HelloWorld.class
。这个文件包含了Java源代码的字节码,JVM可以加载并执行它。
三.执行过程:
1.启动JVM(Java虚拟机):
JVM的执行由Java启动类(通常是java
命令)来启动。
java HelloWorld
java
命令启动JVM并传递应用程序的主类(HelloWorld
)给JVM。
2.类加载:
当我们运行Java程序时,JVM会使用类加载器(ClassLoader)加载.class
文件。JVM并不会直接执行.class
文件,而是解析并执行其字节码。JVM将字节码转化为机器码后执行。
JVM使用类加载器(ClassLoader)加载
.class
文件。加载过程包括以下几个步骤:
- 加载:从文件系统、网络、或者其他位置加载字节码文件。
- 链接:包括验证(确保字节码格式正确)、准备(为类变量分配内存)和解析(解析符号引用)。
- 初始化:执行类的静态初始化块(
static
块)和静态变量的初始化。
JVM有三种主要的类加载器:
- Bootstrap ClassLoader:负责加载核心类库,如
rt.jar
中的类。- Extension ClassLoader:加载Java扩展库(如
ext
目录中的库)。- Application ClassLoader:负责加载应用程序类路径下的类。
3.字节码执行:
一旦类被加载,JVM会执行字节码。
执行字节码方式:
解释执行:
- JVM中的解释器逐条解释字节码指令,并将其翻译成机器码执行。
- 这种方式灵活但较慢,因为每条指令都需要在每次执行时解释一次。
JIT编译(即时编译):
- JIT编译器会在程序运行时,动态地将热点代码(频繁执行的代码)编译成机器码。
- JIT优化性能,将程序中的热路径(经常执行的代码)编译为本地机器代码,以避免每次执行时都需要解释。(机器码是与硬件相关的,并且执行速度更快。)
- 即时编译器(JIT)是JVM的一部分,它在程序运行时将热点代码(频繁执行的部分)从字节码转换成机器码,并将这些机器码存储在内存中以便后续直接执行。JIT编译通常会选择一些最常调用的代码块进行编译,从而优化程序的执行速度。
- JIT能够根据程序的实际运行情况进行优化(如内联、死代码消除等),进一步提高性能。
.class
文件内容:
- 常量池(Constant Pool):存储了类、方法、字段等的引用。
- 字段表(Field Table):包含了类中所有字段的定义。
- 方法表(Method Table):包含了类中所有方法的定义。
- 字节码:具体的指令集,表示类中的方法的操作步骤。
JVM执行这些指令时会根据当前系统平台生成机器代码。字节码指令类似于汇编语言,但它们并非直接执行,而是由JVM的解释器或JIT(即时编译器)执行。
字节码与机器码的区别:
字节码:Java源代码通过
javac
编译器编译后生成的中间代码,平台无关,能够在任何安装了JVM的操作系统上运行。字节码是一种虚拟机指令,JVM根据这些指令执行程序的逻辑。机器码:是特定硬件平台能够直接理解和执行的指令集。每种硬件平台(如x86、ARM等)都有其专用的机器码格式。因此,机器码是与硬件相关的,并且执行速度更快。
字节码到机器码:
- 字节码是一种平台无关的中间代码,它不能直接由硬件执行。
- JIT编译是JVM将字节码转换为平台特定的机器码的过程。通过将热点代码编译为机器码,JIT提高了Java程序的执行性能。
- 解释执行和JIT编译是JVM在字节码执行中的两种主要方式,JIT会根据代码的执行频率将热点代码编译为机器码,避免了每次执行时都进行字节码解释。
- JIT优化(如方法内联、死代码消除、常量折叠等)可以进一步提升性能。
字节码转机器码的过程通过JIT技术优化了Java程序的性能,使得Java在保持平台无关性的同时,也能够接近本地代码的执行效率。
4.JVM内存模型:
JVM(Java Virtual Machine,Java虚拟机)内存模型是 Java 程序运行时内存的管理方式,它决定了 JVM 如何管理堆、栈、方法区、程序计数器等内存区域。理解 JVM 内存模型对于优化 Java 程序的性能、调试和分析内存泄漏非常重要。
JVM在程序运行时管理内存,包括以下几个主要区域:
堆(Heap):堆是 Java 程序运行时动态分配内存的地方,用于存储对象实例。所有通过
new
关键字创建的对象和数组都存储在堆内存中。堆的管理与垃圾回收(GC)紧密相关。
堆内存是 JVM 中最大的内存区域,是所有线程共享的。堆中的内存分配由垃圾回收器管理,通过垃圾回收来回收不再使用的对象。堆内存一般划分为新生代(Young Generation)和老年代(Old Generation)两部分,后者存储长期存在的对象,前者用于存储新创建的对象。方法区(Method Area):存储类的元数据(如类名、方法信息、字段信息等),以及静态变量。
在JDK 1.7 之前,方法区被称为永久代(PermGen),但从 JDK 1.8 开始,它被移至了元空间(Metaspace)。栈(Stack):Java栈用于存储方法的局部变量、操作数栈、动态链接和方法返回地址等信息。
每当一个方法被调用时,JVM 会为该方法分配一个栈帧,每个栈帧包含以下内容:
(1)局部变量表:存储方法中的局部变量(基本数据类型、对象引用、返回地址等)。
(2)操作数栈:用于存储操作过程中的临时数据。
(3)动态链接:为方法的调用提供符号引用。
(4)方法返回地址:当方法调用结束时返回到的地址。
Java 栈是线程私有的,每个线程在栈内有自己的栈空间。每个方法在栈中都有一个对应的栈帧,当方法执行完毕后,栈帧会被销毁。栈空间的大小是有限的,过多的递归调用或方法调用会导致 StackOverflowError。程序计数器(PC Register):程序计数器是一块较小的内存空间,用来记录当前线程执行的字节码的地址。每个线程都有自己的程序计数器,线程之间的执行是独立的,因此它们的程序计数器也是各自独立的。
(为什么要有这个程序计数器?为了方便线程切换,Java程序是多线程的,如果有一个优先级比当前线程优先级高,这个时候挂起优先级低的线程然后调用高优先级的线程,当高优先级线程执行完成之后,就会根据程序计数器的值进行恢复低优先级线程的执行)在 Java 虚拟机中,程序计数器用于追踪当前线程执行的字节码指令的位置,当线程切换时,程序计数器会保存每个线程当前指令的位置,线程恢复时可以继续执行。本地方法栈(Native Method Stack):专主要用于处理 JVM 调用 Native 方法时的栈空间。Native 方法是使用其他语言(如 C 或 C++)编写的,JVM 会通过本地方法栈来执行这些方法。
与 Java 栈类似,本地方法栈是线程私有的。它存储本地方法调用的相关信息,比如栈帧和局部变量等。运行时常量池(Runtime Constant Pool):常量池存储类中定义的常量(如字符串字面量、类常量等)。它是方法区的一部分,用于存储编译时生成的常量,它会在类加载到 JVM 时被初始化,并保存在方法区中。
直接内存(Direct Memory):直接内存并不是 JVM 运行时内存的一部分,而是通过
sun.misc.Unsafe
类直接访问物理内存的一块区域。它通常用于高效的数据交换,或者通过NIO
(New IO)进行文件和网络操作。
直接内存不受 JVM 堆内存限制,但它依赖于操作系统的内存管理,并且如果使用不当可能导致内存泄漏或访问冲突,因此它较少用于一般开发中。
5.垃圾回收(GC):
Java的垃圾回收机制自动回收不再使用的对象,释放内存。JVM通过GC(垃圾回收)来管理堆内存,减少内存泄漏的风险。
GC的过程包括:
- 标记(Mark):标记所有被引用的对象。
- 清除(Sweep):删除所有没有被标记的对象。
- 压缩(Compact):整理内存,确保内存空间连续。
常见的垃圾回收算法包括标记-清除、标记-整理、复制算法等。
6. Java运行时的执行引擎:
JVM的执行引擎负责执行字节码指令,有两种主要的执行方式:
解释器(Interpreter):
- 解释器逐条解释并执行字节码。
- 每次执行时都要将字节码解释成机器码,因此效率较低。
JIT编译器(Just-In-Time Compiler):
- JIT编译器在程序运行时,检测到热点代码并将其编译为机器码。
- 热点代码会直接使用机器码执行,避免了每次解释执行。
- JIT编译器提高了程序的执行速度,尤其是在大量重复执行的代码路径上。