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

目录
相关文章
|
12月前
|
机器学习/深度学习 人工智能 索引
RAG 切片利器 LumberChunker 是如何智能地把文档切割成 LLM 爱吃的块
RAG 里的文档应该怎么切割比较好呢?按固定的字符数或词数?按句?按段落?加个重叠窗口?还是 ...
535 1
RAG 切片利器 LumberChunker 是如何智能地把文档切割成 LLM 爱吃的块
|
4月前
|
人工智能 边缘计算 自然语言处理
智能体来了,AI从“会聊天”到“能干活”,开启全民智能助手时代
当你说“帮我订一家周末的亲子餐厅”,传统AI只会罗列餐厅信息;而智能体会自动查档期、对比评价、预约座位并同步到日历——无需你反复操作,全程自主搞定。 2026年,AI智能体(Agent)不再是实验室概念,而是正在重塑工作与生活的核心力量。它标志着人工智能从“被动问答”的大模型时代,迈入“主动执行”的智能体时代。今天,我们用一篇文章讲透:智能体是什么、能做什么、将如何改变我们的世界。
|
11月前
|
数据采集 Web App开发 调度
我为什么彻底切到Playwright
本文分享从Puppeteer迁移到Playwright的实战经验,详解架构升级动因、模块重构与核心代码。Playwright凭借更强的隔离性、原生反检测支持、简洁代理配置及多浏览器兼容,彻底解决Puppeteer时代资源争抢、稳定性差等痛点,助力构建高可用、易维护的现代数据系统。
465 1
|
10月前
|
JSON 前端开发 Java
Spring Boot返回Json数据及数据封装
Spring Boot中,@RestController注解可自动将数据转为JSON格式,其底层默认使用Jackson框架。该注解等价于@Controller与@ResponseBody结合,简化开发。Jackson支持对象、List、Map等常见数据结构的JSON转换,并可通过配置自定义null值处理方式,如将null转为空字符串,提升接口返回的规范性与前端友好性。(238字)
|
10月前
|
机器学习/深度学习 算法 关系型数据库
🎮 强化学习
强化学习通过智能体与环境交互,基于状态、动作和奖励学习最优策略。核心方法包括价值迭代、Q-learning、策略梯度及Actor-Critic框架,结合在线/离线学习与同/异策略优化,实现高效决策。
962 0
 🎮 强化学习
|
10月前
|
机器学习/深度学习 搜索推荐 算法
推荐引擎:没有搜索词,「头条」怎么找到你感兴趣的文章?
资讯类App通过“下拉刷新”精准推荐内容,背后依赖推荐引擎的检索技术。它基于用户行为数据构建用户画像与文章画像,结合协同过滤、内容召回等算法,实现个性化推荐,并通过多路召回与分层排序提升效率与准确性。
|
11月前
|
人工智能 数据可视化 调度
被Nature旗下刊物收录!我用AgentScope造了个“AI社科实验室”
科学家用AI模拟学术世界!通义实验室联合人大打造虚拟学术宇宙CiteAgent,基于自研多智能体框架AgentScope,实现数万AI科学家协同仿真,复现引文网络三大经典现象。研究获顶刊《Nature》子刊录用,开创社会科学“实验室”,推动“AI for Social Science”新范式。(回复CiteAgent获取论文)
809 0
|
监控 供应链 API
唯品会:对接商品库存API,实现限时特卖活动库存动态预警,避免超卖
唯品会通过对接商品库存API,构建实时动态预警系统,有效解决限时特卖中的超卖问题。系统可及时预警低库存商品,结合自动限流与人工干预,大幅降低超卖风险,提升订单履约率与用户体验,助力电商高效运营。
|
存储 弹性计算 Linux
阿里云服务器地域、实例、带宽和镜像选择注意事项参考
阿里云服务器作为很多用户的首选云服务器产品,对于很多初次选购阿里云服务器的新手用户而言,在选购云服务器的过程中,面对地域、实例、带宽以及操作系统等配置选项,由于是初次选择,可能不知道应该如何选择以及选择过程中有哪些地方是需要注意的。为此,本文为新手用户介绍在选购阿里云服务器过程中,针对地域、实例、带宽和镜像的一些注意事项,以供参考。
|
自然语言处理 测试技术 决策智能
让RAG更聪明,ViDoRAG开启视觉文档检索增强生成新范式,上阿里云百炼可直接体验
视觉丰富文档的高效检索与生成是自然语言处理领域的重大挑战。ViDoRAG(Visual Document Retrieval-Augmented Generation via Dynamic Iterative Reasoning Agents)由阿里巴巴通义实验室、中国科学技术大学和上海交通大学联合提出,通过多智能体框架和动态迭代推理机制解决此问题。其核心包括多模态混合检索策略和多智能体生成流程,同时发布的ViDoSeek数据集,专为大规模文档集合设计,提供复杂推理与精准问答的评估基准。实验表明,ViDoRAG在准确率和效率上优于传统方法,未来将优化系统性能并降低计算成本。
1363 63