资讯动态

第二篇:内存——你的变量到底存在哪

发布时间:2026/8/12 22:23:35 来源:尧图企业网站定制
两个new出来的对象为什么一个快一个慢先看一段代码publicclassMemoryDemo{privateintage25;// 这个age存在哪publicvoiddoSomething(){intcount0;// 这个count存在哪Stringname张三;// 张三这个对象呢ObjectobjnewObject();// new出来的对象存在哪}}你可能能答出来age在堆里count在栈里张三在字符串常量池里new Object()也在堆里。但如果我问你为什么有些数据放在栈里有些放在堆里堆和栈到底有什么区别什么时候栈会溢出为什么局部变量比全局变量快这些问题是对你的代码由谁执行、怎样执行的进一步追问。上一篇文章我们讲清了CPU和IO的分工——CPU执行指令时极快但等待IO时主动让出时间片。现在CPU要去内存里取数据了内存到底长什么样CPU怎么找到你的变量整个过程为什么有快有慢一、内存的分层——不是所有内存都一样快你可能以为计算机的内存就是一块大数组访问任何一个位置速度都一样。但实际的内存体系远比这复杂得多CPU核心最快0.3ns ↓ 寄存器1ns容量几十个字节——CPU运算的临时草稿纸 ↓ L1缓存~1ns容量32KB——每个核心独享 ↓ L2缓存~3ns容量256KB——每个核心独享 ↓ L3缓存~12ns容量几MB——所有核心共享 ↓ 主内存~60ns容量几GB到几百GB——我们常说的内存条 ↓ 本地磁盘 / 网络存储毫秒级容量TB级——不属于内存但CPU最终还是要从这里读数据越往上越快但容量越小、成本越高。越往下容量越大、但速度呈指数级下降。CPU从寄存器拿数据不到1纳秒从主内存拿数据约60纳秒——差了上百倍。上一次我们从CPU切到了IO的视角看到了CPU和网络的差距。这次从CPU切到了内存的视角差距虽然没有网络那么大10ms vs 60ns但仍然是一个数量级的倍数。优化CPU缓存命中率让数据尽量留在L1/L2缓存里是现代程序性能优化的核心手段。二、当进程启动时——操作系统给了你一整块虚拟地盘在你的Java程序启动之前操作系统先做了一件事为这个进程分配虚拟地址空间。所谓虚拟地址空间就是操作系统给每个进程画的一块大饼——一个完全属于你自己的、从0到最大地址的连续空间。你在这个空间里可以任意读写但实际上这些地址不是真实的物理内存地址。操作系统维护着一张映射表把你虚拟地址翻译到真正的物理内存页上。进程的虚拟地址空间简化版 ┌──────────────────────────────┐ 高地址 │ 内核空间 │ 操作系统保留用户程序不能访问 ├──────────────────────────────┤ │ 栈区 │ 局部变量、方法调用栈帧 │ ↓ 向下增长 │ 由编译器自动管理 ├──────────────────────────────┤ │ 空白区域 │ 栈和堆之间的缓冲区——可以动态扩展 │ │ ├──────────────────────────────┤ │ 堆区 │ new出来的对象、数组 │ ↑ 向上增长 │ 由程序员或GC管理 ├──────────────────────────────┤ │ 数据段静态区 │ 静态变量、常量 ├──────────────────────────────┤ │ 代码段文本段 │ 编译后的机器指令只读 └──────────────────────────────┘ 低地址栈从高地址向低地址增长堆从低地址向高地址增长。中间留了空白区域给它们各自扩展。如果栈增长太多递归太深、局部变量太大它就会撞上堆——这就是栈溢出。如果堆增长太多创建了太多对象它就会撞上栈——就是堆内存溢出。三、栈——为什么快因为简单到极致栈只做一件事记住谁调用了谁以及每个方法的局部变量是什么。每次调用方法都压入一个栈帧publicvoida(){intx1;// x 在 a 的栈帧里b();// 调用 bb 的栈帧压到上面// b 返回后b 的栈帧被弹出x 继续使用}publicvoidb(){inty2;// y 在 b 的栈帧里c();// 调用 c}publicvoidc(){intz3;// z 在 c 的栈帧里}每次调用一个方法就在栈顶压入一个新的栈帧里面放着这个方法的局部变量。方法返回时这个栈帧直接弹出——没有碎片不需要垃圾回收只需要移动栈指针。局部变量为什么快因为栈在物理内存中的位置是连续的CPU在访问同一栈帧内的相邻变量时几乎一定能命中L1/L2缓存这是一种极强的空间局部性。栈溢出的原因每个线程的栈大小是固定的JVM默认1MB如果递归太深每一层都压一个新栈帧当栈帧向低地址增长超过了分配的栈空间就触发栈溢出。为什么栈这么快栈的分配和释放只需要移动一个指针——“栈顶指针”。压栈时指针往上移出栈时指针往下移。没有内存碎片不需要垃圾回收不需要找空闲块。正是这种极简的管理方式让栈成为程序中最快的数据存储区。四、堆——为什么相对慢因为需要管理堆里放的是不知道什么时候会死的数据。new Object()创建的对象不知道这会死还是30分钟后被GC回收——因为多个方法可能共享它它被赋值给了某个成员变量或者其他对象引用了它。所以堆需要一套复杂的管理机制——垃圾回收器。堆的结构堆被JVM分成两大区域新生代和老年代。新生代又细分为一个Eden区和两个Survivor区。新创建的对象先进入Eden区。经过一次Minor GC后存活的对象被复制到Survivor区。多次GC后仍然存活的对象最终晋升到老年代。这种分代回收的设计基于一个统计规律——绝大多数对象命短活过几次GC的对象大概率还会继续活下去所以优先在新生代回收成本低效率高。GC如何找到堆里的垃圾JVM通过可达性分析判断一个对象是否还活着。从GC Roots出发——包括栈上的局部变量、静态变量、活跃线程的引用——如果能顺着引用链到达某个对象那它就是活着的如果从所有GC Roots都无法到达它它就是垃圾可以被回收。为什么堆比栈慢堆的分配需要找空闲空间GC需要扫描存活对象并回收垃圾大对象可能直接进入老年代触发Full GC。这些都引入了额外的CPU开销。而且堆里的对象分散在内存各处CPU缓存不容易命中——连续访问多个堆对象时它们可能在物理内存中隔得很远L1/L2缓存频繁miss每次miss都要等主内存把数据送来多花几十纳秒。而栈是连续内存区域CPU提前预取相邻的栈帧数据——这是栈比堆快的硬件级原因。五、一个变量从创建到消亡——完整的生命周期现在我们可以完整回答开头的问题了。publicclassMemoryDemo{privateintage25;// ← 这个age存在哪publicvoiddoSomething(){intcount0;// ← 这个count存在哪Stringname张三;// ← 张三这个对象呢ObjectobjnewObject();// ← new出来的对象存在哪}}变量存在哪生命周期由谁管理age堆对象的一部分和MemoryDemo实例同生共死GCcount栈当前栈帧方法返回即消失编译器自动管理张三堆中的字符串常量池类加载后一直存在GCnew Object()堆新生代Eden区垃圾回收判定为垃圾时回收GC为什么不同变量放在不同区域因为它们的生死规则不同。count是局部变量方法结束它就死了——丢在栈上最简单方法返回时自动弹出。age是成员变量只要对象还活着它就不能死——只能放在堆上交给GC判断它什么时候可以回收。张三是字符串常量整个类生命周期内都存在——放在堆的字符串常量池里。new Object()是运行时动态创建的对象——进入堆的新生代等待垃圾回收。栈是临时工堆是长租客。临时工随叫随走不需要管理长租客需要登记、定期检查还在不在、不在了就清退。六、一个让你真正感到震撼的对比回到本文开头的那个问题一个变量可能在哪些地方如果它在栈上局部变量分配和释放永远只需要移动一个指针。方法调用时压入栈帧——指针移动方法返回时弹出栈帧——指针归位。整个过程没有碎片、没有寻找、没有等待如果它在堆上new出来的对象需要在线程私有的TLAB上分配以避锁竞争、需要GC定期扫描确定死活、可能被移到Survivor区或拷贝到老年代。这些都不是免费的但栈虽然快容量有限默认1MB。这就是为什么递归太深会栈溢出也是为什么大对象必须放在堆上——你没法把一张百万像素的图片塞进1MB的栈帧里。性能差距的根源栈连续分配、连续访问是缓存友好的数据结构——CPU访问栈上连续几个变量的速度可能比访问堆中两个不相邻的对象快几十倍。堆是碎片化的GC后存活对象散落在内存各处CPU缓存命中率远低于栈。这不是快一点和慢一点的差异而是纳秒级和微秒级的差异——数量级的差距。总结整个故事串起来是这样的当你的程序启动时操作系统为进程分配了虚拟地址空间——一个从低到高的完整区域。代码段在最下面数据段在代码段之上堆从数据段之上向上增长栈从最高处向下增长。当你的方法被调用时一个新的栈帧被创建并压入栈中。栈帧里放着这个方法的局部变量。这些变量的分配没有任何管理开销——只移动一个栈指针分配已经完成。当你new一个对象时JVM在堆的Eden区为它分配空间。如果TLAB可用线程私下在自己的分配缓存里完成没有锁开销。对象经过几次垃圾回收后可能被移到Survivor区或老年代。当GC Roots无法再到达它时它的空间被回收。CPU要读写一个变量时先去L1缓存找。L1没有去L2L2没有去L3L3没有才去主内存。栈上变量因为内存连续CPU提前预取相邻的栈帧数据命中率极高。堆上两个相邻的对象在物理内存中可能隔着老远CPU缓存miss的频率远高于栈。理解这一切不是为了背面试题。你写的每行代码都在支配着栈的变化、堆的分配、缓存的命中率。知道这些底层机制你就能写出更适合CPU和内存架构的代码。这个专栏只想说清楚一件事每行代码由谁执行怎样执行。配合后端技术内核的五个专栏Java基础、JVM、并发编程、MySQL、Redis对你的每一行代码的理解从怎么用贯通到为什么这么运行。这是计算机基础专栏的第二篇讲清楚了内存的层级结构和堆栈的完整运作机制。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价