程式執行環境 FAQ
一個程式怎麼被載入、在記憶體裡怎麼擺、又怎麼被執行 —— 系統與語言內部機制面試常問的 那些概念。
1) 行程(process)與執行緒(thread)
| 行程 Process | 執行緒 Thread | |
|---|---|---|
| 定義 | 一個正在執行的程式實例 | 行程內部的一個執行單位 |
| 記憶體 | 有自己隔離的位址空間 | 共用行程的 heap 與程式碼 |
| 擁有 | 程式碼、heap、檔案 handle、PCB | 自己的 stack、暫存器、程式計數器 |
| 溝通 | IPC(pipe、socket、共享記憶體) | 共享記憶體(便宜,但需要同步) |
| 成本 | 建立/切換都貴 | 建立/切換都輕 |
| 失敗影響 | 崩潰只影響該行程 | 一條掛掉可能拖垮整個行程 |
重點
- 執行緒共用 heap → 需要同步機制(鎖、atomic)來避免資料競態。每條執行緒有 自己的 stack。
- 行程之間的 context switch(要換位址空間、刷 TLB)比同一行程內執行緒之間的切換貴。
- 並行(concurrency)=多個任務在重疊的時間區間內各自推進; 平行(parallelism)=多個任務真的在同一瞬間跑在多顆核心上。
2) 程式的記憶體佈局
典型的行程位址空間,由低位址到高位址:
text
+------------------------+ high address
| Stack | grows down ↓ (local vars, call frames, return addrs)
+------------------------+
| ↓ |
| |
| ↑ |
+------------------------+
| Heap | grows up ↑ (dynamic allocation: malloc/new)
+------------------------+
| BSS | uninitialized globals/statics (zeroed)
+------------------------+
| Data | initialized globals/statics
+------------------------+
| Text | program code (read-only, executable)
+------------------------+ low address
- Stack:快、自動、後進先出。每次函式呼叫推入一個 frame,返回就彈出。大小有 上限 → 太深或無窮遞迴會造成 stack overflow。
- Heap:手動或由 GC 管理,生命週期是動態的,較慢,而且會碎片化。 記憶體洩漏就住在這裡(還可達但用不到,或是沒有釋放的記憶體)。
- Text/Data/BSS:載入時就固定了。
Stack 與 Heap(面試必考)
| Stack | Heap | |
|---|---|---|
| 配置 | 自動(依作用域) | 明確配置/GC |
| 速度 | 非常快(移一下指標) | 較慢 |
| 生命週期 | 到函式返回為止 | 到被釋放/不可達為止 |
| 大小 | 小、固定 | 大、有彈性 |
| 執行緒 | 每條執行緒一個 | 共用 |
3) 一個程式是怎麼被載入與執行的
text
Source code
│ compile / assemble
▼
Object files ──link──► Executable (or bytecode / script)
│
▼ OS loader
Loaded into memory: text, data segments mapped; stack & heap set up
│
▼
Entry point (e.g. main) → CPU fetch-decode-execute cycle
- 編譯/建置:原始碼 → 機器碼或 bytecode。連結(linking)負責解析符號 (靜態連結=直接包進去;動態連結=載入或執行時透過共享函式庫解析)。
- 載入:OS 的 loader 建立位址空間、映射各段、設好 stack、載入動態函式庫, 然後跳到程式入口點。
- 執行:CPU 跑 fetch–decode–execute 循環;OS 排程器把執行緒分時排到核心上; 系統呼叫(syscall)則跨進 kernel mode 去做 I/O 之類的事。
4) 編譯式、直譯式與 JIT
| 模式 | 做法 | 優點 | 缺點 |
|---|---|---|---|
| 編譯式(AOT) | 執行前先把原始碼編成原生機器碼 | 啟動與執行都快,不需要 runtime | 綁平台、建置循環較慢 |
| 直譯式 | 執行時邊讀邊執行指令 | 可攜、開發循環快、有彈性 | 執行較慢 |
| JIT | 先直譯 bytecode,執行時再把熱點編成原生碼 | 接近原生的速度+可攜性 | 有暖機成本、記憶體用量較高 |
- Java 把原始碼編成 bytecode(
.class),再由 JVM 執行。 JVM 一開始是直譯,同時收集執行剖析資料,之後由 JIT 編譯器(C1/C2)把熱門 方法編成原生碼,並套用積極的最佳化(inline、迴圈展開、逃逸分析)。 - 這也是為什麼 Java 要跑一段「暖機」時間才會到達最高吞吐量。
5) 垃圾回收基礎
自動記憶體管理:回收 heap 上已經不可達的物件。
- 可達性:只要能從 GC root(stack 上的參考、static 欄位、活著的執行緒) 走到的物件就是活的。其他都是垃圾。
- 世代假說:大部分物件都很早死。所以 heap 分成新生代(Eden + survivor)與
老年代。
- Minor GC 收新生代(頻繁、便宜)。
- Major/full GC 收老年代(較少、較貴)。
- 常見演算法:mark-and-sweep、mark-compact、copying collector。
- **Stop-the-world(STW)**會在 GC(的某些階段)凍結應用執行緒;現代的收集器 (G1、ZGC、Shenandoah)把停頓時間壓到最小。
- 取捨:GC 消滅了一整類 bug(use-after-free、double-free),代價是吞吐量、 額外記憶體開銷,以及停頓時間不好預測。
沒有 GC 的語言(C、C++、Rust)靠手動管理或所有權機制,用安全性與心力換取控制權與 可預測的延遲。
6) Runtime 執行環境
Runtime 是程式執行期間提供各種服務的軟體層:記憶體管理、排程、標準函式庫,以及 對平台的抽象。
- JVM(Java):載入 bytecode、驗證、JIT 編譯,並管理 GC 與執行緒。 「一次撰寫,到處執行」—— bytecode 在各家 JVM 實作之間是可攜的。
- 語言 VM/直譯器:它們負責解析與執行。其中受管理的那些還會帶自己的記憶體 管理 —— 但那是實作的性質,不是「身為直譯器」就有的(嵌在宿主程式裡的 Lua 或 C 直譯器可能完全沒有)。而會不會同時做 JIT 則因實作而異:V8(JavaScript)會,而且很積極; CPython 在標準版本裡不會 —— 它直譯 bytecode,並用參考計數加上環收集器 回收記憶體(3.13 附了一個實驗性的 JIT,而 PyPy 是有 JIT 的另一個實作)。
- OS 也是一種 runtime:提供行程、執行緒、虛擬記憶體與系統呼叫。
- 容器:把應用程式+它的 runtime+依賴一起打包,讓它在各種環境下的執行一致 (靠 namespace/cgroup 隔離,不是完整的虛擬機)。
關鍵想法:同一個程式在不同 runtime 下可以有不同表現(GC 參數、JIT 暖機、 可用的核心數與記憶體),所以效能測試一定要在具代表性的環境裡跑。