← 返回 Git内部原理图解 目录

📸 Git对象与存储图解

.git目录结构 + 4种对象(blob/tree/commit/tag) + SHA-1 + 对象图 · SVG可视化

① .git目录结构

Git项目的所有秘密都藏在 .git/ 目录里。把它想象成一个照片馆的档案室——你平时只管拍照(commit),但所有底片、相册、标注都自动归档在这里。
.git/ 档案室目录结构 .git/ objects/ 底片库(所有对象) refs/ 指针簿(分支/标签) HEAD 当前指向(你正看哪) objects/ 底片库详解 ab/ (前2字符=目录) 3f/ (SHA-1前2字符) e7/ ... → ab/cd1234... (38字符) → 3f/8e9a2b... (38字符) → e7/4d2c1f... (38字符) 每个文件=一个Git对象(zlib压缩) refs/ 指针簿详解 heads/main → commit SHA heads/feature → commit SHA tags/v1.0 → commit SHA remotes/origin/main → SHA index (暂存区快照) config (仓库配置) logs/ (操作日志) 🔑 核心理解 Git = 内容寻址文件系统。一切皆对象,对象用SHA-1命名,引用用指针指向对象
.git目录 = Git的整个"档案室",objects存底片,refs存指针,HEAD标记当前位置
terminal — ls -la .git/
📁 .git/
objects/
refs/
HEAD
index
config
# 查看.git目录结构 $ ls -la .git/ total 24 drwxr-xr-x 12 new staff 384 Jun 14 10:00 . drwxr-xr-x 5 new staff 160 Jun 14 10:00 .. drwxr-xr-x 13 new staff 416 Jun 14 10:00 objects # ← 所有对象(底片库) drwxr-xr-x 4 new staff 128 Jun 14 10:00 refs # ← 分支/标签指针 -rw-r--r-- 1 new staff 23 Jun 14 10:00 HEAD # ← 当前位置 -rw-r--r-- 1 new staff 137 Jun 14 10:00 config # ← 仓库配置 -rw-r--r-- 1 new staff 73 Jun 14 10:00 index # ← 暂存区 drwxr-xr-x 2 new staff 64 Jun 14 10:00 hooks drwxr-xr-x 2 new staff 64 Jun 14 10:00 info drwxr-xr-x 4 new staff 128 Jun 14 10:00 logs # ← 操作日志 -rw-r--r-- 1 new staff 41 Jun 14 10:00 ORIG_HEAD # ← merge前的HEAD
.git/objects/ 是Git最核心的目录。Git用SHA-1哈希的前2字符作为子目录名,后38字符作为文件名。比如对象 abc123... 存在 objects/ab/c123...。这样做是为了避免单个目录下文件太多导致性能问题。

② 四种Git对象

Git只有4种对象,就像照片馆只有4种存档方式:
blob = 照片底片(文件内容,不含文件名!)
tree = 相册(记录哪些底片在哪个位置,含文件名)
commit = 照片标注(谁拍的、什么时候、拍了什么)
tag = 相册封面标签(给某个标注起个响亮的名字)
四种Git对象关系图 commit 对象 📸 照片标注 tree: 3f8e9a... parent: abcd12... author/date/message tree 对象 📔 相册 blob README.md → e74d... tree src/ → 5a1b... blob pom.xml → 9c2f... tree src/ 📁 子相册 blob App.java → f3a1... blob 🎞️ 底片(纯内容) "Hello World" blob 🎞️ 底片(纯内容) <project>...</project> blob 🎞️ 底片(纯内容) public class App... tag 对象 🏷️ 封面标签 object: 3f8e9a... tag name: v1.0 tree指针 blob指针 子目录tree 指向commit
commit → tree → blob 的引用链,就像 标注→相册→底片

四种对象详解

对象类型类比存储内容关键特征
blob🎞️ 底片文件内容(纯字节)不含文件名!同名内容=同一个blob
tree📔 相册文件名+blob/tree的SHA一个tree可以引用blob和子tree
commit📸 照片标注tree SHA+parent+作者+消息指向一个tree,记录快照元数据
tag🏷️ 封面标签对象SHA+标签名+消息轻量标签只是引用,附注标签是对象
blob不含文件名!这是一个极易误解的点。blob只存内容,文件名存在tree里。这意味着:如果两个文件内容完全一样(即使文件名不同),Git只存一个blob。这就是Git省空间的秘密——内容去重。
terminal — 查看Git对象内容
📁 .git/
📁 objects/
3f/8e9a2b...
ab/cd1234...
# 查看对象类型 $ git cat-file -t 3f8e9a commit # 查看对象内容(commit) $ git cat-file -p 3f8e9a tree 5a1b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b parent abcd1234ef567890123456789012345678901234 author 张三 <zhang@example.com> 1718342400 +0800 committer 张三 <zhang@example.com> 1718342400 +0800 feat: 添加用户注册功能 # 查看tree对象 $ git cat-file -p 5a1b3c4d 100644 blob e74d2c1f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d README.md 040000 tree 9c2f4a6b8c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f src 100644 blob f3a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9 pom.xml # 查看blob对象 $ git cat-file -p e74d2c1f # My Project Hello World

