Java和Go都拍胸脯说"内存我包了",可它们到底是怎么下手的?

简介: 上回咱们把内存翻车的根源捋了一下,最后得出一个结论:要根治,得靠"接管型 Runtime"把内存的分配、访问、释放一手包办。今天咱就顺着一条"申请内存"的代码链路,看看它到底是怎么"下场"接管的?#Runtime #接管型Runtime #内存管理 #JVM #Java #Go #字节码 #解释器 #JIT #AOT #TLAB #安全点 #GC #编程语言 #原理 #知识分享

文章封面.jpeg

温馨提示:另有配套视频版,附带图解演示,观感不同,欢迎搜索同名标题或【蛋先生说识】

开场

丹尼尔:蛋兄,上回您说"接管型 Runtime"能包办内存安全,还撂下一句"且听下回分解"。我这"下回"可等了一个星期了啊!(没看过上回的,可在各平台搜索"内存安全翻车现场的罪魁祸首,根源就在这三步里?")

蛋先生:哈哈,行,那今天就唠这个!具体怎么包办,还得看语言最终走的是哪条运行路线。咱先按主要的执行方式分成两类——字节码解释型和机器码编译型(说明:这里仅仅是为了介绍原理设计,实际上不少现代语言是混合的,比如 Java 会先用解释器,热点再 JIT 编译)

先分清两种"包办"的姿势

丹尼尔:怎么区分?

蛋先生:有的语言,程序跑的是字节码,而字节码指令的具体实现都在 Runtime 里,由它来解释执行——既然每条指令都是它亲手执行的,内存管理自然也就由它直接上手管了。这就是"字节码解释型"(以下简称解释型)

丹尼尔:那"机器码编译型"(以下简称编译型)呢?

蛋先生:编译型就稍麻烦一点!代码被直接编译成 CPU 能执行的机器码,CPU 自己就能按顺序执行,执行指令流本身不再需要 Runtime 逐条调度。但内存分配怎么办呢?Runtime 想接管,只能换一招——"替换用户代码"

丹尼尔:哦?一个是"我直接管",一个是"我偷偷改你的代码"?

蛋先生:( ╯▽╰) 总结到位!那咱就先看"直接管"的解释型

解释型:指令都归我解释,内存自然归我管

丹尼尔:那解释型具体是怎么把内存管起来的?

蛋先生:先解释下啥叫解释型。它解释的是"字节码所描述的指令"——注意,字节码不是机器码指令,CPU 不认,得由 Runtime 来解释执行。既然每条指令的实现都攥在 Runtime 手里,那内存管理的活,自然也只有它能干

丹尼尔:明白,但我想知道"怎么管"

蛋先生:那咱就拿一句最经典的"申请内存"的代码开刀,把「源码 → AST → 字节码 → Runtime 真的申请到内存」这条链路从头串到尾,保准你一看就懂

丹尼尔:成!走起!

✧ 源码:一行代码,暗含三步

蛋先生:就这句,写 Java 的没人不认识吧

Foo foo = new Foo();

丹尼尔:像我这种不怎么写 Java 的也看得懂啊,各语言的语法都差不多嘛

蛋先生:这句看似简单的代码,其实暗含三步:先在堆上申请一块能装下 Foo 实例的内存,再调用构造方法做初始化,最后把对象引用交给变量 foo

丹尼尔:我平时倒是没看得这么深

蛋先生:有没有发现,"申请内存"这件事,程序员甚至可以完全不关心——因为真正动手的是 Runtime

✧ AST:把意图"解析"成一棵树

丹尼尔:那我倒想看看,这行源码接下来都要经历哪些环节?

蛋先生:第一步,编译器会先按语法规则把它解析成一棵树,语义没变,但结构化了,这就是 AST(抽象语法树)。刚才那句代码,大致长这样

VariableDeclarationStatement              // 一条"声明变量"的语句
├── type: SimpleType "Foo"                // 变量类型
└── fragments
    └── VariableDeclarationFragment
        ├── name: "foo"                   // 变量名
        └── initializer                   // 初始化器 = 下面的 new Foo()
            └── ClassInstanceCreation
                ├── type: SimpleType "Foo"
                └── arguments: []

丹尼尔:哦!结构化之后,机器确实更容易处理了

蛋先生:对!还记得上次咱们说过,为了避免反复遍历这棵树的开销,需要怎么做吗?

✧ 字节码:翻译成 Runtime 能听懂的话

丹尼尔:把它拍平成可执行的字节码指令呗

蛋先生:没错!编译器接着把这棵树"拍平"成 Runtime 认识的语言——字节码。刚才那句源码,大致就对应这几条 JVM 字节码

