vault backup: 2026-08-08 19:01:04
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
---
|
||||
tags: [go/lang, pprof, cpu-profile, heap-profile, mutex-profile, block-profile]
|
||||
create time: 2026-08-08 19:00
|
||||
update time: 2026-08-08 19:00
|
||||
---
|
||||
|
||||
# Pprof 性能分析指南
|
||||
|
||||
## 概述
|
||||
|
||||
pprof 是 Go 生态中最核心的性能诊断工具链,内置于 `runtime/pprof` 和 `net/http/pprof` 中。它能采集 CPU、内存、锁竞争和 goroutine 阻塞等维度的数据,并通过 `go tool pprof` 生成可视化的调用关系图。掌握 pprof 的使用是排查线上性能问题的必备技能。
|
||||
|
||||
> [!NOTE] 一句话总结
|
||||
> pprof 的价值不在于你收藏了多少命令,而在于你能否在给定一份 profile 数据的几秒内定位到瓶颈所在。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 数据采集原理
|
||||
|
||||
Go 运行时在每个 OS 线程上定期注入信号采样(默认每 10ms 一次),记录:
|
||||
- 当前执行的函数栈
|
||||
- 分配的内存大小和位置
|
||||
- 锁持有状态
|
||||
- goroutine 的阻塞原因
|
||||
|
||||
这些数据被聚合后写入 Profile 文件,可由 `go tool pprof` 解析。
|
||||
|
||||
### go tool pprof 基本用法
|
||||
|
||||
```bash
|
||||
# 从 HTTP 端点获取 profile(需导入 _ net/http/pprof)
|
||||
go tool pprof -http=:8080 http://localhost:8080/debug/pprof/profile?seconds=30
|
||||
|
||||
# 从本地文件获取
|
||||
go tool pprof myapp.cpu.pprof
|
||||
|
||||
# 从 running binary 获取(需 pid)
|
||||
go tool pprof myapp http://localhost:6060/debug/pprof/heap
|
||||
```
|
||||
|
||||
常用输入模式:
|
||||
|
||||
| 参数 | 含义 |
|
||||
|------|------|
|
||||
| `-top` | 按指标排序列出顶级函数 |
|
||||
| `-tree` | 树形展示调用链 |
|
||||
| `-web` | 用 Graphviz 生成调用图 |
|
||||
| `-focus=regex` | 只显示匹配 regex 的函数及其子树 |
|
||||
| `-ignore=regex` | 忽略匹配 regex 的函数 |
|
||||
| `-alloc_space` / `-alloc_objects` | 指定分配维度 |
|
||||
|
||||
### CPU Profile 解读
|
||||
|
||||
CPU profile 记录了哪个函数消耗了最多的 CPU 时间。获取方式:
|
||||
|
||||
```go
|
||||
// 方法一: HTTP endpoint (推荐用于服务)
|
||||
import _ "net/http/pprof"
|
||||
http.ListenAndServe("localhost:6060", nil)
|
||||
|
||||
// 方法二: 代码手动开启
|
||||
f, _ := os.Create("cpu.pprof")
|
||||
pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
```
|
||||
|
||||
```bash
|
||||
go tool pprof -top app cpu.pprof
|
||||
# 输出示例:
|
||||
# flat flat% sum% cum cum%
|
||||
# 45.2s 45.2% 45.2s 89.1s 89.1% main.heavyComputation
|
||||
# 23.1s 23.1% 68.3s 23.1s 23.1% main.parseJSON
|
||||
```
|
||||
|
||||
> [!TIP] 关键指标
|
||||
> - **flat**: 该函数自身消耗的 CPU 时间(不含调用的子函数)
|
||||
> - **cum**: 包括所有子函数的总消耗
|
||||
> - 如果 flat 很高但 cum 也很高 → 函数本身耗 CPU
|
||||
> - 如果 flat 很低但 cum 很高 → 问题在子函数中
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["main.handleRequest<br/>cum: 100s"] -->|"30s"| B["parseJSON<br/>flat: 23s"]
|
||||
A -->|"70s"| C["heavyComputation<br/>flat: 45s"]
|
||||
C -->|"25s"| D["sortAlgorithm<br/>flat: 25s"]
|
||||
|
||||
style A fill:#e3f2fd
|
||||
style C fill:#ffebee
|
||||
style D fill:#fff3e0
|
||||
```
|
||||
|
||||
### Heap Profile — inuse vs alloc
|
||||
|
||||
Heap profile 区分两种视角:
|
||||
|
||||
| 维度 | 含义 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| `inuse_space` / `inuse_objects` | 当前仍占用的内存/对象数 | 排查内存泄漏 |
|
||||
| `alloc_space` / `alloc_objects` | 累计分配的内存/对象数 | 排查频繁分配导致的 GC 压力 |
|
||||
|
||||
```bash
|
||||
# 查看当前活跃的内存占用
|
||||
go tool pprof -top -sample_index=inuse_objects app heap.pprof
|
||||
|
||||
# 查看累计分配量
|
||||
go tool pprof -top -sample_index=alloc_objects app heap.pprof
|
||||
```
|
||||
|
||||
理解这两个指标的区别是关键的:
|
||||
|
||||
```
|
||||
Allocated: 1GB total (all allocations ever made)
|
||||
In Use: 50MB currently alive
|
||||
Freed: 950MB already collected by GC
|
||||
```
|
||||
|
||||
- **alloc_space 高 + inuse_space 低** = 正常,GC 在正常工作,大量短命对象已回收
|
||||
- **inuse_space 持续增长** = 可能的内存泄漏,需要检查是谁持有了引用
|
||||
|
||||
```go
|
||||
func demonstrateHeapProfile() {
|
||||
// 一次性分配大对象
|
||||
big := make([]byte, 1<<20) // 1MB
|
||||
|
||||
// 大量小对象分配
|
||||
var smalls []string
|
||||
for i := 0; i < 100000; i++ {
|
||||
smalls = append(smalls, fmt.Sprintf("item-%d", i))
|
||||
}
|
||||
|
||||
// 大对象释放
|
||||
big = nil
|
||||
|
||||
// 小对象仍在使用 → alloc 和 inuse 都反映这部分
|
||||
}
|
||||
```
|
||||
|
||||
### Block Profile — Goroutine 阻塞分析
|
||||
|
||||
block profile 测量 goroutine 在哪些地方等待了最长时间(channel 收发、mutex 锁等):
|
||||
|
||||
```go
|
||||
// 必须显式启用,默认关闭
|
||||
runtime.SetBlockProfileRate(1) // 每次阻塞事件采样一次
|
||||
```
|
||||
|
||||
```bash
|
||||
go tool pprof -top app block.pprof
|
||||
# 输出示例:
|
||||
# flat flat% sum% cum cum%
|
||||
# 12.3s 61.5% 61.5s 12.3s 61.5% runtime.chanrecv
|
||||
# 5.2s 26.0% 87.5s 5.2s 26.0% sync.runtime_SemacquireMutex
|
||||
```
|
||||
|
||||
> [!WARNING] Block Profile 的性能开销
|
||||
> `SetBlockProfileRate(N)` 会让每个阻塞事件有 1/N 的概率被采样。设为 1 意味着全部采样,对性能影响较大。生产环境建议使用较小值如 100 或 1000。
|
||||
|
||||
### Mutex Profile — 锁竞争分析
|
||||
|
||||
mutex profile 测量哪些锁被争用最多、goroutine 等待锁的时间最长:
|
||||
|
||||
```go
|
||||
// 启用 mutex profiling(默认关闭)
|
||||
runtime.SetMutexProfileFraction(1) // 1 = 每次锁竞争都采样
|
||||
```
|
||||
|
||||
```bash
|
||||
go tool pprof -top app mutex.pprof
|
||||
# 输出示例:
|
||||
# flat flat% sum% cum cum%
|
||||
# 8.7s 87.0% 87.0s 8.7s 87.0% main.processData.func1
|
||||
# 1.3s 13.0% 100.0s 1.3s 13.0% main.cacheLookup
|
||||
```
|
||||
|
||||
> [!TIP] 实战技巧
|
||||
> 如果发现大量 goroutine 在同一个锁上等待,说明存在严重的锁竞争。可以考虑:
|
||||
> 1. 缩小临界区范围
|
||||
> 2. 使用 RWMutex 替代 Mutex(读多写少时)
|
||||
> 3. 使用分片锁(sharded lock)分散竞争
|
||||
|
||||
### Pprof Web UI 常用操作
|
||||
|
||||
```bash
|
||||
go tool pprof -http=:8080 app.pprof
|
||||
```
|
||||
|
||||
打开浏览器访问 `http://localhost:8080` 后:
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **Graph** | 以 DOT/Graphviz 格式显示调用关系图 |
|
||||
| **List** | 列出源代码级别的函数耗时分布 |
|
||||
| **Top** | 表格形式按指标排序 |
|
||||
| **Flame graph** | 火焰图展示调用栈的深度分布(需要 graphviz) |
|
||||
| **Search** | 在图中搜索特定函数 |
|
||||
| **Focus/Ignore** | 聚焦或忽略某些函数路径 |
|
||||
|
||||
> [!TIP] Flame graph 阅读要领
|
||||
> 火焰图的宽度表示该函数消耗的 CPU 时间占比,高度表示调用深度。顶层窄而高的柱子通常是需要优化的热点函数。
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 在 Web 服务中暴露 pprof 端点
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof" // 注册 /debug/pprof/* 路由
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Hello!")
|
||||
})
|
||||
|
||||
log.Println("Server starting on :8080")
|
||||
log.Println("PPROF available at :6060/debug/pprof/")
|
||||
|
||||
go func() {
|
||||
log.Fatal(http.ListenAndServe(":6060", nil))
|
||||
}()
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
```
|
||||
|
||||
导入 `_ "net/http/pprof"` 后自动注册以下端点:
|
||||
- `/debug/pprof/profile` — CPU profile(30 秒)
|
||||
- `/debug/pprof/heap` — Heap profile
|
||||
- `/debug/pprof/goroutine` — Goroutine 信息
|
||||
- `/debug/pprof/block` — Block profile
|
||||
- `/debug/pprof/mutex` — Mutex profile
|
||||
|
||||
### 分析 goroutine 泄露
|
||||
|
||||
```go
|
||||
func checkGoroutineLeak() {
|
||||
f, _ := os.Create("goroutine.pprof")
|
||||
defer f.Close()
|
||||
pprof.WriteGoroutineProfile(f)
|
||||
|
||||
// 或者获取堆 profile 中的 goroutine 信息
|
||||
p := pprof.Lookup("goroutine")
|
||||
p.WriteTo(os.Stdout, 0)
|
||||
}
|
||||
```
|
||||
|
||||
当某个 goroutine 数量持续增加不下降时,结合 `go tool pprof -top -nodecount=20 goroutine.pprof` 可查看哪些 goroutine 类型在堆积。
|
||||
|
||||
## 实践场景
|
||||
|
||||
### 面试高频问题
|
||||
|
||||
**Q: CPU profile 显示某个函数 flat 很高但它是标准库函数怎么办?**
|
||||
先确定它是否在业务代码中被频繁调用。如果是 standard library 且被你的代码频繁调用,考虑是否有更高效的替代方案(比如 `strconv.Itoa` vs `fmt.Sprintf`)。如果标准库内部有优化空间,提交 issue。
|
||||
|
||||
**Q: Heap profile 显示大量 string 分配该如何处理?**
|
||||
字符串在 Go 中是不可变对象,难以复用。常见优化策略:
|
||||
1. 用 `[]byte` + `unsafe.String()` (Go 1.20+)避免拷贝
|
||||
2. 使用 `bytes.Buffer` 代替频繁的 `fmt.Sprintf`
|
||||
3. 对于网络传输,预分配 buffer pool(sync.Pool)
|
||||
|
||||
**Q: 如何区分内存泄漏和正常的高内存占用?**
|
||||
对比 `inuse_objects` 和 `alloc_objects`:如果 inuse 持续增长而 alloc 趋于稳定,大概率是泄漏;如果两者都很高但 inuse 相对稳定,可能是应用本身的正常高内存需求。
|
||||
|
||||
### 实战排查流程
|
||||
|
||||
1. **确认症状**:CPU 飙高?内存不足?延迟增加?
|
||||
2. **采集 profile**:根据症状选择对应类型(cpu / heap / block / mutex)
|
||||
3. **看 Top 列表**:找出消耗最大的前几个函数
|
||||
4. **看 Tree/Graph**:理解调用链中是哪个环节出了问题
|
||||
5. **定位源码**:用 List 模式查看具体行号
|
||||
6. **修复后验证**:重新采集 profile 确认改善
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- [[三色标记GC原理]] — GC 的频率和停顿直接影响 heap profile 的表现
|
||||
- [[Goroutine 调度模型]] — goroutine leak 会导致调度器负担加重,间接影响 CPU profile
|
||||
@@ -0,0 +1,241 @@
|
||||
---
|
||||
tags: [go/lang, gc, tri-color-marking, write-barrier, pacer]
|
||||
create time: 2026-08-08 19:00
|
||||
update time: 2026-08-08 19:00
|
||||
---
|
||||
|
||||
# 三色标记 GC 原理
|
||||
|
||||
## 概述
|
||||
|
||||
Go 的垃圾回收器(GC)是 Go 语言高性能的核心支柱之一。从 v1.5 引入的三色标记清除算法到 v1.9 完善的混合写屏障,每一次演进都在压缩 STW(Stop-The-World)时间、提升并发效率。理解 GC 的工作原理,不仅能在面试中从容应对底层细节问题,更能指导写出低 GC 压力的代码。
|
||||
|
||||
> [!NOTE] 为什么需要三色标记?
|
||||
> 传统标记阶段必须暂停所有 goroutine(STW),如果标记期间对象引用关系发生变化(如 A 指向 B,然后 A 不再指向 B 但其他白色对象还指向 B),可能导致被错误回收。三色标记用颜色抽象解决了这个问题。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 三种颜色的含义
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
W["白色 White<br/>未扫描"] -->|"被扫描后"| G["灰色 Gray<br/>已发现但未扫描子节点"]
|
||||
G -->|"扫描完所有子节点后"| B["黑色 Black<br/>完全扫描过"]
|
||||
|
||||
style W fill:#f5f5f5,stroke:#9e9e9e,color:#000
|
||||
style G fill:#bbdefb,stroke:#1565c0,color:#000
|
||||
style B fill:#a5d6a7,stroke:#2e7d32,color:#000
|
||||
```
|
||||
|
||||
| 颜色 | 含义 | GC 行为 |
|
||||
|------|------|--------|
|
||||
| **白色** | 未被标记访问过的对象 | 可能被回收 |
|
||||
| **灰色** | 已被发现,但其引用的对象尚未扫描 | 加入扫描队列 |
|
||||
| **黑色** | 已完成扫描且其引用的对象也都是黑色或灰色 | 不可回收,不会再次出现白色引用 |
|
||||
|
||||
关键不变性:**黑色对象永远不会直接引用白色对象**。这保证了任何可达对象都被正确标记。
|
||||
|
||||
### 扫描流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["STW: 标记根集<br/>所有全局变量/栈上的指针"] --> B["根对象标为灰色<br/>放入扫描队列"]
|
||||
|
||||
B --> C{"扫描队列空?"}
|
||||
C -->|否| D["取出一个灰色对象<br/>扫描它的所有字段"]
|
||||
D --> E["发现的白色子节点<br/>→ 标为灰色并入队"]
|
||||
E --> F["当前对象标为黑色"]
|
||||
F --> C
|
||||
|
||||
C -->|是| G["触发下一轮 STW<br/>准备进入清除阶段"]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
style D fill:#fff3e0
|
||||
style G fill:#e8f5e9
|
||||
```
|
||||
|
||||
每个被取出的灰色对象会被完整扫描:遍历其内存中的所有指针字段,对每个指针对象执行:
|
||||
|
||||
```go
|
||||
if obj.isWhite() {
|
||||
obj.color = gray // 从未发现变为已发现
|
||||
addScanQueue(obj) // 加入待扫描队列
|
||||
} else if obj.isGray() {
|
||||
whiteObjectDelta++ // 记录灰色对象的数量变化
|
||||
}
|
||||
// 自身标记为 black
|
||||
obj.color = black
|
||||
```
|
||||
|
||||
> [!TIP] 面试常考点
|
||||
> Go GC 的扫描不是传统的标记阶段。标记和清除是交错进行的——当用户代码运行的同时,后台也有 goroutine 在执行 GC 扫描工作。这被称为"并发标记"。
|
||||
|
||||
### 混合写屏障(Hybrid Write Barrier)
|
||||
|
||||
这是 Go GC 最核心的创新之一。写屏障确保在并发标记期间,即使对象的引用关系被修改,也不会导致可达对象被误回收。
|
||||
|
||||
Go 在 v1.8 引入白色前置写屏障,v1.9 完善为混合写屏障(白色后置 + 白色前置):
|
||||
|
||||
```go
|
||||
// 混合写屏障伪代码
|
||||
func storePointer(p *unsafe.Pointer, val unsafe.Pointer) {
|
||||
old := *p
|
||||
new := val
|
||||
|
||||
// 白色前置屏障
|
||||
if isWhite(old) {
|
||||
markWhiteObject(old) // 将旧值重新标灰
|
||||
}
|
||||
|
||||
// 真正的赋值
|
||||
*p = new
|
||||
|
||||
// 白色后置屏障
|
||||
if isWhite(new) && isGray(gcWorker) {
|
||||
markGrayObject(new) // 将新值标灰
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
两种屏障的组合确保了即使在并发修改的情况下:
|
||||
- **白色前置**:保证被取消引用的旧白色对象不会被漏扫
|
||||
- **白色后置**:保证新引用的白色对象会被纳入扫描
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as "用户代码 (Mutator)"
|
||||
participant WB as "写屏障"
|
||||
participant GC as "GC 扫描线程"
|
||||
|
||||
M->>WB: ptr = newObject(white)
|
||||
WB->>GC: 白色后置: new → gray
|
||||
GC->>GC: 从队列取出 new 扫描
|
||||
|
||||
M->>WB: ptr = anotherObject(white)
|
||||
Note over WB: 旧值被移除
|
||||
WB->>GC: 白色前置: old → gray
|
||||
GC->>GC: 从队列取出 old 扫描
|
||||
|
||||
GC->>GC: 继续扫描...
|
||||
```
|
||||
|
||||
> [!WARNING] 为什么叫"混合"写屏障?
|
||||
> 纯前置屏障只需要在赋值前检查旧值,简单但无法处理新值的情况;纯后置屏障则相反。混合方案结合了两者的优点,同时保证不遗漏任何应该被扫描的对象。代价是每个指针存储操作多了两次颜色检查。
|
||||
|
||||
### STW 阶段
|
||||
|
||||
整个 GC 周期包含若干 STW 阶段,Go 的目标是让它们尽可能短:
|
||||
|
||||
| 阶段 | Go 版本 | 作用 | STW 时长目标 |
|
||||
|------|---------|------|-------------|
|
||||
| 初始 STW | v1.5+ | 标记根集为灰色,启动后台扫描器 | < 1ms |
|
||||
| 终止 STW (v1.8 之前) | v1.5-v1.8 | 配合单纯写屏障 | < 1ms |
|
||||
| 终止 STW (混合屏障后) | v1.9+ | 完成最后一批对象的标记 | ~微秒级 |
|
||||
| 清除 STW | v1.5+ | 重置 freed 对象的白色状态 | ~几毫秒 |
|
||||
|
||||
Go 1.8 之后,除了初始化时的根集扫描和结束时的少量清理外,大部分 GC 工作与用户代码并行运行。
|
||||
|
||||
### Pacer 算法与触发阈值
|
||||
|
||||
Go 使用一个称为 Pacer 的反馈控制系统来决定何时触发 GC:
|
||||
|
||||
```
|
||||
Pacer(t) = GC_time / Wall_time - target_fraction
|
||||
|
||||
触发条件:
|
||||
heap_live >= last_heap_live * 1.44^(Pacer调整系数)
|
||||
```
|
||||
|
||||
核心参数:
|
||||
- **GCCyCleTargetRatio**: 默认 0.1(即 10%)。控制每轮 GC 占 CPU 时间的比例上限
|
||||
- **HeapGoal**: `heap_live` 增长 44% 时触发一轮 GC(e^0.37 ≈ 1.44)
|
||||
|
||||
这意味着 GC 触发频率自适应:堆越大、分配越快,GC 越频繁。Go 通过指数增长的阈值来平滑 GC 触发的节奏。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["堆内存增长"] -->|"达到 threshold × 1.44"| B["触发 GC"]
|
||||
B --> C["STW: 标记根集"]
|
||||
C --> D["并发标记: 后台 goroutine 扫描"]
|
||||
D --> E["并发清除: 释放白色对象"]
|
||||
E --> F["更新 heap_live"]
|
||||
F --> A
|
||||
|
||||
style B fill:#ffebee
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
### Go GC 演进历史
|
||||
|
||||
| 版本 | 关键改进 | 突破点 |
|
||||
|------|---------|--------|
|
||||
| v1.5 | 引入三色标记 + 并发标记 | 首次实现用户代码与 GC 并行 |
|
||||
| v1.8 | 引入白色前置写屏障 | 支持更灵活的并发策略 |
|
||||
| v1.9 | 混合写屏障 + 改进 Pacer | STW 时间大幅缩减至可忽略级别 |
|
||||
|
||||
> [!TIP] 面试加分项
|
||||
> 提到 Go 采用增量式 GC(incremental GC)而非全量 STW 标记,并且通过写屏障处理了并发场景下的引用一致性问题是证明你对 GC 有深入理解的标志。
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 减少 GC 压力的预分配技巧
|
||||
|
||||
```go
|
||||
// ❌ GC 压力大:每次循环都创建新的临时切片
|
||||
func processItems(items []Item) []Result {
|
||||
var results []Result
|
||||
for _, item := range items {
|
||||
results = append(results, transform(item)) // 多次扩容 + GC
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ✅ GC 友好:一次性预分配
|
||||
func processItemsOptimized(items []Item) []Result {
|
||||
results := make([]Result, len(items)) // 零额外分配
|
||||
for i, item := range items {
|
||||
results[i] = transform(item)
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
预分配切片的价值在于:不仅避免了多次扩容的拷贝开销,更重要的是减少了 GC 需要跟踪的中间对象数量。在高吞吐场景中,这点优化可能带来显著的性能差异。
|
||||
|
||||
### 主动触发 GC(通常不需要)
|
||||
|
||||
```go
|
||||
runtime.GC() // 立即触发一次完整的 GC
|
||||
stats := runtime.MemStats{}
|
||||
runtime.ReadMemStats(&stats)
|
||||
fmt.Printf("heap alloc: %d bytes\n", stats.HeapAlloc)
|
||||
```
|
||||
|
||||
大多数应用不应该主动调用 `runtime.GC()`——Go 的 Pacer 已经做得足够好。手动触发只适合 benchmark 或调试场景。
|
||||
|
||||
## 实践场景
|
||||
|
||||
### 面试高频问题
|
||||
|
||||
**Q: 什么时候会触发 GC?**
|
||||
当 `heap_live` 相较于上一轮 GC 结束时增长了约 44% 时触发。这个比例由 Pacer 动态调整。
|
||||
|
||||
**Q: 如何降低 GC 压力?**
|
||||
- 预分配已知大小的容器(map/slice),避免扩容
|
||||
- 复用对象(sync.Pool)
|
||||
- 减少短生命周期对象的创建(逃逸到堆上会增加 GC 负担)
|
||||
- 使用指针数组而非接口类型(消除间接引用)
|
||||
|
||||
**Q: STW 和 CTW 的区别?**
|
||||
STW (Stop-The-World) 是暂停所有用户 goroutine。CTW (Concurrent The-World) 是 Go 的特色——大部分 GC 工作与用户代码并发运行。Go 1.8 之后几乎没有真正意义上的 CTW。
|
||||
|
||||
### 实战建议
|
||||
|
||||
- **关注 Pprof heap profile**:定期审查哪些对象占用了最多的堆空间并存活时间过长
|
||||
- **使用 `GOGC=100` 调低 GC 频率**:高延迟敏感场景下可以让堆增长更多再触发 GC
|
||||
- **使用 `GOGC=10` 提高 GC 频率**:内存受限环境下减少峰值占用
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- [[Pprof 性能分析指南]] — pprof 中的 heap profile 可直接观察 GC 产生的对象分布
|
||||
- [[Goroutine 调度模型]] — GC 扫描工作由独立的 GC worker goroutine 执行
|
||||
Reference in New Issue
Block a user