Ruby会在堆空间中维护一个对象池,这个对象池也被称作
ObjectSpace
, 我们所使用的所有Ruby对象全部是从这个池子中取出的,而Ruby GC也会去清理池子中已经没有被使用的对象,达到循环利用的目的。
ObjectSpace
对象池是由很多堆页(heap page
)构成的,而每一个heap page页的大小为16Kb。 每页中包含408个槽(slot),因此每个slot的大小为40 bytes。 每个slot槽对应一个对象,你所使用的每一个对象都在对象池中占有一个槽。这些数据可以通过下面的方式得到:
GC::INTERNAL_CONSTANTS
=> {:RVALUE_SIZE=>40, :HEAP_PAGE_OBJ_LIMIT=>408, :HEAP_PAGE_BITMAP_SIZE=>56, :HEAP_PAGE_BITMAP_PLANES=>4}
# 一个RVALUE结构体40个字节:HEAP_PAGE_OBJ_LIMIT=>408
Ruby有一种称为RValue的结构,对象被存放在这个结构体中,除了对象之外,还附有一个flag,也就是在mark-and-sweep机制被mark时要使用的flag。
在上图的栈空间中存放的是 当前运行栈的RVALUE指针和值,而堆空间存放的是RVALUE的值,按照Ruby2.1以下版本默认初始化10000个slots 计算,结果就是堆空间默认包含24个heap page。
系统堆
空间存放的是数据,即RVALUE中无法存放的的数据,比如一个1K的字符串,RVALUE只能存放引用和元信息和23个字符,剩下真正的数据是存放在系统管理的堆空间的,数据用完后会被GC释放。
require 'objspace'
ObjectSpace.memsize_of('a') # 0 or 40?
ObjectSpace.memsize_of('a'*23) # 0 or 40 or more ?
ObjectSpace.memsize_of('a'*24) # 0 or 40 or more ?
不会被回收的内存
Ruby 中的常量是永远不会被垃圾回收的,所以如果常量引用了一个对象,那么这个对象也永远不会被垃圾回收,是由于将来 可能 会使用它们。在 Ruby 中一个对象一旦被全局对象引用,它就不会被垃圾回收。 这一原则也适用于常量,全局变量,模块 (modules) 和类 (class)。因此,在全局可访问的任何地方引用对象都要注意这一点。
ruby 的程序会释放内存给操作系统,但是如果一个 heap 里存在不会被垃圾回收的对象,那这个 heap 的空间就不会释放给操作系统,同时内存释放很慢,容易发生内存持续上涨的情况。
RubyGC方式
- Ruby 1.8:简单的标记和扫描
ruby中的GC使用的是 mark and sweep机制,ruby会从根目录扫描全部对象,如果能 reach到这个对象,就使用名为FL_MARK的内部标志之一标记变量,扫描完毕没有被 mark的对象就会被回收。扫描过程是处于 Stop The word 暂停状态。
Ruby 1.9.3:懒惰扫描
标记完成后,不一次性清除所有无用的对象,将清除的过程分散到后面的操作中,这大大降低了单次清除过程中的中断时间。Ruby 2.0中的垃圾收集:位图标记
不是在每个RValue结构中使用FL_MARK位来指示Ruby仍在使用值并且无法释放它,而是将Ruby 2.0中的信息保存在称为“位图”的内容中(位图指映射回RValue结构的文字位集合)
1值等同于在Ruby 1.8或Ruby 1.9进程中设置的FL_MARK标志,而0等效于未设置的FL_MARK标志。换句话说,FL_MARK位已经移出RString和其他对象值结构,并进入称为位图的这个单独的存储区域,允许Unix系统的 Copy -On- Write 跨子进程共享内存。
Ruby 2.1:分代GC
oldgen和minor标记 (一个可以逐步实现并支持C扩展的分代收集器)Ruby2.2:引入增量标记和symbol GC
把主标记
拆分为数个小任务,每个任务分散在Ruby的执行中,这样单次的暂停时间就会变得很短
GC触发条件
- 少于free slots时执行GC (RUBY_GC_HEAP_FREE_SLOTS)
- mallocate 分配16MB以上内存时执行GC ( RUBY_GC_MALLOC_LIMIT ~ RUBY_GC_MALLOC_LIMIT_MAX 32MB区间个字节 具体数字是Ruby根据内存使用情况动态确定的)
系统变量说明:
RUBY_GC_HEAP_INIT_SLOTS: Ruby初始化slots数量,数量越大后续malloc次数越小
RUBY_GC_HEAP_GROWTH_FACTOR:内存分配增长因子
RUBY_GC_MALLOC_LIMIT: GC触发条件之一, 分配内存超过限制后运行GC
RUBY_GC_HEAP_FREE_SLOTS:对堆空间free slot少于该参数时触发GC
如何查看页面分配了多少内存?
rack-mini-profiler包含一个非常方便的报告,用于获取各种页面中内存使用的句柄。 只需在您的网址末尾附加 ?pp=profile-gc :
http://test.com/gc_check?pp=profile-gc
Overview
--------
Initial state: object count: 583769
Memory allocated outside heap (bytes): 109579497
GC Stats:
--------
count : 86
heap_allocatable_pages : 0
heap_allocated_pages : 2602
heap_available_slots : 1060592
heap_eden_pages : 2602
heap_final_slots : 0
heap_free_slots : 238107
heap_live_slots : 822485
heap_marked_slots : 822484
heap_sorted_length : 2602
heap_tomb_pages : 0
major_gc_count : 16
malloc_increase_bytes : 26568
malloc_increase_bytes_limit : 19983014
minor_gc_count : 70
old_objects : 808267
old_objects_limit : 1616534
oldmalloc_increase_bytes : 2575192
oldmalloc_increase_bytes_limit : 46281415
remembered_wb_unprotected_objects : 11399
remembered_wb_unprotected_objects_limit : 22798
total_allocated_objects : 9572514
total_allocated_pages : 2602
total_freed_objects : 8750029
total_freed_pages : 0
New bytes allocated outside of Ruby heaps: 492677
New objects: 6141
ObjectSpace delta caused by request:
-----------------------------------
String : 4241
Array : 735
MatchData : 414
Hash : 321
Integer : 106
ActiveRecord::MigrationProxy : 92
Proc : 49
NewRelic::Agent::Attributes : 28
NewRelic::Agent::Transaction::Segment : 28
IPAddr : 14
Time : 11
可以看到本次访问导致分配了0.5M的内存,6141个新的对象。
注意:由于在禁用 GC 的情况下通过 ObjectSpace 进行迭代,不可避免地运行此报告可能会导致 Ruby 堆增长。 建议您在分析会话后循环生产过程。
count: GC运行的次数,它是由minor_gc_count + major_gc_count 来组成的
minor_gc_count: GC进行的一次小型的回收
major_gc_count: GC进行的是一次full垃圾回收
heap_live_slot: GC运行以来,还存活的对象的个数
heap_free_slots: 空闲slot个数,也就是可以用来存放新的对象的slot的个数
内存优化 jemalloc
GitHub - jemalloc/jemalloc
Ruby 传统上使用 C 语言函数 malloc 在存储对象时动态分配、释放和重新分配内存。 Jemalloc 是由 Jason Evans 开发的 malloc(3) 实现(因此 malloc 开头的首字母为“je”),与其他分配器相比,jemalloc 比其他分配器更好地处理小对像,因为它专注于避免碎片和可扩展的并发支持.
jemalloc 的使用还是比较简单的只需要下载安装,配置一个系统环境变量再重新启动应用成就可以应用jemalloc了,而效果经过我的观察大概可以降低20%~30%的内存占用。
安装jemalloc:
wget https://*.aliyuncs.com/jemalloc-5.3.0.tar.bz2
tar -jxvf jemalloc-5.3.0.tar.bz2
cd jemalloc-5.3.0
./autogen.sh
make
make install
# ~/.bashrc
export LD_PRELOAD=$LD_PRELOAD:/usr/local/lib/libjemalloc.so.2
然后执行:
source ~/.bashrc
echo $LD_PRELOAD
然后重启Ruby应用就可以了,注意 unicorn.sh restart 是不行的,必须停掉进程再重新启动才生效
验证是否使用jemalloc:
lsof -n|grep jemalloc
效果见confluence
FullStap Ruby
由REE 的创建者、Phusion 的首席技术官 Hongli Lai 发布的Fullstaq Ruby。
为什么会有fullstaq ruby?
对于在服务器用例(例如大多数 Ruby 网络应用程序)中使用 Ruby 的人来说,Ruby 核心开发人员有点谨慎/保守并且对采用 Jemalloc 犹豫不决,理由是担心 Jemalloc 可能会导致非服务器用例中的回归,以及 作为其他与兼容性相关的问题。
这就是 Fullstaq Ruby 的用武之地:我们固执己见。 Fullstaq Ruby 不关心非服务器用例,我们喜欢更优的方法,因此我们选择针对该服务器应用进行优化。