【编程语言】深度梳理C/C++、Java、Python、Go、Rust的区别
作者介绍
大家好,我是 CodeStats。
一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程) 范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。
写这篇文章的初衷很简单:我发现太多开发者只停留在“会用”层面,却很少追问“为什么这么设计”。 我希望用一篇文章,帮你把从CPU指令到高级语言运行时的这条认知链彻底打通。
📖 你将从本文获得什么?
-
一套从CPU物理执行到操作系统管理的完整认知框架
-
对编译型 vs 解释型 vs JVM型语言运行机制的本质理解
-
对 Rust内存安全、Go调度器设计等现代语言核心特性的深度剖析
-
面试官问“程序怎么跑起来的”时,能答出别人答不出来的那一层
📑 问题目录
-
问题一:程序运行的本质是什么?操作系统是如何调度进程的?
-
问题二:操作系统内核是C语言写的,为什么C语言需要依赖操作系统库文件?
-
问题三:Python和Java虚拟机都是C/C++实现的,为什么Python环境在Linux通常需要源码编译,而JDK通常解压安装就行?
-
问题四:Rust/C++/C都是编译型语言,Rust编译解决内存安全问题,为什么无法取代C/C++?
-
问题五:Go语言也是编译型高级语言,本质是用户态解决多线程切换提高操作系统调度并发效率问题,为什么不像JVM一样,而是GC和调度器都打包进程序里面?
-
总结:50+种编程语言的分类图谱
问题一:程序运行的本质是什么?操作系统是如何调度进程的?
1.1 CPU眼中的程序:就是一条条二进制指令
程序运行的终极真相极其简单:CPU只认识二进制指令。
你写的每一行代码,最终都会被编译(或解释)成CPU能够执行的机器码。CPU内部有一个程序计数器(PC / IP寄存器),它永远指向内存中下一条待执行指令的地址。控制单元根据PC的指向,将主存中的指令装载到指令寄存器,然后经过取址 → 译码 → 执行的循环,一条一条地把指令“吃”进去。
text
┌──────────────────────────────────────────────────────────────┐
│ 你写的代码:int a = 10; int b = 20; printf("%d", a+b); │
│ ↓ 编译 │
│ CPU执行的二进制:01001011 11010010 00101101 ... │
│ ↓ 逐条执行 │
│ 取址 → 译码 → 执行 → 取址 → 译码 → 执行 → ... │
└──────────────────────────────────────────────────────────────┘
关键认知: 程序不是“活”的,它是一张被编排好的乐谱。CPU是那个无情的演奏机器,按部就班地弹完每一个音符。
1.2 操作系统眼中的程序:进程与调度
程序在磁盘上只是一个文件(.exe / ELF)。操作系统把它加载到内存后,给它分配一个“身份”——进程(Process),并建立一个进程控制块(PCB)来记录它的所有信息。
操作系统的核心任务之一,就是让多个进程“同时”运行。但CPU核心数量有限,怎么办?
答案:时间片轮转 + 上下文切换。
每个进程都会被分配到CPU的一个时间片(通常是几十毫秒)。当一个进程用完时间片,或者被更高优先级的进程抢占时,操作系统会:
-
保存当前进程的状态(寄存器、程序计数器、栈指针等)到PCB中
-
从就绪队列中选出一个新进程
-
恢复新进程的PCB中的状态到CPU寄存器
这个过程叫做上下文切换(Context Switch)。
text
┌─────────────────────────────────────────────────────────────────┐ │ 进程A(运行中) → 时间片用完 → 保存状态到PCB_A │ │ ↓ │ │ 操作系统调度器选择进程B │ │ ↓ │ │ 进程B(恢复运行) ← 从PCB_B恢复状态 ← 加载进程B │ └─────────────────────────────────────────────────────────────────┘
上下文切换是有成本的——每次切换都要保存和恢复大量寄存器数据,这就是为什么操作系统线程切换(内核态)比用户态协程切换(如Go的Goroutine)慢得多的根本原因。
🧠 思考总结
我当年手写框架时,第一个“顿悟”时刻就是发现:不管代码写得多么花哨,CPU根本不关心你是Java还是C++,它只认二进制指令。这个认知一旦建立,你就不会再被“运行时”、“虚拟机”、“解释器”这些概念唬住了——它们都只是在这条指令流上加了不同层级的“包装”。
问题二:操作系统内核是C语言写的,为什么C语言需要依赖操作系统库文件?
这个问题看似矛盾,实则揭示了C语言与操作系统之间微妙的分层关系。
2.1 操作系统内核 vs C标准库:不是一回事
操作系统内核(如Linux内核)是用C语言写的,它运行在CPU的Ring 0(内核态),拥有对硬件的完全控制权。内核本身不依赖任何外部库——它自己就是“库”的提供者。
但你写的C语言程序,运行在Ring 3(用户态)。它不能直接操作硬件,必须通过系统调用(System Call) 来请求内核帮它做事。
而C标准库(如glibc)就是用户态程序和内核之间的“翻译官”:
text
┌─────────────────────────────────────────────────────────────────┐
│ 你的C程序:printf("Hello") │
│ ↓ │
│ C标准库(glibc):将printf翻译成write()系统调用 │
│ ↓ │
│ 操作系统内核:执行write(),把数据写入终端 │
└─────────────────────────────────────────────────────────────────┘
2.2 为什么C程序依赖库文件?
因为C标准库提供了大量封装好的函数(printf、malloc、fopen等),让你不用每次都用汇编去触发系统调用。
而这些库函数有两种链接方式:
| 链接方式 | 文件后缀 | 特点 |
|---|---|---|
| 静态链接 | .a | 库代码复制进你的exe,程序独立但体积大 |
| 动态链接 | .so / .dll | 运行时加载库,程序体积小但依赖外部库文件 |
关键认知: 操作系统内核不依赖任何库,但你的C程序依赖libc——因为libc是你和内核之间的桥梁。用ldd命令查看任意C编译的程序,几乎都能看到对libc.so的依赖。
🧠 思考总结
我在手写框架的经历中有一个很深的体会:分层越清晰,系统越健壮。 操作系统和用户程序之间,正是通过“系统调用”这一层明确的边界来完成分工。C标准库不是“多余”的,它是用户程序能够安全、便捷地使用操作系统功能的必要抽象。没有它,你写每一行代码都得手动触发系统调用——那会倒退到汇编时代。
问题三:Python和Java虚拟机都是C/C++实现的,为什么Python环境在Linux通常需要源码编译,而JDK通常解压安装就行?
这个问题触及了“分发策略”与“运行依赖”的核心差异。
3.1 相同点:底层实现都是C/C++
Python的主流实现CPython和Java虚拟机(JVM),底层都是用C/C++写的。两者都是“在操作系统之上又搭了一层软件环境”。
3.2 不同点:分发的是什么?
| 维度 | Python(源码编译安装) | JDK(解压即用) |
|---|---|---|
| 分发内容 | 源代码(.c文件 + .py文件) | 已编译好的二进制文件(javac、java、.so/.dll) |
| 依赖关系 | 依赖系统编译器(gcc)、开发头文件 | 不依赖编译器,只依赖操作系统的基础C库 |
| 安装方式 | ./configure → make → make install |
解压 → 配置PATH → 即用 |
3.3 为什么Python常需要源码编译?
原因一:系统自带版本太老。 很多Linux发行版默认带的是Python 2.7或3.6,而你需要的是3.12的新特性。
原因二:预编译包缺失。 某些Linux发行版的软件仓库没有提供最新版Python的预编译包。
原因三:定制需求。 你需要修改Python源码、启用/禁用某些模块、或者指定安装路径。
3.4 为什么JDK通常解压即用?
因为Oracle/OpenJDK官方直接提供了针对各平台的预编译二进制包。
JDK的二进制文件(java、javac)已经是编译好的本地机器码,只需解压到任意目录、配置JAVA_HOME和PATH即可使用。
🧠 思考总结
我从零手写Java Web框架时,深度体验了Java生态的“开箱即用”。当时我写了一个IoC容器和一个简化的Spring MVC,跑在嵌入式Tomcat上,整个过程没有任何“编译C代码”的步骤。Java这种“一次编译,到处运行”的体验,本质上是各平台预编译好的二进制分发策略带来的。 而Python之所以需要源码编译,不是因为它“更底层”,而是因为它的分发方式更依赖于你所在的操作系统环境。
问题四:Rust/C++/C都是编译型语言,Rust编译解决内存安全问题,为什么无法取代C/C++?
Rust通过所有权系统、借用规则和生命周期检查,在编译期就杜绝了悬垂指针、数据竞争等内存安全问题。它不需要垃圾回收(GC),却能保证内存安全。
那为什么Rust无法取代C/C++?
4.1 原因一:历史代码的“天文数字”
全球有数十亿行C/C++代码在关键系统中运行——Linux内核、Windows内核、数据库引擎、游戏引擎、浏览器内核。
重写这些代码的成本是天价。不是技术问题,是经济问题。
4.2 原因二:ABI(应用程序二进制接口)不稳定性
Rust 没有稳定的ABI。这意味着:
-
用Rust 1.70编译的库,无法直接链接给Rust 1.80使用
-
Rust无法成为操作系统的系统动态链接库标准
-
而C/C++的ABI在特定编译器下相对稳定
4.3 原因三:学习曲线陡峭
Rust的所有权和生命周期概念需要完全重塑编程思维。C/C++程序员习惯了“手动管理内存的自由”,切换到Rust的“编译器管一切”模式,学习成本极高。
4.4 原因四:编译速度
Rust的编译速度远慢于C/C++。因为编译器要执行大量的借用检查、生命周期分析和泛型单态化。
4.5 原因五:生态成熟度
在某些领域(如特定GUI框架、游戏引擎),Rust的库和工具链还不如C++丰富和稳定。
关键认知: Rust不是在“取代”C/C++,而是在“围剿”C/C++的应用边界——新项目可以用Rust,但存量系统会继续用C/C++。
🧠 思考总结
我手写框架的过程中,有一个深刻的体会:真正约束技术选型的,往往不是技术本身,而是历史惯性。 你写的框架再优雅,如果它依赖的底层库只提供C/C++ API,你就只能用Java/JNI去调它。Rust的内存安全再强大,它撼动不了那几十亿行已经跑在生产环境上的C/C++代码。技术演进不是“推倒重来”,而是在存量基础上做增量创新。
问题五:Go语言也是编译型高级语言,本质是用户态解决多线程切换提高操作系统调度并发效率问题,为什么不像JVM一样,而是GC和调度器都打包进程序里面?
5.1 Go vs JVM:设计哲学的根源差异
| 维度 | JVM(Java) | Go Runtime |
|---|---|---|
| 运行形态 | 独立的虚拟机进程,需要预先安装 | 嵌入在二进制文件中的代码库 |
| 调度模型 | 操作系统线程(1:1映射) | G-P-M用户态调度器(M:N映射) |
| 部署方式 | 需要安装JRE/JDK + 配置环境变量 | 一个二进制文件,拷过去就能跑 |
| 启动速度 | 慢(JVM初始化 + 类加载) | 极快(毫秒级) |
| 内存占用 | 大(JVM本身占几百MB) | 小(运行时嵌入,无额外进程) |
5.2 为什么Go要把Runtime打包进二进制?
原因一:部署即运行,零依赖。 Go编译出的二进制文件是静态链接的,不依赖任何外部库(甚至不依赖libc)。在容器化时代,这意味着更小的镜像、更快的启动。
原因二:没有“中间层”的性能损耗。 Go直接编译为本地机器码,运行时没有字节码解释或JIT编译的额外开销。
原因三:调度器与程序“共生”。 Go的调度器(G-P-M模型)直接与操作系统线程协作,调用clone、epoll等系统调用。它不是“寄生”在操作系统之上,而是与操作系统协同工作。
5.3 Go Runtime vs JVM:一张图说清
text
┌─────────────────────────────────────────────────────────────────────┐ │ JVM模式(Java) │ │ ┌─────────┐ ┌──────────────┐ ┌─────────────────────────┐ │ │ │ 你的代码 │ → │ .class字节码 │ → │ JVM(需预先安装) │ │ │ └─────────┘ └──────────────┘ │ - 解释器/JIT编译器 │ │ │ │ - GC │ │ │ │ - 线程调度器 │ │ │ └─────────────────────────┘ │ │ ↓ │ │ 操作系统 + CPU │ ├─────────────────────────────────────────────────────────────────────┤ │ Go模式(静态编译) │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 一个独立的二进制文件(.exe / ELF) │ │ │ │ ┌───────────┐ ┌───────────┐ ┌─────────────────────┐ │ │ │ │ │ 你的业务 │ + │ 调度器 │ + │ GC + 内存分配器 │ │ │ │ │ │ 代码 │ │ (G-P-M) │ │ + netpoll │ │ │ │ │ └───────────┘ └───────────┘ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ 操作系统 + CPU │ └─────────────────────────────────────────────────────────────────────┘
关键认知: Go不是“不想做成JVM”,而是“不需要做成JVM”。Go的设计目标是云原生时代的服务端语言——快速部署、低内存占用、高并发。把Runtime打包进二进制,牺牲了“跨平台字节码”的灵活性,换来了极致的部署简单性和启动速度。
🧠 思考总结
我手写框架的经验让我明白了:没有“最好”的设计,只有“最适合场景”的设计。 我当年选择用Java写框架,就是因为Java的生态和跨平台能力最适合“一次编写,到处跑”的Web场景。而Go选择把Runtime打包进二进制,是因为它瞄准的是云原生场景——在那里,极致的部署速度和低内存占用比“跨平台字节码”重要得多。设计语言不是在追求“完美”,而是在做“取舍”。
总结:50+种编程语言的分类图谱
至此,我们已经从CPU指令一路讲到了五种主流语言的运行机制。现在把这些知识整理成一张完整的分类图谱:
📊 编程语言运行模式分类
text
┌─────────────────────────────────────────────┐
│ 编程语言分类 │
└─────────────────────────────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌───────▼───────┐
│ 编译型 │ │ 虚拟机型 │ │ 解释型 │
│ (AOT) │ │ (VM-Based) │ │ (Interpreted)│
└─────┬─────┘ └──────┬──────┘ └───────┬───────┘
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ │ │ │ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│C/C++ │ │Rust │ │Java │ │C# │ │Python │ │Ruby │
│Rust │ │Go │ │Scala │ │Kotlin│ │PHP │ │JS │
│Zig │ │ │ │Groovy │ │F# │ │Perl │ │Lua │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘
│ │ │ │ │ │
└────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
直接编译为 编译为字节码 逐行解释执行
本地机器码 + JIT编译 无编译过程
无运行时依赖 需安装VM/运行时 需安装解释器
📋 各语言运行机制速查表
| 语言 | 执行模式 | 编译产物 | 运行时依赖 | 内存管理 | 并发模型 |
|---|---|---|---|---|---|
| C | 编译型 | 本地机器码 | libc(动态/静态) | 手动管理 | OS线程 |
| C++ | 编译型 | 本地机器码 | libstdc++ | 手动/RAII | OS线程 |
| Rust | 编译型 | 本地机器码 | 无(静态链接) | 所有权系统 | OS线程 + async |
| Go | 编译型 | 本地机器码 | 无(Runtime内嵌) | GC | G-P-M协程 |
| Java | VM型 | 字节码(.class) | JVM | GC | OS线程 + 虚拟线程 |
| C# | VM型 | IL字节码 | .NET CLR | GC | OS线程 + Task |
| Python | 解释型 | 无(.pyc缓存) | CPython解释器 | 引用计数+GC | OS线程(GIL限制) |
| JavaScript | 解释/JIT | 无 | V8/SpiderMonkey等 | GC | 事件循环 |
🔑 四条核心认知
-
程序运行的本质没有变——无论什么语言,最终都是CPU在逐条执行二进制指令。所有“运行时”、“虚拟机”、“解释器”,都只是在这条指令流上加了不同层级的“包装”。
-
语言选择的本质是“场景匹配”——没有“最好”的语言,只有“最适合”的场景。C/C++统治操作系统和嵌入式,Java统治企业应用,Go统治云原生基础设施,Rust开始统治高性能系统软件。
-
“打通”认知的价值——真正理解从CPU指令到高级语言运行时的全链路,不是为了炫技,而是为了在遇到性能问题、内存问题、并发问题时,能准确判断问题出在哪一层。
-
技术演进不是“推倒重来”,而是在存量基础上做增量创新——这就是Rust取代不了C/C++的根本原因,也是Go选择“把Runtime打包进二进制”而非“做成JVM”的现实考量。
世界上有50多种主流编程语言,但CPU只有一种执行方式——逐条执行二进制指令。 理解了这一点,你就理解了所有语言的本质。
🤔 互动
如果这篇文章对你有帮助,欢迎:
-
👍 点赞:让更多开发者看到这篇硬核内容
-
⭐ 收藏:随时回顾从CPU到应用的完整认知框架
-
💬 评论:分享你对某个语言运行机制的独特见解
-
🔗 分享:转发给正在学习编程语言底层原理的朋友
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)