0: new            #2   // class Foo                 ← 申请对象内存,引用入栈
3: dup                //                            ← 复制引用(构造器调用会消耗一份)
4: invokespecial  #3   // Method Foo."<init>":()V   ← 调用构造方法做初始化
7: astore_1           //                            ← 把引用存入局部变量 foo

丹尼尔:目前看,还没看到"申请内存"的显式指令

蛋先生:是的,申请内存的逻辑,就藏在 new 指令的实现里

丹尼尔:那,瞧一眼呗

✧ Runtime 对字节码 new 的实现:终于轮到我出手

蛋先生:安排!接管型 Runtime 会预先为每一条字节码指令写好解释逻辑。new 的实现大致长这样(伪代码示意)

void opcode_new(u2 class_index) {
   
    // 1. 从常量池查出要创建的是哪个类(Klass = 类的运行时结构)
    Klass* cls = constant_pool->klass_at(class_index);

    // 2. 首次使用时触发类加载:把 class 文件变成运行时结构
    if (!cls->is_loaded()) {
   
        load_class(cls);
    }

    // 3. 检查堆空间,不够就先触发 GC(对象都是它管的,它可以先清理再分配)
    while (heap->free_size() < cls->instance_size()) {
   
        gc();
    }

    // 4. 申请内存:从 Runtime 自己掌管的那块堆上划出一块
    oop obj_ref = heap->allocate(cls->instance_size());

    // 5. 零值初始化:字段先全部填默认值(数值 0 / 引用 null)
    zero_fill(obj_ref, cls->instance_size());

    // 6. 把对象引用压入操作数栈,交给下一条指令(dup/invokespecial)继续处理
    operand_stack.push(obj_ref);
}

丹尼尔:哇~!看到了!申请内存、触发 GC,全都在这段实现里

蛋先生:(≧∇≦) 对!因为对象全是它管的,堆不够了就先清理再分配,主动权全在它手里

✧ 完整链路:从意图到内存

丹尼尔:明白了,new Foo() 只是往 Runtime 那儿递了张"我要个 Foo"的申请单,后面怎么处理,全由 Runtime 接管

蛋先生:正解!真正的内存申请动作,被完整封装在 Runtime 解释器里——它不把这套逻辑交给用户,内存的分配、释放节奏就完全在它掌控之中,这就是解释型里"接管"的真相。解释型的主流程大致如下:

源码 new Foo()  →  AST(语法树)  →  字节码 new(指令编号)  →  Runtime 的 opcode_new()  →  从堆上申请到内存

编译型:代码都成机器码了,Runtime 咋插一脚?

丹尼尔:解释型我算看明白了。可编译型咋整?像 Go 那样,源码直接编译成机器码,CPU 自己就能执行,根本不用 Runtime 来调度——那它又是怎么接管内存的呢?

蛋先生:所以编译型得换思路——它没有"Runtime 统一逐条解释"的字节码,Runtime 想接管,只能在"生成机器码"这一环节做手脚

丹尼尔:咋整?

蛋先生:无论是 AOT(提前编译)还是 JIT(即时编译),编译器在生成机器码时,都知道 Runtime 的内存分配接口,于是会在 new Foo() 这类地方自动植入内存管理的代码——相当于替 Runtime 在用户代码里进行"埋点"。还是用那句 Foo foo = new Foo(); 来演示,方便你跟刚才的解释型对比着看

✧ 内联方式(快路径):把分配直接内联成几条 CPU 指令

丹尼尔:您继续!

蛋先生:最常见的情况,是当前线程私有的 TLAB(线程本地分配缓冲区,Thread-Local Allocation Buffer)里还有空余

丹尼尔:等等,TLAB 又是啥新词?

蛋先生:你是不是经常给你家小孩一点零花钱?

丹尼尔:嗯,跟这有啥关系?

蛋先生:所以你家小孩买点小零食的时候,是不用来找你的,自己兜里的小零钱就够了。把你比作 Runtime,那 TLAB 就是你提前塞给你小孩(线程)的"零钱"。这种小内存分配起来比较简单,编译器就索性把"分配"展开成几行机器码,直接插进你的方法体里(x86-64 伪汇编示意)

; JIT 编译 `Foo foo = new Foo();` 生成的机器码(x86-64 伪汇编示意)
; rdx = 当前线程的 TLAB 结构
mov   rax, [rdx + TLAB.top]        ; 1. 取 TLAB 当前水位(下一块内存从哪开始)
mov   rcx, rax
add   rcx, 32                      ; 2. 32 = Foo 实例大小,算出划完后的新水位
cmp   rcx, [rdx + TLAB.end]        ; 3. 会超出 TLAB 上界吗?
jg    slow_path                    ;    超出 → 跳去"函数调用方式"处理
mov   [rdx + TLAB.top], rcx        ; 4. 水位前移 = 这块内存被划走了

