Git Internals

对象可达性与垃圾回收

理解对象为什么有时还能恢复、有时会永久丢失,本质上取决于可达性和垃圾回收,而不只是命令本身。

适合谁看
  • 想建立稳定 Git 心智模型的学习者
  • 经常遇到历史、引用、恢复问题的开发者
前置知识
  • 会看基础命令输出
  • 知道提交、分支、HEAD 这些名词
常见风险
  • 只背底层术语却不连接到实际命令
  • 把对象、引用、工作区混成一层理解

对象“还在仓库里”和“你还容易找到它”不是一回事。

先把"可达"理解成什么

对象可达性与垃圾回收只要对象还能从分支、tag 或 reflog 等入口追踪到,它就是可达的。失去引用的对象会成为 unreachable 对象,最终被 gc 清理。
历史引用链
HEAD@{3}HEAD@{2}HEAD@{1}当前 HEAD
救援入口
rescue/recover

Git 内部保存的是对象:blob、tree、commit、tag。
这些对象不是平铺地放在那里,而是通过引用和对象之间的指向关系串起来。

如果一个对象还能从这些入口被一路追踪到,它通常就是可达的:

  • 分支引用,比如 refs/heads/main
  • 标签引用,比如 refs/tags/v1.0.0
  • HEAD
  • 远端跟踪引用
  • reflog 记录里的历史位置

你可以把它想成一张图:

  • 入口还在
  • 入口指向某个提交
  • 提交再指向它的 tree、父提交等对象
  • 那么这些对象就仍然在“可达链路”上

为什么可达性这么重要

Git 并不是靠“你还记不记得这个提交的 SHA”来判断对象该不该保留,而是更关心:

  • 这个对象是否还被当前历史结构引用
  • 是否还能从某个已知入口走到它

这直接影响两件事:

  1. 你还能不能比较容易地找回它
  2. Git 垃圾回收时会不会考虑清理它

所以很多恢复问题,本质都不是“某个命令太危险”,而是“这个对象现在还可达吗”。

为什么误操作后经常还能恢复

很多人第一次执行下面这些操作后会以为“历史被删了”:

  • git reset --hard
  • git rebase
  • git commit --amend
  • git branch -D

但在很多情况下,真正发生的是:

  • 某个分支引用被改到了新位置
  • 原来的提交暂时失去了最明显的名字
  • 但旧提交对象并没有立刻消失

也就是说,先消失的往往是“入口名称”,不是底层对象本身。

只要旧对象还能够通过 reflog 或其他引用链被找到,它通常就还有恢复机会。

reflog 为什么常常是救命工具

很多“已经没了”的提交,实际上是从正常分支历史里掉出去了,但还留在 reflog 里。

reflog 记录的是:

  • 某个引用过去指向过哪里
  • 它是怎样一步步移动过来的

这意味着即使当前 main 已经不再指向旧提交,只要 reflog 里还记得过去的位置,你通常还能重新找到那个旧提交,再:

  • 新建一个分支
  • 做一次 reset
  • 或者手动 cherry-pick 回来

所以恢复时一个很常见的判断顺序是:

  1. 先看引用还在不在
  2. 不在就看 reflog
  3. 再决定是恢复引用、恢复分支,还是只摘回其中几个提交

垃圾回收到底在做什么

git gc 的目标不是“随机删除旧东西”,而是整理和清理:

  • 打包对象,减少存储碎片
  • 优化对象查找效率
  • 清理长时间不可达、且不再需要保留的对象

这也是为什么垃圾回收和“仓库瘦身”有关,但同时又影响恢复窗口。

从原理上说:

  • 可达对象通常会被保留
  • 不可达对象不会立刻清掉
  • 但如果它长期不可达,又超过了一些保留期限,就更可能被清理

用例 1:为什么 reset 后还能找回

假设你在 main 上做了几次提交,然后执行:

git reset --hard HEAD~2

表面上看,最近两个提交“没了”。
实际上往往只是:

  • main 指针往回移动了
  • 那两个提交暂时不再被 main 指向

如果 reflog 还保留着旧位置,那么这两个提交仍然很可能能找回。

用例 2:为什么 amend 后旧提交还在一段时间

git commit --amend 并不是原地修改原来的 commit。
它通常会生成一个新的 commit,然后让当前分支指向新的对象。

旧的 commit 如果没有其他引用继续指向,就会变成“不再从当前分支可达”。
但它不会马上消失,所以你在发现 amend 改错时,往往还有恢复空间。

用例 3:为什么删分支后不一定立刻丢历史

删除分支,本质上是删掉一个引用。
如果这个分支上的提交没有被其他分支、tag 或 reflog 继续指向,它们会变得更难找到。

但只要垃圾回收还没把这些长期不可达对象清理掉,就仍然可能恢复。

这也是为什么“删分支了”不一定等于“提交永久消失了”。

特殊情况:不可达不等于立刻不存在

这是最容易误解的地方之一。

  • 可达:更稳定,更容易恢复
  • 不可达:更危险,但通常不是即时删除

很多 Git 教程会把这个过程讲得过于简单,导致用户以为“命令一执行,对象就物理消失”。
实际情况通常更像:

  • 先失去入口
  • 再进入一段可能还能恢复的窗口
  • 之后才可能随着垃圾回收被真正清理

特殊情况:恢复窗口和仓库维护策略有关

不同仓库的配置和维护节奏不同:

  • reflog 保留多久
  • gc 什么时候触发
  • prune 规则怎么配

这些都会影响“还能不能找回”。
所以在团队环境里,恢复能力不只是个人命令技巧,也和仓库维护策略有关。

常见误解

“只要我还记得 SHA,这个对象就永远安全”

不对。
如果对象长期不可达,并且已经被垃圾回收清理,知道 SHA 也没法把不存在的对象变回来。

“reset 会把对象立即删除”

也不对。
reset 更常见的效果是移动引用,而不是马上删除底层对象。

“gc 很危险,所以最好永远别跑”

也不准确。
git gc 是 Git 正常维护的一部分。真正危险的是在还没确认恢复需求前,就放任不可达对象过久,或者不了解 reflog / gc 对恢复窗口的影响。

这篇原理对命令理解有什么帮助

理解可达性之后,你会更容易判断这些问题:

  • 为什么 reflog 能救回很多误操作
  • 为什么 branch 删除后有时还能恢复
  • 为什么 amend、rebase、reset 会制造“旧提交还在但分支名不见了”的情况
  • 为什么恢复要尽快做
  • 为什么有些历史拖久了就真的找不回来了

建议连着看

建议和这些内容一起看:

  • git reflog
  • git reset
  • git fsck
  • git gc
  • git prune