③ SHA-1计算

SHA-1就像给每张底片生成一个唯一指纹。同样的内容,指纹永远一样;内容改哪怕一个字节,指纹就完全不同。Git靠这个指纹来"认人"——不需要文件名,靠指纹就能找到对象。
SHA-1计算过程 输入:对象头 + 内容 "blob 12\0Hello World" 类型 + 空格 + 大小 + \0 + 内容 SHA-1 输出:40位十六进制 e74d2c1f3a4b5c6d... 前2位=目录 / 后38位=文件名 ✅ "Hello World" → e74d2c1f... (永远一样) ✅ "Hello World" → e74d2c1f... (再算还是一样) ❌ "Hello World" → e74d2c1f... ❌ "Hello World!" → a1b2c3d4... (只多一个!就变了) 🔑 SHA-1 = 160位 = 2¹⁶⁰种可能。Git依赖它做内容寻址,碰撞概率极低(比被陨石砸中还低) SHA-1已被Google碰撞实验攻破,但Git增加了SHA-256支持(实验性),目前SHA-1对代码仓库仍安全
SHA-1计算:对象头(类型+大小)+内容 → 40位哈希 → 前2位做目录,后38位做文件名
terminal — 手动计算SHA-1
📁 demo/
hello.txt
# 创建测试文件 $ echo "Hello World" > hello.txt $ git hash-object hello.txt 3b18e512dba79e4c8300dd08aeb37f8e728b8dad # 手动计算(加上对象头) $ printf "blob 12\0Hello World" | openssl sha1 3b18e512dba79e4c8300dd08aeb37f8e728b8dad # ← 一样! # 把文件写入对象库 $ git hash-object -w hello.txt 3b18e512dba79e4c8300dd08aeb37f8e728b8dad # 现在objects/里有了这个对象 $ find .git/objects -type f .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad # ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # 前2位=目录 后38位=文件名
Git的SHA-1不仅算内容,还算对象头(类型+空格+大小+\0)。所以即使内容相同的blob和commit,SHA-1也不同。这也是为什么blob和commit不会"撞车"。

④ 对象图与引用链

把Git仓库想象成一面照片墙:每张照片(commit)贴在墙上,用线连着它拍的相册(tree),相册里夹着底片(blob)。分支(branch)就是贴在墙上的一根红绳,指向某张照片。HEAD就是你手里拿的激光笔,照亮你正在看的那张。
对象图:3次提交的引用链 commit C3 tree: 5a1b3c parent: abcd12 commit C2 tree: 7c8d9e parent: ef0123 commit C1 tree: 4f5a6b parent: (无) parent parent tree 5a1b3c README + src/ + App.java tree 7c8d9e README + src/ tree 4f5a6b README blob README 3个tree共享同一个blob! HEAD refs/heads/main refs/tags/v1.0 🔑 引用链:HEAD → branch → commit → tree → blob(分支只是指针,移动分支=移动指针)
整个Git的引用链:HEAD→分支指针→commit→tree→blob,所有"指针"都只是存SHA-1的文本文件
分支的真相refs/heads/main 这个文件里只存了一个40字符的SHA-1!创建分支就是在 refs/heads/ 下新建一个文件,写入当前commit的SHA-1。所以Git创建分支是O(1)操作——超快!不像SVN要复制整个目录。
terminal — 验证分支只是指针
📁 .git/refs/
📁 heads/
main
feature
📁 tags/
v1.0
# HEAD文件 —— 就一行文本 $ cat .git/HEAD ref: refs/heads/main # ← 指向main分支 # main分支 —— 就一个SHA-1 $ cat .git/refs/heads/main 3f8e9a2b1c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f # 创建新分支 —— 就是在refs/heads/下新建一个文件 $ git branch feature $ cat .git/refs/heads/feature 3f8e9a2b1c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f # ← 和main一样! # 切换分支 —— 修改HEAD指向 $ git checkout feature $ cat .git/HEAD ref: refs/heads/feature # ← HEAD现在指feature

⑤ 存储原理(zlib压缩)

Git存对象就像把底片真空压缩——原始底片可能很大,但用zlib一压缩就变很小。解压时再恢复原样。Git不会自动删除不用的对象(就像照片馆不会随便扔底片),需要你手动 git gc 打扫。
对象存储与读取流程 写入流程 (git add → commit) 原始内容 "Hello World" 加对象头 "blob 12\0Hello World" zlib压缩 二进制压缩数据 写入objects/ ab/cd1234... 读取流程 (git show / diff) SHA-1哈希 abcd1234ef... 定位文件 objects/ab/cd1234... zlib解压 "blob 12\0Hello World" 去掉对象头 "Hello World"
写入:内容→加对象头→zlib压缩→存入objects/;读取:SHA-1→定位文件→zlib解压→去对象头