丹尼尔:哇塞!整个"申请内存"的过程,就是几行 CPU 指令?

蛋先生:没错!所以它叫"内联 / 快路径"。但这块内存可不是用户自己拿的——它来自 Runtime 提前塞给线程的"零钱"。Runtime 只是把最频繁的小额分配"预授权"给了线程,让它几纳秒内自己划完,省去每次分配都要进 Runtime 的开销

✧ 函数调用方式(慢路径):把分配替换成一次"call 进 Runtime"

丹尼尔:那要是"额度"不够用呢?

蛋先生:你小孩零花钱不够,不是得找回你吗?所以就需要把这次分配替换成一次真正的函数调用——直接 call 进 Runtime 的 C++ 代码里,这就是"慢路径"

; 慢路径:情况变复杂了,把控制权交回 Runtime
slow_path:
    call  runtime_allocate_object   ; call 进入 Runtime(C++ 实现的)
    ; 返回后 rax = 新对象地址

丹尼尔:这看起来就是个普通的分配函数嘛

蛋先生:关键在调用方式!这个 call 不是用户主动调的,是编译器在生成机器码时替用户埋好的——你写的明明还是那句 new Foo(),机器码里却悄悄多了一次 call 进 Runtime

两种姿势,一个目的

丹尼尔:这下全明白了,舒服。来,蛋兄,收个尾呗

蛋先生:好!解释型:指令的实现全在 Runtime,内存分配由它亲手操办;编译型:机器码虽由 CPU 直接执行,但靠"内联分配 + 替换成函数调用",内存照样被它管得死死的

丹尼尔:一句话总结:甭管哪种姿势,核心就一条——"申请内存"这个动作,永远轮不到用户亲手碰

蛋先生:说得漂亮!这,就是"接管"的全部真相了

写在最后

亲们,看到这,不妨在评论区聊聊:你平时最常写的编程语言是哪一款?它身上有"接管型 Runtime"吗?有的话,它采用的是本文说的哪种姿势——"解释型直接管","编译型埋点偷偷管",还是两者结合的"混合姿势"?欢迎带上你熟悉的语言来现身说法

都到这了,要不,顺手点赞 / 收藏 / 关注支持下呗? o( ̄▽ ̄)d

目录
相关文章
|
7天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1814 13
|
7天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
|
13天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
12天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1647 3
|
7天前
|
人工智能
千问办公官网入口:阿里AI办公QwenWork产品页和免费网页端链接
千问办公官网含两大入口:一是网页端(qwenwork.cn),即开即用,支持浏览器直接访问;二是阿里云产品页 https://t.aliyun.com/U/JNKJuO 提供免费/付费版详情、功能介绍及使用指南。
|
9天前
|
SQL 人工智能 前端开发
QoderWake 1.0 正式发布:从桌面里的 Agent,到工作现场的数字员工
QoderWake v1.0正式发布:企业级数字员工团队平台。支持“一句话建岗”,预置10类特训岗位;Waker常驻钉钉/飞书群,@即响应、自动协作、跨任务记忆;具备定时/事件/API多触发方式与统一任务看板;已沉淀27.6万条记忆、12.3万项技能,助力组织实现人机协同增效。
790 2
|
6天前
|
缓存 测试技术 API
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
DeepSeek V4.1 Flash 内测不用申请,base_url 不变、改个模型名就能调,9/10 到期。本文讲清接入、计费限流与多模态注意点。
812 0
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
|
14天前
|
缓存 数据可视化 开发工具
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
DeepSeek Harness 的更新分两层:本体更新(npx 自动最新、npm update -g、源码 git pull)与插件更新(插件市场点更新、命令行覆盖安装)。本文按「准备 → 更新本体 → 更新插件 → 更新后检查」四步走,覆盖新手常见疑问。
1607 1
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
|
21天前
|
人工智能 自然语言处理 安全
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
本文聚焦阿里云2026年推出的三款自研AI办公产品,清晰拆解千问办公、Qoder Teams、Qoder CN的差异化定位与能力边界:千问办公主打职场全场景提效,支持自然语言指令一键完成PPT生成、数据分析等高频办公任务;Qoder Teams面向程序员团队,深度整合AI代码生成、团队协同与企业知识库能力;Qoder CN则专为金融、政务等强合规场景打造,实现数据不出境与VPC私有化部署。文章同步给出分场景选型指南与最新活动定价,帮助不同类型的企业按需组合产品,实现业务岗、研发岗与强合规场景的AI能力全覆盖。
3989 5
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
|
12天前
|
人工智能 自然语言处理 安全
阿里云AI数智鉴密:AI 生成内容如何拿到一张"防篡改的身份证"
隐形水印 + C2PA签名:让AI生成内容“持证上岗”。
1156 0