Skip to content

Commit ccd36da

Browse files
puppylpgclaude
andcommitted
Add: JAR 读取原理 + Python venv 核心概念两篇博文
- _posts/2026-04-12-jar-zip-random-access.md: ZIP 协议与随机访问机制 - _posts/2026-06-08-python-venv-principles.md: venv 隔离原理、activate 机制、与 conda 对比 - CLAUDE.md: 新增 front matter date 必须用真实时间的规则 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f6d3530 commit ccd36da

3 files changed

Lines changed: 428 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ bundle exec htmlproofer _site --disable-external --no-enforce-https
2727
- 新内容 front matter 必填:`title``date`
2828
- 推荐补齐:`categories``tags``description`
2929
- 日期带 `+0800`;站点时区是 `Asia/Shanghai`
30+
- `date` 必须用当前真实时间,不要编造。用 `date '+%Y-%m-%d %H:%M:%S %z'` 获取。
3031
- `categories``tags` 保持小写。
3132
- 新增集合文章时不要写 `layout:`,交给 `_config.yml` 的 defaults scope。
3233
- 不要手写 `last_modified_at`,它由 `_plugins/posts-lastmod-hook.rb` 从 git 历史注入。
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
title: "JAR 文件读取原理:ZIP 协议与随机访问"
3+
date: 2026-04-12 00:00:00 +0800
4+
categories: [java, jvm]
5+
tags: [jar, zip, classloader, random-access]
6+
description: "JAR 不就是磁盘上一个文件吗,getResourceAsStream 怎么能读到里面?答案在 ZIP 二进制协议和 Central Directory 索引——不需解压,直接寻址。"
7+
---
8+
9+
JAR 不就是磁盘上一个文件吗,那 `getResourceAsStream("config.properties")` 怎么能读到"里面"的东西?
10+
11+
1. Table of Contents, ordered
12+
{:toc}
13+
14+
# 读取 JAR 内资源的三种姿势
15+
16+
```java
17+
// 1. ClassLoader(最常见)
18+
InputStream is = MyClass.class.getResourceAsStream("/config.properties");
19+
20+
// 2. 直接操作 JarFile
21+
try (JarFile jar = new JarFile("myapp.jar")) {
22+
JarEntry entry = jar.getEntry("config/app.properties");
23+
InputStream is = jar.getInputStream(entry);
24+
}
25+
26+
// 3. NIO FileSystem(Java 9+)
27+
FileSystem fs = FileSystems.newFileSystem(Path.of("myapp.jar"), (ClassLoader) null);
28+
Path inside = fs.getPath("/config.properties");
29+
String content = Files.readString(inside);
30+
```
31+
32+
为什么不能用 `new File("config/app.properties")`?因为 JAR 内的路径是虚拟路径,不存在于磁盘文件系统。
33+
34+
# 不解压怎么读?直接解析 ZIP 二进制字节
35+
36+
上面三种方式虽然 API 不同,但底层做的是同一件事:**不把 JAR 解压到磁盘,直接从文件内部定位并读取目标字节。** 怎么做到的?JAR 本质上就是 ZIP 格式,所以答案是按 ZIP 协议直接解析二进制字节。
37+
38+
## ZIP 文件结构
39+
40+
```plain
41+
[Local File Header + Data] ... [Central Directory] [End of Central Directory]
42+
```
43+
44+
关键是末尾的 **Central Directory(中央目录)**,它是一个索引,记录每个 entry 的:
45+
46+
- 文件名
47+
- 压缩方式(stored / deflated)
48+
- **在文件中的字节偏移量(offset)**
49+
- 压缩后/原始大小
50+
51+
## 读取过程
52+
53+
```plain
54+
1. open JAR 文件(一次 syscall)
55+
2. seek 到文件末尾,找到 End of Central Directory
56+
3. 解析 Central Directory,建立"文件名 → offset"内存索引
57+
4. 需要某个 entry 时,直接 seek 到对应 offset
58+
5. 按压缩方式(通常 Deflate)解压那段字节,返回流
59+
```
60+
61+
## 示意图
62+
63+
```plain
64+
myapp.jar
65+
├─ offset 0: [Local Header] [Main.class deflate 压缩数据]
66+
├─ offset 4096: [Local Header] [app.properties 数据]
67+
68+
└─ offset 98304: [Central Directory]
69+
"com/example/Main.class" → offset=0, size=2048
70+
"config/app.properties" → offset=4096, size=512
71+
```
72+
73+
`config/app.properties`:查索引 → `lseek(fd, 4096)` → 读 512 字节 → inflate → 返回流。
74+
75+
# 本质类比
76+
77+
> **随机文件访问 + 内存索引**,和数据库 B-Tree 索引思路一样。
78+
79+
不需要扫描整个文件,直接跳到目标位置读字节。ZIP 格式本身就是为随机访问而设计的——Central Directory 存在的意义就是支持这种"不解压、直接寻址"的访问模式。
80+
81+
JDK 的 `ZipFile` 底层用 C 实现(`zip_util.c`),`getEntry` 是 O(1) 哈希查找,`getInputStream` 做的是 seek + inflate。

0 commit comments

Comments
 (0)