Git GC(垃圾回收)

Git不会自动删除不再被引用的对象。当你删除分支、rebase、amend时,旧对象变成了悬空对象(dangling),但还在objects/里。

1. 找出可达对象 2. 打包成packfile 3. 删除松散对象 4. 清理空目录
# 手动触发GC $ git gc # 查看悬空对象 $ git fsck --unreachable # GC做了什么 # 1. 把 objects/ 下的松散文件 → 打包成 .git/objects/pack/pack-xxx.idx + .pack # 2. packfile用delta压缩:相似对象只存差异,极大节省空间 # 3. 删除不再被引用的松散对象
git gc --prune=now 会立即删除所有不可达对象。如果你刚做了 git reset --hard 想恢复,千万别跑gc!因为GC会删掉你还能找回的悬空commit。先 git reflog 找回,再GC。

⑥ 面试速答

Git的4种对象是什么?各自存什么?

blob:存文件内容(不含文件名),相同内容共享同一个blob。
tree:存目录结构(文件名+blob/tree的SHA),相当于一个目录快照。
commit:存一次提交的元数据(tree指针、parent指针、作者、时间、消息)。
tag:附注标签是对象,存指向的对象SHA+标签名+消息;轻量标签只是引用文件。

Git的SHA-1是怎么算的?为什么不会冲突?

SHA-1的输入 = "对象类型 空格 内容大小\0内容"(注意有对象头!不是只算内容)。
输出是40位十六进制(160位)。SHA-1碰撞概率极低(约2⁸⁰次运算才可能碰撞),对代码仓库来说足够安全。Git已在实验SHA-256支持。

Git分支的本质是什么?为什么创建分支那么快?

分支的本质就是一个40字节的文本文件(存在 .git/refs/heads/ 下),里面只存一个commit的SHA-1。
创建分支 = 新建一个文件写入当前commit的SHA-1,时间复杂度O(1),所以几乎瞬间完成。这和SVN的"复制整个目录"完全不同。

blob对象为什么不存文件名?有什么好处?

blob只存内容,文件名存在tree对象里。这样做的核心好处是内容去重:两个文件内容完全一样(即使文件名不同),Git只存一个blob。这在大型仓库里能节省大量空间,特别是复制文件、重命名文件的场景。

.git/objects里的文件为什么用前2位SHA-1做目录?

这是为了避免单个目录下文件过多导致文件系统性能下降。SHA-1有16²=256种前2位组合,相当于把对象均匀分散到256个子目录。Linux文件系统(EXT4)在单个目录下文件超过几万时,查找速度会明显下降。Git这种设计即使在百万对象的大仓库里也能保持高效。

git gc做了什么?什么时候需要手动执行?

Git GC做了3件事:
1. 把松散对象(loose objects)打包成packfile(使用delta压缩,只存对象间差异)
2. 删除不可达的悬空对象
3. 清理空目录
通常Git会自动GC(默认超过700个松散对象时触发)。需要手动执行的场景:仓库空间占用异常大、clone后优化、删除大量分支后清理。

packfile是什么?和松散对象有什么区别?

松散对象:每个对象一个文件(zlib压缩),存放在 objects/xx/yyyy...
packfile:多个对象打包到一个 .pack 文件,用delta压缩——只存对象之间的差异,不存完整内容。配合 .idx 索引文件快速查找。
GC会把松散对象打包成packfile。clone/pull时Git也会优先传输packfile,减少网络传输量。

git cat-file命令怎么用?能做什么?

git cat-file 是Git底层命令,直接操作对象:
git cat-file -t <SHA> — 查看对象类型(blob/tree/commit/tag)
git cat-file -p <SHA> — 查看对象内容(美化的)
git cat-file -s <SHA> — 查看对象大小
这是理解Git内部的"透视镜",面试时提到cat-file能展示你对Git底层的深入理解。

不小心reset --hard了,还能恢复吗?

能!用 git reflog 找回。
reflog记录了HEAD的所有移动历史(默认保留90天)。
步骤:
1. git reflog — 找到reset前的commit SHA
2. git reset --hard <SHA> — 恢复到那个commit
⚠️ 前提是还没跑 git gc --prune=now!GC会删掉悬空对象。
⚠️ 如果commit已经push到远程,也可以从远程找回。

HEAD是什么?detached HEAD是什么状态?

HEAD是一个指针,通常指向某个分支名(ref: refs/heads/main),间接指向某个commit。
Detached HEAD:HEAD直接指向一个commit而不是分支名。常见于 git checkout <SHA>git checkout v1.0
在detached HEAD下做的commit,一旦切走就可能丢失(因为没有分支指着它)。解决办法:git checkout -b new-branch 在当前位置创建新分支。

下一篇:Git工作流与暂存区图解 →
核心三原则速记

Git三原则:① 提交信息写清楚为什么改而不是改了什么 → ② 永远不在共享分支force push → ③ 合并前先rebase保持线性历史