← 返回 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目录结构
$ 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 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
# 创建测试文件
$ 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 — 验证分支只是指针
# 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三原则:① 提交信息写清楚为什么改而不是改了什么 → ② 永远不在共享分支force push → ③ 合并前先rebase保持线性历史