Java 基础语言 ① ------ Java 运行机制与开发环境:从 javac 到 JVM 全流程解析
-
- [一、Java 的编译与运行过程](#一、Java 的编译与运行过程)
-
- [1.1 编译阶段:javac 与字节码](#1.1 编译阶段:javac 与字节码)
- [1.2 运行阶段:java 与 JVM](#1.2 运行阶段:java 与 JVM)
- 二、内存布局初探:栈与堆
-
- [2.1 栈(Stack):方法调用与局部变量](#2.1 栈(Stack):方法调用与局部变量)
- [2.2 堆(Heap):对象与全局数据的存储池](#2.2 堆(Heap):对象与全局数据的存储池)
- [2.3 引用:栈与堆之间的桥梁](#2.3 引用:栈与堆之间的桥梁)
- 三、垃圾回收(GC)基本概念
-
- [3.1 GC 的作用范围与判定逻辑](#3.1 GC 的作用范围与判定逻辑)
- [3.2 三种核心回收算法](#3.2 三种核心回收算法)
- [3.3 分代垃圾回收(主流 JVM 的实现方式)](#3.3 分代垃圾回收(主流 JVM 的实现方式))
- [3.4 内存压缩与句柄机制](#3.4 内存压缩与句柄机制)
- [四、包管理与 import](#四、包管理与 import)
-
- [4.1 包的本质](#4.1 包的本质)
- [4.2 import 机制](#4.2 import 机制)
- [4.3 包级访问保护------容易被忽视的封装利器](#4.3 包级访问保护——容易被忽视的封装利器)
- [4.4 CLASSPATH:JVM 去哪找类](#4.4 CLASSPATH:JVM 去哪找类)
- 总结

🎬 博主名称: 超级苦力怕
🔥 个人专栏: 《Java 后端修炼手册》《Java 基础语言》
🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!
很多初学者学 Java 时,只记住了"写代码 → 点运行",却不清楚
javac和java背后到底发生了什么,更不知道内存里的对象究竟存在哪里、什么时候会被回收。本文带你从编译到运行,系统梳理 JVM 内存布局、垃圾回收机制和包管理规则,帮你建立 Java 程序的完整心智模型。适合刚学完基础语法但想"知其所以然"的 Java 初学者。
如果你是初学者,可以选择暂时跳过这章,或留有印象即可。
一、Java 的编译与运行过程
Java 程序的执行不是单步完成的,而是分为编译 和运行两个独立阶段。这种分阶段设计兼顾了代码的可移植性、安全性和执行效率。
1.1 编译阶段:javac 与字节码
Java 源代码(.java 文件)首先需要通过编译器 javac 转换为字节码 ,存储在 .class 文件中。
源代码(.java) ──javac──▶ 字节码(.class) ──java──▶ JVM 执行
关键细节:
- 每个类一个 .class :即使在一个
.java文件中定义了多个类或接口,编译器也会为每一个生成独立的.class文件。 - 静态检查:约有一半的错误在编译阶段就会被发现,包括类型不匹配、语法错误、包依赖问题等。
- 性能提升:字节码比源代码更接近机器指令,因此 Java 的运行速度显著快于纯解释型语言。
1.2 运行阶段:java 与 JVM
当执行 java 类名 命令时,实际启动的是 JVM(Java 虚拟机) 。JVM 充当字节码解释器,逐条执行 .class 文件中的指令。
JVM 在运行时承担了编译器无法完成的工作:
- 运行时检查:空指针异常、数组越界检查等,这些在编译阶段无法预见的问题,由 JVM 在运行时捕获。
- 动态方法查找 :JVM 根据对象的动态类型(而非变量的静态类型)来决定调用哪个方法实现,这是多态的底层基础。
| 阶段 | 工具 | 输入 | 输出 | 主要检查 |
|---|---|---|---|---|
| 编译 | javac |
.java 源文件 |
.class 字节码 |
语法错误、类型检查、包依赖 |
| 运行 | java(启动JVM) |
.class 字节码 |
程序执行结果 | 空指针、数组越界、动态绑定 |

二、内存布局初探:栈与堆
JVM 将内存划分为不同的区域来管理数据,每种区域有不同的职责和生命周期。理解栈与堆是理解 Java 一切运行行为的基础。
2.1 栈(Stack):方法调用与局部变量
栈是管理程序执行流程的地方,特点是先进后出、存取极快、空间有限。
- 存储内容 :所有的局部变量 和方法参数。
- 栈帧 :每当调用一个方法时,JVM 在栈顶创建一个栈帧 ,存储该方法的参数和局部变量。对于非静态方法,栈帧中还会包含一个隐藏的
this引用。 - 生命周期:方法执行完毕,栈帧立即弹出,其中的变量随之消失。
- 物理限制 :栈空间通常只有几 MB,递归过深会导致
StackOverflowError。
✅ 栈帧的创建与销毁
java
void a() {
int x = 10; // x 存入 a 的栈帧
b(x); // 调用 b,创建新栈帧
}
void b(int val) {
int y = val + 5; // val 和 y 存入 b 的栈帧
} // b 结束,栈帧弹出,y 和 val 消失
2.2 堆(Heap):对象与全局数据的存储池
堆是一个巨大的共享存储池,用于存放需要长期存在或跨方法共享的数据。
- 存储内容 :所有对象 (包括数组)和类变量(静态变量)。
- 生命周期:实例变量随对象存在,直到对象不再被引用;静态变量在类加载后一直存在。
- 内存管理:堆不会在方法结束时自动释放,而是由**垃圾回收器(GC)**统一管理。
2.3 引用:栈与堆之间的桥梁
Java 不允许直接操作内存地址,程序员只能通过引用来访问对象。
- 变量本身(引用)存储在栈 (局部变量)或堆(实例变量)中
- 引用指向的实际对象始终位于堆中
- 这种抽象避免了 C/C++ 中常见的野指针和内存越界问题
| 对比维度 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 存储内容 | 局部变量、方法参数、this |
所有对象、数组、静态变量 |
| 管理方式 | 栈帧自动压入/弹出 | 垃圾回收器自动回收 |
| 生命周期 | 方法结束后立即释放 | 对象不再被引用后才回收 |
| 空间大小 | 较小(几 MB) | 较大(可动态扩展) |
| 存取速度 | 极快 | 相对较慢 |
| 线程关系 | 每个线程独立栈 | 所有线程共享 |

⚠️ 误区:所有变量都存在栈里
正确理解: 局部变量的确存在栈中,但实例变量(成员变量)存在于堆中的对象内部。基本类型的局部变量直接把值存在栈里,而引用类型的局部变量存的是"指向堆中对象的引用",真正的对象仍在堆中。
三、垃圾回收(GC)基本概念
垃圾回收是 JVM 自动管理堆内存的核心机制,将程序员从手动内存管理中解放出来,有效减少内存泄漏。
3.1 GC 的作用范围与判定逻辑
- GC 只作用于堆:栈内存由栈帧自动管理,无需 GC 参与。
- 可达性分析 :JVM 从根出发执行深度优先搜索(DFS),判定对象是否存活。
根(Roots)的定义:
- 所有栈帧中的局部变量
- 所有静态类变量
如果一个对象能从根通过引用链触达,它就是存活的 ;否则即被视为垃圾,可以被回收。
✅ 对象何时变成垃圾
java
void method() {
Object obj = new Object(); // obj 引用指向新对象,对象存活
obj = null; // 失去引用,对象变成垃圾
// 或者方法结束,obj 随栈帧消失,对象也变成垃圾
}
3.2 三种核心回收算法
| 算法 | 核心思想 | 优点 | 缺点 |
|---|---|---|---|
| 标记-清除 | 先标记存活对象,再清除未标记的 | 不浪费额外空间 | 产生内存碎片,需暂停程序 |
| 复制算法 | 将存活对象复制到新空间,清空旧空间 | 速度快,顺便完成压缩 | 浪费一半内存 |
| 分代回收 | 年轻代用复制算法,老年代用标记-清除 | 综合效率最优 | 实现复杂 |
3.3 分代垃圾回收(主流 JVM 的实现方式)
现代 JVM(如 HotSpot)基于"大多数对象寿命很短"这一观察,将堆分为:
- 年轻代(Young Generation) :
- Eden 区:对象诞生的地方。Eden 满时触发 Minor GC
- Survivor 区(S0/S1):Eden 中幸存的对象被复制到这里
- 老年代(Old Generation):多次在年轻代回收中幸存的对象被"晋升"至此,回收频率较低

3.4 内存压缩与句柄机制
频繁分配和回收会造成内存碎片------空闲空间被切割成小块,可能导致大对象无法分配。
- 压缩:GC 回收时将存活对象移动到一起,腾出连续的空闲空间。
- 句柄表:JVM 使用句柄("指向指针的指针")来引用对象。当 GC 移动对象时,只需更新句柄表中的单个指针,无需修改程序中成千上万个引用变量。

⚠️ 误区:调用
System.gc()能强制立即回收内存正确理解:
System.gc()只是向 JVM 提出回收建议,JVM 不一定立即执行,甚至可能完全忽略。实际开发中不应依赖手动触发 GC。
四、包管理与 import
包不仅是组织代码的文件夹,更是 Java 实现命名空间管理 和访问控制的核心机制。
4.1 包的本质
- 类的逻辑集合:将功能相关的类和接口组织在一起
- 防止命名冲突 :两个开发者都可以定义
Frame类,只要分别放在java.awt和photo包中即可共存 - 与文件系统对应 :包名
X.Y.Z对应目录结构X/Y/Z
4.2 import 机制
Java 允许使用全限定名 直接访问任何类,但 import 让代码更简洁。
| 导入方式 | 语法 | 示例 |
|---|---|---|
| 导入单个类 | import 包名.类名 |
import java.io.File |
| 导入整个包 | import 包名.* |
import java.io.* |
| 隐式导入 | 无需声明 | java.lang 包自动导入 |
✅ import 的使用
java
import java.io.File; // 导入单个类
import java.util.*; // 导入整个包
// java.lang 自动导入,无需手动声明
String s = "hello"; // 直接用
System.out.println(s); // 直接用
⚠️ 误区:
import java.io.*会把包下所有类都加载到内存正确理解:
import只是让编译器知道去哪找类,不会在运行时加载所有类。JVM 只在首次实际使用时才加载类(懒加载机制)。
4.3 包级访问保护------容易被忽视的封装利器
Java 有四种访问权限,其中默认(包级)权限最容易被忽略但非常实用:
- 如果不写
public、private或protected,成员拥有包级访问权限 - 同一包内的类"相互信任",可以访问彼此的包级成员
- 包外的类完全看不到包级成员
这在构建**抽象数据类型(ADT)**时特别有用------可以将公开的 API 类设为 public,但将其内部节点的字段设为包级,防止外部代码直接篡改内部数据结构。
✅ 包级访问权限保护内部结构
java
// List.java ------ 公开的 API
public class List {
ListNode head; // 包级:包外不可直接访问
}
// ListNode.java ------ 内部节点,包外不可见
class ListNode { // 包级类:包外无法实例化
int item; // 包级字段:包外不可直接修改
ListNode next;
}
4.4 CLASSPATH:JVM 去哪找类
JVM 和编译器通过 CLASSPATH 环境变量来确定包的查找路径,它通常包含当前目录(.)和 JAR 文件的路径。编译带包定义的源文件时,必须从包目录之外 执行 javac(如 javac list/SList.java),否则编译器会因包名与路径不匹配而报错。

总结
| 知识点 | 核心概念 | 关键要点 |
|---|---|---|
| 编译与运行 | javac 编译 → .class 字节码 → JVM 解释执行 |
编译时检查语法和类型;运行时检查空指针和数组越界 |
| 内存布局 | 栈存局部变量,堆存对象 | 栈帧随方法结束自动弹出;堆由 GC 管理 |
| 垃圾回收 | 从根出发做可达性分析,不可达即垃圾 | 分代 GC 综合效率最优;System.gc() 只是建议 |
| 包管理 | 命名空间 + 访问控制 + 文件系统映射 | 默认包级权限是封装利器;import 不触发类加载 |
💡 核心结论: Java 通过"编译生成字节码 + JVM 解释执行"实现了跨平台,通过"栈管理执行流程 + 堆 + GC"实现了自动内存管理,通过"包 + import + 访问修饰符"实现了命名空间隔离和封装保护。这三层机制共同构成了 Java 程序的运行基础。
💡 核心结论: 理解栈与堆的区别是排查内存问题和理解 GC 行为的前提------栈上的数据随方法结束自动消失,堆上的对象只有在失去所有引用后才可能被 GC 回收。
