ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ruby Hash内存优化实战:从结构分析到瘦身方案

Ruby Hash内存优化实战:从结构分析到瘦身方案 你是否遇到过这样的场景一个 Ruby 服务运行一段时间后RSS 内存一路平稳上涨看起来没有明显内存泄漏却总觉得进程越来越“重”。用各种工具排查下来很多时候问题并不在某个神秘的 C 扩展而就在你代码里最普通的 Hash 上。Ruby 的 Hash 是极其灵活的键值容器但这种灵活性同样意味着额外的内存开销。当业务数据量达到百万、千万级别时Hash 的内存占用会非常可观。本文围绕Shrinking Ruby Hashes这个主题系统拆解 Ruby Hash 的内存结构、内存占用来源以及缩小 Hash 内存占用的完整实操方案。你会看到如何用内存观测工具量化 Hash 的消耗如何通过删除无用键、冻结字符串、控制初始容量、合理使用默认值等手法把内存占用降下来。全文配有可复现的 Ruby 代码示例、优化前后的数据对比以及生产环境落地时的常见问题和注意事项。无论你是正在排查 Ruby 服务内存问题的后端开发者还是希望写出更节省资源的 Ruby 代码的进阶学习者这篇文章都值得收藏一份按步骤在自己环境里实测一遍。1. 为什么要关注 Ruby Hash 的内存占用1.1 Hash 是 Ruby 的“万能容器”在 Ruby 的日常开发里Hash 几乎是使用频率最高的数据结构之一。它既能表达配置项也能表示数据库查询结果还能充当缓存对象、请求参数容器、状态存储等。只要是一个键对应一个值的关系大多数开发者第一时间就会想到 Hash。Hash 的好处是极大的灵活性键可以是 Symbol、String、Integer任意对象都能作为键值也可以是任意对象。这种设计让 Ruby 代码写起来非常舒服几乎没有容器层面的束缚。不过灵活的背面就是成本。Ruby 的每个 Hash 对象内部都包含哈希表结构、桶数组、键和值的引用再加上一条链上的元数据实际占用的内存往往比“键数量乘以平均键值大小”要多不少。当业务数据量较小时这些开销几乎可以忽略。但一旦 Hash 成为服务里的核心数据载体比如一个长期驻留的缓存、大批量会话数据、分析任务中的中间结果集Hash 数量会迅速膨胀内存占比就会变得刺眼。尤其是使用默认配置的 Hash内部容量会自动扩容但扩容后即使删除很多键容量也不会立刻收缩这个问题在长生命周期 Hash 中尤其常见。1.2 Hash 的内存开销来自哪里要理解 Shrinking Ruby Hashes 的优化方向首先要知道 Hash 的内存到底花在了哪里。从 Ruby 实现角度看Hash 的固定部分包括对象头、散列表元数据、桶数组指针等。随数据量增长的是桶数组本身以及键和值对象。举一个最简单的例子如果你用字符串作为键Ruby 中每个字符串都是一个独立对象包含字符串缓冲区、编码信息、对象头等如果你用 Symbol 作为键每个 Symbol 在进程生命周期内不会重复且是不可变的但 Symbol 表本身也会占用内存。因此Hash 的内存开销可以大致拆成三个部分桶数组占用Hash 为了保持查询效率桶数组通常保持一定冗余容量负载因子约为 0.75 左右。容量一般按 2 的幂次增长因此最后一个桶可能只用了三分之一。键对象占用每次向 Hash 写入新键时键对象会被 Hash 持有引用。String 键如果内容相同但对象不同会存在多个重复字符串对象内存浪费明显。值对象占用值的类型决定了值大小。如果值是数组、嵌套 Hash 或自定义对象还会继续递归占用内存。这不是说 Hash 本身设计有问题而是所有采用开放寻址或链地址法的哈希表都存在的通用代价。关键是意识到这些代价后有针对性地做“瘦身”。1.3 “Shrinking Ruby Hashes”到底在优化什么“Shrinking Ruby Hashes”并不是一个只存在于理论中的概念它指的是通过一系列手段减少 Hash 对象及其内部元素对内存的占用从而降低进程 RSS提高服务稳定性。常见的优化维度包括减少键值对数量删除无用数据、定期清理过期缓存。压缩键类型使用 Symbol 键、冻结字符串、复用字符串对象。控制 Hash 容量提前设置合理初始大小避免反复扩容。替换数据结构在大字典场景下用数组、Set、Struct 等更轻量的结构替代普通 Hash。避免意外的大容量残留对长期存活的 Hash 做 compact 或重新构建。接下来的章节会围绕这些维度一一展开并给出可直接运行的 Ruby 代码。2. 环境准备与内存观测方法2.1 Ruby 版本环境本文示例基于常见的 Ruby 3.x 环境编写代码不需要特殊 gem 即可运行。不过 Ruby 版本迭代很快不同的 Ruby 实现如 CRuby、JRuby、TruffleRuby对 Hash 内部结构和内存统计方式并不完全一致因此建议你在自己的环境里验证一遍。下面是我的本地测试环境参考$ ruby -v ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux]如果你的版本是 Ruby 2.7 或 Ruby 3.2、3.3大部分 API 行为是一样的。真正有差异的是内存布局和内部优化所以本文不会写死某个具体版本的数据而是教给你一套测量和对比的方法。运行内存观测代码前可以先把 GC 调成保守模式避免垃圾回收干扰读数。2.2 使用 ObjectSpace 观测 Hash 占用Ruby 标准库提供了ObjectSpace模块可以遍历进程内所有对象。当我们需要统计某个 Hash 及其内部元素占用多少内存时最常见的方法是ObjectSpace.memsize_of它能返回对象本身占用的内存字节数。不过要小心一点memsize_of对 Hash 的统计粒度在不同版本中并不完全一样。它通常包含 Hash 内部桶数组的占用但不一定递归统计每个键值对中嵌套对象的大小。因此我们在做优化对比时最好固定使用同一环境、同一阶段的统计方法再看前后差异。下面是一个基础的观测代码片段require objspace def hash_memory(hash) ObjectSpace.memsize_of(hash) end sample {} 1000.times do |i| sample[i.to_s] value_#{i} end puts Hash 自身及内部结构内存: #{hash_memory(sample)} bytes这段代码创建了一个包含 1000 个键值对的 Hash打印它自身和内部结构的内存占用。实际运行结果会受 Ruby 版本和系统架构影响你可以在自己机器上先跑一次记录基线数据。后续所有优化操作都可以用同样的函数做前后对比。2.3 用 benchmark 对比优化效果内存优化不能只看内存还要看性能是否被牺牲得太多。比如删除无用键可以减少内存但如果每次删除都触发大量复制就得不偿失。所以我们通常把内存测量和耗时测量放在一起。Ruby 标准库的benchmark模块可以很方便地计算代码块耗时require benchmark time Benchmark.realtime do 1000.times do |i| hash[i.to_s] value_#{i} end end puts 创建 1000 个键值对耗时: #{time.round(4)}s在实际项目中建议把内存占用和耗时对比放到同一个脚本里生成一个简单的优化报告。这样你能直观看到某次调整是“内存下降、耗时基本不变”还是“内存下降、耗时明显变差”。3. Ruby Hash 的内存模型与核心参数3.1 默认容量与负载因子Ruby Hash 底层采用哈希表结构为了保证查询性能桶数组会保持足够的空位。简单理解普通空 Hash 并不是从容量 1 开始增长而是拥有一个内置的初始容量当元素数量超过阈值时Hash 会重新分配更大的桶数组并把现有键值对重新哈希到新桶中。这种自动扩容机制很适合通用场景但对内存敏感的业务来说有两个问题扩容时机的不可控性如果不断插入数据Hash 会在容量达到 75% 左右时自动扩容通常按倍数增长。最终桶数组容量会远大于键值对数量。删除键后的容量残留Hash 删除部分键后桶数组并不会立刻缩小因为缩容会影响现有键的分布和整体性能Ruby 默认并不会频繁触发缩容。因此如果你的 Hash 是长期驻留的缓存并且经常“先填满、后删除一批”就可能出现内部容量很大但实际键很少的情况。这时重建 Hash 或使用rehash效果有限更直接的手段是创建新 Hash 并只复制需要的键值对。3.2 Symbol、String 与冻结字符串在 Ruby Hash 中键的类型对内存影响非常大。如果使用 String 作为键每次新增一个键时该 String 对象会被 Hash 持有。两个内容完全相同的字符串在内存中是两个不同的对象分别占用各自的缓冲区。如果业务中大量使用字符串键比如从 JSON 解析出来的 Hash键名反复创建时会带来很大的字符串对象开销。对比之下Symbol 作为键时相同的 Symbol 在进程中只有一份Symbol 本身不可变查找速度也更快。但 Symbol 也有代价Symbol 不会被 GC 回收现代 Ruby 中 Symbol 表可能受 GC 管理但仍是进程级全局表如果动态创建大量 Symbol同样会导致内存增长。所以推荐的方式是在固定键名场景使用 Symbol 或冻结字符串。冻结字符串用String#freeze标记为不可变Ruby 在遇到相同内容的冻结字符串字面量时有机会复用对象。从 Ruby 2.1 开始文件内出现的冻结字符串字面量可以被自动去重在 Ruby 3.x 中如果启用了# frozen_string_literal: true魔术注释字符串字面量默认冻结。# frozen_string_literal: true hash {} 1000.times do |i| hash[user_#{i}] i end在这个例子里user_#{i}是插值字符串插入后产生的是新字符串对象frozen_string_literal并不能让动态插值结果自动去重。因此对于动态键更实用的是手动冻结并复用已有字符串或使用 Symbol。3.3 Hash 的默认值与默认块Hash.new 可以传入默认值或默认块这会影响“读取缺失键时返回什么”。很多开发者不明白Hash 的默认设置对内存也有影响。需要注意以下几点Hash.new(0)的默认值 0 是同一个共享对象读取缺失键不会自动写入 Hash内存占用不大。Hash.new { |h, k| h[k] [] }这种带块的默认值非常常用但它每次访问缺失键都会执行块并且如果块里写了h[k] ...就会向 Hash 里插入新键可能造成内存膨胀。如果你只是想安全读取一个 Hash不希望它因为“读取”而写入键值对就不要在默认块里做赋值。一个典型的反例是统计词频时counter Hash.new { |hash, key| hash[key] 0 } words.each { |word| counter[word] 1 }这段代码看起来没问题但它会在读取不存在的键时自动创建键值对且默认值都是 0。如果 words 数量极大这个 Hash 会包含所有出现过的词可能正是你需要的但也可能是你内存暴涨的原因。理解这一行为你才能判断该不该用Hash.new(0)再手动累加。3.4 compare_by_identity 的影响Ruby Hash 默认用eql?和hash方法比较键这意味着内容相同的字符串键会被视为同一个键。如果启用compare_by_identityHash 会比较对象身份即使是两个内容完全相同的字符串也视为不同键。h {} h.compare_by_identity s1 name s2 name h[s1] 1 h[s2] 2 p h.length # 2这个模式在某些场景有性能优势因为省去了对键内容做哈希和相等性比较的时间。但内存上要特别注意它不会帮你合并重复内容的键反而会因为对象身份不同而保留多个键对象。所以如果目标是 Shrinking Ruby Hashescompare_by_identity通常不是首选方向只有在明确需要对象身份语义时才使用。4. Shrinking Ruby Hashes 的完整实战4.1 模拟业务场景用户会话缓存下面我们构建一个贴近真实业务的场景一个长期驻留在内存中的用户会话缓存。假设一个 Web 服务会从数据库读取用户信息并以用户 ID 为键把用户信息 Hash 作为值缓存起来。代码结构如下# 文件路径session_cache.rb class SessionCache def initialize cache {} end def load_user(user_id) cache[user_id] || fetch_user_from_db(user_id) end def cleanup(active_ids) cache.keys.each do |key| cache.delete(key) unless active_ids.include?(key) end end private def fetch_user_from_db(user_id) { id: user_id, name: user_#{user_id}, email: user_#{user_id}example.com, tags: [ruby, hash, memory], address: { city: City, street: Street #{user_id} } } end end这里cache是业务上的核心 Hash。运行一段时间后它可能包含几万甚至几十万个用户信息 Hash。每个用户信息 Hash 里又有符号键、字符串值、数组、嵌套 Hash。我们要做的第一件事是量化这种结构的开销然后逐步优化。4.2 第一批优化删除无用键与 compact先说最容易见效的两步删除无用键、压缩冗余值。cleanup方法本身已经会删除非活跃会话但在真实项目里这个清理操作不一定会被频繁调用。你可能只在内存告警时手动清理或者在每天低峰期清理一次。这种低频清理会让 Hash 在很长一段时间里保留大量过期数据内存自然降不下来。改进方向是提高清理频率同时使用更精确的过期判断。比如给缓存项增加过期时间字段定期扫描并删除过期项。这个方案会提升 CPU 开销但能显著降低峰值内存。另外Hash 值里的tags数组如果是默认[ruby, hash, memory]每个用户都复制一份就完全没有必要。可以把它提取为共享常量COMMON_TAGS [ruby, hash, memory].freeze def fetch_user_from_db(user_id) { id: user_id, name: user_#{user_id}, email: user_#{user_id}example.com, tags: COMMON_TAGS, address: { city: City, street: Street #{user_id} } } endfreeze之后所有用户共享同一个数组对象内存消耗立刻降下来。Ruby 2.6 之后还提供了Hash#compact可以返回一个新的 Hash去掉所有值为 nil 的键值对Hash#compact!则原地修改。如果业务 Hash 中经常出现临时写入了 nil 值但又不想删除键的情况compact非常有用。data { name: Alice, email: nil, tags: [ruby] } p data.compact # {:nameAlice, :tags[ruby]}在长期缓存中建议在清理任务里做一次compact!把值为 nil 的键彻底删除避免值对象占位。4.3 第二批优化冻结字符串与合理默认值继续看缓存里的字段。name、email、street都是动态字符串内容因用户不同而不同无法简单共享。但城市字段city: City在所有用户中可能是同一个字符串可以提取成常量并冻结DEFAULT_CITY City.freeze再进一步很多用户信息的name是user_#{user_id}这种规则化命名。如果业务允许可以考虑不存储 name 字段而是在读取时动态生成。这样可以省掉大量字符串对象。默认值方面cache[user_id] || fetch_user_from_db(user_id)这段代码有一个隐藏问题每次读取不存在的用户时都会调用数据库并创建 Hash。如果大量请求都在读取不存在的数据缓存里会塞满无意义的键值对。更合理的写法是def load_user(user_id) if cache.key?(user_id) cache[user_id] else user fetch_user_from_db(user_id) cache[user_id] user user end end这样至少能避免“读取”操作本身导致缓存体积膨胀。这里的原则是在内存敏感的 Hash 上尽量显式判断 key?不要用||无条件写入占位值。4.4 第三批优化控制初始容量与结构替换当缓存的数据量级可以预期时可以给 Hash 设置合理的初始容量。Ruby 的Hash.new并不直接支持指定初始容量但可以通过Hash.new配合Hash#rehash或者直接使用Hash.new(capacity)的底层内部优化实际上CRuby 允许在创建 Hash 时传入容量参数但这个能力不够稳定也不建议依赖。更通用的做法是先预估数据量一次性创建足够大的 Hash避免后续频繁扩容。扩容过程需要重新计算哈希、重新分配桶数组既有 CPU 开销也会造成临时内存峰值。例如expected_size 10_000 cache {} cache Hash.new这样并不能指定容量所以更实际的策略是如果缓存数据量很大且经常需要重建不如直接用数组或 Struct 替代部分 Hash 结构。用户信息这种“字段固定”的结构很适合用Struct或Data定义UserInfo Struct.new(:name, :email, :tags, :city, :street, keyword_init: true) def fetch_user_from_db(user_id) UserInfo.new( name: user_#{user_id}, email: user_#{user_id}example.com, tags: COMMON_TAGS, city: DEFAULT_CITY, street: Street #{user_id} ) endStruct 实例比同等 Hash 更轻量因为它不需要维护桶数组和哈希元数据字段访问直接通过偏移量完成。如果你的数据字段固定且不会随意增删这是 Shrinking Ruby Hashes 的有力手段。4.5 优化前后的内存数据对比我们把优化过程整合到一个脚本中分别模拟优化前和优化后的用户信息缓存比较内存占用。require objspace require benchmark COMMON_TAGS [ruby, hash, memory].freeze DEFAULT_CITY City.freeze UserInfo Struct.new(:name, :email, :tags, :city, :street, keyword_init: true) def old_user_info(user_id) { name: user_#{user_id}, email: user_#{user_id}example.com, tags: [ruby, hash, memory], city: City, address: { street: Street #{user_id} } } end def new_user_info(user_id) UserInfo.new( name: user_#{user_id}, email: user_#{user_id}example.com, tags: COMMON_TAGS, city: DEFAULT_CITY, street: Street #{user_id} ) end count 20_000 old_hash {} new_hash {} GC.start memory_before ObjectSpace.memsize_of(old_hash) ObjectSpace.memsize_of(new_hash) old_time Benchmark.realtime do count.times { |i| old_hash[i] old_user_info(i) } end new_time Benchmark.realtime do count.times { |i| new_hash[i] new_user_info(i) } end GC.start old_memory ObjectSpace.memsize_of(old_hash) new_memory ObjectSpace.memsize_of(new_hash) puts 优化前 Hash 占用: #{old_memory} bytes, 构建耗时: #{old_time.round(4)}s puts 优化后 Hash 占用: #{new_memory} bytes, 构建耗时: #{new_time.round(4)}s puts 内存下降: #{((old_memory - new_memory) * 100.0 / old_memory).round(2)}%注意memsize_of对 Hash 的统计在不同 Ruby 版本中表现并不完全一致它可能不递归统计 Struct 内部字符串。因此这里的“内存下降”更多是结构性对比实际 RSS 变化需要通过/usr/bin/time -v或ps观察完整进程内存才能得到更准确结论。这个脚本的核心意义在于它把优化动作变成了可重复的测量实验。你可以根据真实业务调整结构并在自己环境里跑出最可信的对比结果。5. 常见问题与排查思路5.1 为什么 GC 已经运行了内存还是没有降下来问题现象常见原因解决思路调用GC.start后 RSS 高居不下Hash 内部桶数组仍然存活数据对象被 Hash 持有检查是否有全局变量、常量或长生命周期缓存引用 HashHash 删除了大量键但内存占用几乎不变桶数组没有缩容重建 Hash把剩余键值对复制到新 Hash单个 Hash 很小但总内存很大创建了大量重复的小 Hash用数组、Struct、Data 替代固定结构 Hash第一个问题非常典型。很多人以为调用GC.start就能强制回收一切但 Ruby 的 GC 只能回收“不可达对象”。如果你的 Hash 仍然挂在某个全局变量或缓存对象下GC 即使执行也不会把它回收。第二个问题对应前面提到的容量残留。删除键只是减少了 Hash 中的活动键值对数量桶数组可能仍然保持扩容后的容量。你可以建立一个新 Hash把原 Hash 的键值对重新塞进去然后让旧 Hash 离开作用域。5.2 如何确认内存是被 Hash 占用的如果你怀疑 Hash 是内存大户可以先定位进程里最大的对象集合。Ruby 提供了一个弱引用遍历方法ObjectSpace.each_object可以统计各类对象的数量但遍历所有对象也会带来一定性能开销不适合在线上频繁执行。更稳妥的方式是在测试环境复现问题用ObjectSpace.memsize_of_all(Hash)统计所有 Hash 的总占用。通过GC.stat观察堆状态。用ps -o rss或/usr/bin/time -v观察进程 RSS。下面是一个统计脚本示例require objspace GC.start total_hash_memory ObjectSpace.memsize_of_all(Hash) hash_count ObjectSpace.each_object(Hash).count puts Hash 数量: #{hash_count} puts Hash 相关内存: #{total_hash_memory} bytes如果你在一个大型 Rails 应用里跑这个脚本数据可能会让你吓一跳。Hash 数量往往高达数十万甚至上百万内存占比非常可观。5.3 compact 之后为什么容量还是很大Hash#compact只负责去掉值为 nil 的键值对并不会触发底层存储的缩容。这就像在一个大数组里删掉几个元素数组的容量不会自动缩小。若想真正缩小容量需要把数据迁移到新 Hash。data { a: 1, b: nil, c: 3 } data data.compact # 新 Hash容量按实际键数分配这里的data.compact返回的是新对象新对象会根据实际键数量分配相对紧凑的桶。如果原 Hash 本来就有很多冗余容量这个操作可以带来明显的内存下降。但要注意如果原 Hash 是被多处引用的共享对象直接替换变量名并不能解决问题你需要保证所有引用方都指向新 Hash。生产环境改动前最好先检查引用关系。5.4 冻结字符串后性能变差怎么办冻结字符串能帮助复用对象但某些场景下却可能让性能变差。比如频繁创建同一个动态字符串然后立即存入 Hash冻结与否对内存影响不大因为字符串本身就是新建的临时对象使用后会被回收。如果优化后发现性能下降可以检查是否是过度冻结导致的比如你把一个需要频繁修改的 String 也 freeze 了导致每次修改都要 dup 新对象。实际上冻结字符串的核心收益是复用固定内容而不是冻结所有字符串。优化的正确姿势是先测量再优化只对真正产生大量重复对象或长生命周期对象的场景做冻结。6. 工程实践与生产建议6.1 明确何时值得优化Shrinking Ruby Hashes 不是让你把所有 Hash 都改掉那是过度优化。比较值得投入的场景包括长生命周期缓存比如会话缓存、权限缓存、配置缓存。大数据量处理任务比如一次加载百万行数据再聚合。高并发服务中的共享 Hash可能被数百个请求线程同时读取。内存告警频繁、每次扩容都需要重启的服务。在这些场景下做一次 Hash 内存画像往往能收获非常可观的收益。而如果只是一个局部方法里的临时 Hash用完即弃优化它的意义就很有限。6.2 在监控体系里加入 Hash 内存指标生产环境里不能只靠人肉分析。建议在监控指标中加入与 Hash 相关的关键数据Hash 对象总数。Hash 相关的memsize_of_all总量。进程 RSS 和GC.stat中的堆页数。长生命周期缓存的实际键数量。通过时间序列对比你能看到 Hash 数量是否随业务增长而线性增长还是出现了异常突刺。发现问题后再结合链路追踪定位是哪个业务模块在创建大量 Hash。6.3 借鉴分桶和分片思想当单个 Hash 大到难以优化的程度比如内存集中在某个超大 Hash 上可以考虑分桶或分片。分桶的核心思想是把一个“巨型 Hash”拆成多个“小型 Hash”比如按用户 ID 哈希值取模分到 16 个或 32 个子 Hash 中。这样带来的额外好处是单个 Hash 的桶数组容量更小减少容量残留。可以按桶粒度做缓存淘汰降低 GC 压力。在多线程场景下可以按桶加锁减少竞争。这个思路在 Redis Hash 分桶方案中也经常被提到本质上是把大对象拆小让内存和并发控制更加可控。但对于一般 Ruby on Rails 应用来说如果一个 Hash 大到需要分桶你可能应该先考虑是否真的需要长期持有这么多数据而不是急着写分桶逻辑。6.4 不要盲目追求“更小”的数据结构优化内存的同时要关注代码可读性和数据安全。比如我们前面用 Struct 替代 Hash虽然内存更小但 Struct 的结构是固定的新增字段需要修改定义对后续迭代会产生一定约束。如果你的业务 Hash 字段经常变化Struct 就不一定合适。另一个极端是有些开发者为了省内存把多个字段拼成一个字符串再压缩存储。这在某些极嵌入式场景或许可行但在 Web 服务中会让代码变得难以理解还增加了序列化开销通常不建议。正确的工程态度是在满足可读性、可维护性、性能要求的前提下选择合适的数据结构而不是只看“谁内存最小”。7. 总结与下一步这篇文章围绕 Shrinking Ruby Hashes系统介绍了 Ruby Hash 的内存模型、观测方法和立体化优化手段。你看到了 Hash 内存开销主要来自桶数组、键对象、值对象和额外的元数据也看到了删除无用键、使用 compact、冻结字符串、提取共享常量、控制初始容量、用 Struct 替代 Hash 等具体做法。如果你正准备优化项目中的 Hash 内存可以从一个小模块开始先写一个内存测量脚本记录当前 Hash 的内存情况和构建耗时再按本文的优化顺序逐步调整每做完一步对比一次数据和写入性能。这样你能清楚知道每一步带来了什么改变。下一步可以继续研究的方向包括Ruby GC 参数对内存的影响、弱引用WeakMap在缓存场景的用法、以及用Ractors或线程隔离减少共享 Hash 的竞争。如果你在优化过程中遇到了有意思的案例欢迎在评论区分享大家互相借鉴。如果本文对你有帮助可以收藏备用也欢迎转发给正在为 Ruby 内存问题头疼的朋友。
返回列表