news 2026/9/7 13:31:37

【DIY系列:Java虚拟机】第06篇:类路径(classpath)是什么鬼——JVM 怎么找到你的 class 文件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【DIY系列:Java虚拟机】第06篇:类路径(classpath)是什么鬼——JVM 怎么找到你的 class 文件

上一篇【第05篇】项目结构与第一行代码——jvmgo 项目骨架搭建
下一篇【第07篇】Entry 接口设计——类路径的"积木块"


摘要

写一个 JVM,第一件要解决的事就是:去哪儿找 class 文件?

有意思的是,Java 虚拟机规范并没有规定虚拟机该从哪里寻找类——这是留给实现者的自由。Oracle 的 JVM 用的是"类路径(classpath)"方案,我们的 jvmgo 也照抄这套。

本文讲透类路径的三大组成部分(启动类路径 / 扩展类路径 / 用户类路径)、-classpath选项的各种用法、Windows 分号与 Linux 冒号的分隔符差异、Java 6 引入的通配符lib/*,以及一个反直觉的结论:为什么你可能听说过 CLASSPATH 环境变量,但绝大多数情况下不该用它。


一、为什么需要类路径:HelloWorld 背后的"隐形依赖"

先看一个你可能没注意过的事实。

我们的 HelloWorld 只有 5 行代码:

publicclassHelloWorld{publicstaticvoidmain(String[]args){System.out.println("Hello, world!");}}

编译后只有一个HelloWorld.class。看起来干干净净,对吧?

但 JVM 要运行它,实际上得加载几十上百个类

【运行 HelloWorld 实际需要加载的类(部分)】 HelloWorld ← 我们自己写的,1 个 │ ├─ java.lang.Object ← 所有类的父类,必须有 ├─ java.lang.String ← main 的参数 String[] args ├─ java.lang.String[] ← 数组类型本身也是类! ├─ java.lang.System ← System.out 用到 │ └─ java.io.PrintStream ← System.out 的类型 │ └─ java.io.FilterOutputStream │ └─ java.io.OutputStream │ └─ java.lang.Object ├─ java.lang.StringBuilder ← 字符串拼接会被编译成 StringBuilder ├─ java.lang.AbstractStringBuilder ├─ java.lang.CharSequence ← 接口 └─ ... 还有 JDK 内部的启动类、异常处理类等

想想看:加载HelloWorld之前要先加载它的父类java.lang.Object;调用main()之前要准备参数数组,得加载java.lang.StringString[];把字符串打印到控制台,得加载java.lang.Systemjava.io.PrintStream……层层依赖,一环扣一环。

那么问题来了:这些类在磁盘的哪个角落?

  • HelloWorld.class在你当前目录,这个好找
  • java.lang.Object在哪?在 JDK 的jre/lib/rt.jar这个压缩包里!
  • 如果你用了第三方库(比如 commons-lang3),它们的 jar 包又在lib/目录下

JVM 需要一个统一的机制来定位这些散落各处的 class 文件——这就是类路径。

重点:类路径本质上是一份"搜索清单"。JVM 要加载一个类时,就按这份清单挨个地方去找,找到第一个匹配的就用。


二、类路径的三大来源

Oracle 的 JVM 把类路径分成三部分,按搜索的先后顺序排列:

【类路径的三大组成部分】 ┌─────────────────────────────────────────────────────────┐ │ 类路径 │ │ (Classpath) │ │ │ │ ① 启动类路径 ② 扩展类路径 ③ 用户类路径 │ │ Bootstrap Classpath Extension Classpath User Classpath│ │ ┌────────────────┐ ┌────────────────┐ ┌───────────┐ │ │ │ jre/lib/* │ │ jre/lib/ext/* │ │ 默认 "." │ │ │ │ │ │ │ │ │ │ │ │ · rt.jar │ │ · 扩展包 │ │ · 你的类 │ │ │ │ · resources │ │ · 本地化jar │ │ · 第三方库 │ │ │ │ · charsets │ │ │ │ │ │ │ │ · ... │ │ │ │ │ │ │ └────────────────┘ └────────────────┘ └───────────┘ │ │ 优先级最高 优先级中 优先级最低 │ │ │ │ 搜索顺序:① → ② → ③ (找到了就停止) │ └─────────────────────────────────────────────────────────┘
类路径默认位置装的是什么能改吗
启动类路径jre/lib/Java 标准库(主要都在rt.jar里)可以,用-Xbootclasspath(很少用)
扩展类路径jre/lib/ext/Java 扩展机制的类一般不改
用户类路径当前目录.我们自己写的类 + 第三方库-classpath/-cp指定

rt.jar 是个什么来头?

rt.jarRuntime的缩写,位于$JAVA_HOME/jre/lib/rt.jar。它是 Java 核心类库的打包文件,大小约 60MB(JDK 8),里面塞了大约 2 万个类:

# 看看 rt.jar 里有什么cd$JAVA_HOME/jre/lib jar tf rt.jar|head-20# 输出(节选)java/lang/Object.class java/lang/String.class java/lang/System.class java/lang/Integer.class java/lang/Thread.class java/util/ArrayList.class java/util/HashMap.class java/io/PrintStream.class...# 统计有多少个类jar tf rt.jar|wc-l# 约 20000+

重点:我们的 jvmgo 运行时,必须能加载 rt.jar 里的类。所以启动类路径是三块里最关键的——没有它,连java.lang.Object都找不到,程序根本跑不起来。

搜索顺序很重要

为什么顺序重要?举个经典的坑:

// 你自己写了一个 java.lang.String(作死行为)packagejava.lang;publicclassString{// ...}

你编译好放进 classpath,然后运行程序。你觉得 JVM 会用你写的 String 吗?

不会。因为启动类路径(rt.jar)优先级最高,JVM 先在那里找到了正牌的java.lang.String,就直接返回了,根本不会去你的用户类路径里找。

这就是双亲委派模型在类路径层面的体现——核心类库永远优先,防止被恶意/误操作替换。


三、-classpath / -cp 选项详解

用户类路径的默认值是当前目录.。但通常我们需要指定更复杂的位置,这时就用-classpath(简写-cp)。

基本用法

# 指定目录java-cppath\to\classes HelloWorld# 指定 JAR 文件java-cppath\to\lib1.jar HelloWorld# 指定 ZIP 文件(是的,zip 也行)java-cppath\to\lib2.zip HelloWorld

指定多个位置

分隔符把多个路径串起来:

# Windows:用分号 ;java-cppath\to\classes;lib\a.jar;lib\b.jar;lib\c.zip HelloWorld# Linux / Mac:用冒号 :java-cppath/to/classes:lib/a.jar:lib/b.jar:lib/c.zip HelloWorld

重点:这是新手最容易踩的坑之一。Windows 用分号;,类 UNIX(Linux/Mac)用冒号:。写错了 JVM 会把整串当成一个路径,然后报ClassNotFoundException

我们的 jvmgo 会用os.PathListSeparator来自动适配,不用硬编码:

constpathListSeparator=string(os.PathListSeparator)// Windows 下是 ";",Linux/Mac 下是 ":"

通配符:Java 6 的黑科技

从 Java 6 开始,可以用通配符*一次指定某个目录下的所有 JAR 文件

# lib 目录下的所有 jar 都会被加入类路径java-cpclasses;lib\* HelloWorld

注意几个细节:

细节说明
写法必须是lib\*不能写成lib\*.jar
是否递归不递归!只匹配lib/下的 jar,不含子目录
匹配什么只匹配.jar.JAR不匹配.zip
顺序匹配到的 jar 顺序不确定(依赖文件系统),别依赖顺序
【通配符匹配示意图】 lib/ ├── a.jar ✅ 匹配(lib/* 会加载) ├── b.jar ✅ 匹配 ├── c.JAR ✅ 匹配(大小写不敏感) ├── d.zip ❌ 不匹配(通配符只认 jar) ├── e.class ❌ 不匹配 └── sub/ └── f.jar ❌ 不匹配(不递归子目录)

四、CLASSPATH 环境变量:为什么"不推荐"

除了-cp选项,还可以设置CLASSPATH 环境变量来指定用户类路径:

# WindowssetCLASSPATH=D:\classes;D:\lib\a.jar# Linux / MacexportCLASSPATH=/home/user/classes:/home/user/lib/a.jar

但强烈不推荐这么做。原因有三:

理由 1:优先级问题

-classpath/-cp选项的优先级更高,会覆盖CLASSPATH 环境变量:

【优先级】 -cp 选项 > CLASSPATH 环境变量 > 默认当前目录 "." (最高) (最低)

这会造成困惑:你明明设了环境变量,但别人用-cp跑程序时它完全不起作用,排查起来很懵。

理由 2:全局污染

环境变量是全局的。你在机器上设了 CLASSPATH,会影响所有Java 程序——包括那些你不该影响的程序(比如 IDE、构建工具、其他项目)。

理由 3:不可移植

你的程序换台机器跑,CLASSPATH 就得重新配一遍。而-cp选项通常写在启动脚本里,跟着项目走,可移植性好得多。

最佳实践永远用-cp选项,别用 CLASSPATH 环境变量。如果非要用环境变量,也只在临时测试时用,别写进系统配置。


五、我们的 jvmgo 要怎么实现?

理解了概念,来看实现思路。

jvmgo 的 ch02 会做这些事:

【ch02 类路径实现规划】 ┌─────────────────────────────────────────────────────┐ │ Classpath │ │ ┌───────────────┬───────────────┬────────────────┐ │ │ │ bootClasspath │ extClasspath │ userClasspath │ │ │ │ (jre/lib/*) │(jre/lib/ext/*)│ (-cp 或 ".") │ │ │ └───────┬───────┴───────┬───────┴────────┬───────┘ │ │ │ │ │ │ │ │ 每种都是 Entry 接口的某个实现 │ │ ▼ ▼ ▼ │ │ ┌────────────────────────────────────────────┐ │ │ │ Entry 接口 │ │ │ │ readClass(className) ([]byte, Entry, error)│ │ │ │ String() string │ │ │ └────────────────────────────────────────────┘ │ │ │ │ 4 种实现: │ │ · DirEntry —— 目录形式 │ │ · ZipEntry —— JAR/ZIP 文件形式 │ │ · CompositeEntry —— 多路径组合(分号/冒号分隔) │ │ · WildcardEntry —— 通配符形式(lib/*) │ └─────────────────────────────────────────────────────┘

核心 API 设计(这是 JVM 规范之外的实现细节,我们参考 Oracle 的做法):

// 解析类路径funcParse(jreOption,cpOptionstring)*Classpath// 读取 class 文件(按 启动 → 扩展 → 用户 的顺序搜索)func(self*Classpath)ReadClass(classNamestring)([]byte,Entry,error)

调用示例:

// 在 startJVM 中使用cp:=classpath.Parse(cmd.XjreOption,cmd.cpOption)data,entry,err:=cp.ReadClass("java/lang/Object")// 参数用斜线分隔iferr!=nil{fmt.Printf("找不到 java.lang.Object: %v\n",err)return}fmt.Printf("找到 java/lang/Object.class,来自:%v,共 %d 字节\n",entry,len(data))

重点:注意ReadClass的参数格式——用斜线/分隔,带.class后缀。比如java.lang.Object要写成java/lang/Object.class。这跟磁盘路径的写法(Windows 用反斜杠)不一样,是 JVM 内部的统一表示法。


本篇小结

类路径是 JVM 定位 class 文件的机制,核心要点:

  1. 三大来源:启动类路径(jre/lib,核心类库 rt.jar)→ 扩展类路径(jre/lib/ext)→ 用户类路径(默认.),按此顺序搜索,找到即停
  2. -cp选项:可指定目录、jar、zip,多个用分隔符隔开(Windows 分号 / Linux 冒号)
  3. 通配符:Java 6+ 支持lib/*匹配目录下所有 jar(不递归、只认 jar、顺序不定)
  4. 别用 CLASSPATH 环境变量:优先级低于-cp、全局污染、不可移植
  5. 我们的实现:用 Entry 接口 + 4 种实现(Dir/Zip/Composite/Wildcard)来统一抽象

下一篇,我们正式开始写代码——设计 Entry 接口,把类路径抽象成可组合的"积木块"。这里会用到一个经典设计模式:组合模式(Composite Pattern)。


上一篇【第05篇】项目结构与第一行代码——jvmgo 项目骨架搭建
下一篇【第07篇】Entry 接口设计——类路径的"积木块"


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 13:29:18

C# Windows截屏功能实战:CopyFromScreen到自动化截屏详解

简介:这是一份基于C#的Windows截屏工具源码包,面向正在学习WinForms桌面开发、图形处理或网络通信的初中级开发者。程序覆盖全屏截取、自定义区域框选、截图画线标记、本地保存以及通过HttpClient上传服务器等完整流程,能帮助读者理解System.…

作者头像 李华
网站建设 2026/9/7 13:26:38

Python教学大纲深度拆解:从基础语法到项目实战的完整学习路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:25:06

爱沙尼亚大件货运物流公司怎么选?一份按需求决策的选购指南

结论摘要选择爱沙尼亚大件货运物流公司,核心不是比谁家"便宜",而是先想清楚三件事:你的货物是标准件还是超大件、你要的是"到港"还是"到门"、你有没有能力自己处理清关和税务。如果你的货是家具、机械设备、建材等非标大件,且希望省心省事、价格…

作者头像 李华
网站建设 2026/9/7 13:24:31

零基础学51单片机:从点灯到智能小车的完整路径

如果你是从零开始学单片机,又正好刷到“51单片机”这个关键词,那我猜你现在大概处于这么个状态:听说过Arduino,也听说过STM32,但总感觉直接上手那些不太踏实,想从更底层的东西开始打基础。这个感觉是对的。…

作者头像 李华
网站建设 2026/9/7 13:23:35

SOF固件与Topology源码编译实战:从环境到定制音频管线

干这行越久,越觉得那句“能用就行”最耽误事。你从发行版仓库里装好 SOF 固件,录音、放音、HDMI 输出全都正常,于是觉得没必要碰源码。可等你真正想给扬声器做一套自定义 EQ、想把多声道管线里的某个一路单独接出来做处理,或者只是…

作者头像 李华
网站建设 2026/9/7 13:23:07

基于单片机与MQTT的智能家电远程控制系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华