44.熟悉ruby的垃圾收集器

45.用finalizer构建资源安全器

46.认识ruby性能分析工具

47.避免在循环中使用对象字面量

48.考虑记忆性大开销计算

44.熟悉ruby的垃圾收集器

https://ruby-china.org/topics/24242

https://ruby-china.org/topics/28127

GC的作用:1.为新生成的对象分配内存 2.识别垃圾对象 3.从垃圾对象中回收内存;缺点:会损失一些性能,GC在工作的时候会暂停程序。

GC标记-清除机制:遍历对象图,能被访问到的对象会被标记为存活,任何未在第一阶段未被标记的对象会被视为垃圾并清除,之后将内存释放回Ruby或者操作系统。

GC中的对象分类:年轻代对象和年老代对象。这种分类是基于一种前提,大多数的对象生存时间都不会太长,如果一个对象的存活时间很长,那么可以设想,它可能生存很长时间。

年轻代对象和年老代对象的区分:如果年轻代对象在第一阶段的标记中存活下来,那么Ruby的分代式垃圾收集器就把他们提升为年老代对象。

标记阶段的两种模式:主要标记阶段和次要标记阶段。主要标记阶段中,所有的对象都被标记,在次要阶段中,仅仅考虑年轻代对象,并自动标记年老带对象,而不检查能否被访问。这意味着,年老带对象只会在主要标记阶段之后才会被清除。

GC的清除阶段有两种机制:即时机制和懒惰机制。即时模式中,GC会清除所有的未标记对象。如果有很多对象需要被清楚,那么这种模式的开销就很大。懒惰机制是尝试释放尽可能少的对象。

Ruby的内存池被称为堆(heap),堆又被分为内存页,内存页又被分为槽,每一个槽都被用来存储一个对象。

#GC的方法
GC.stat #返回一个hash,包含垃圾收集器中相关的所有信息

45.用finalizer构建资源安全器

#一般在ensure中关闭资源
class Resource
  def self.open(&block)
    resource = new
    block.call(resource) if block
  ensure
    resource.close if resource
  end
end

一般在ensure中关闭资源,但是在某些场合可能不合适,比如需要维持资源一段时间,而该时间超过了ensure子句的时间。

解决方法是通过垃圾收集器,通过手动的方式来保证资源最终被释放。

#使用ObjectSpace的define_finalize和undefine_finalize方法
class Resource
  def initialize
    @resource = allocate_resource
    finalizer = self.class.finalizer(@resource)
    #self被垃圾处理之后,finalizer会被执行
    ObjectSpace.define_finalizer(self, finalizer)
  end

  def close
    #取消所有的define_finalizer
    ObjectSpace.undefine_finalizer(self)
    @resource.close
  end

  def self.finalizer(resource)
    #必须接受参数,这个参数就是需要销毁对象的id值
    lambda { |id| resource.close }
  end
end

经常会出现的典型错误代码

#错误代码
def initialize
  @resource = allocate_resource
  finalizer = lambda { |id| resource.close }
  ObjectSpace.define_finalizer(self, finalizer)
end

作为对象的self被销毁之后,finalizer会被调用执行,这里需要注意的是代表finalizer的proc对象不能指向一个需要销毁对象的引用。这里的proc对象在创建的时候存在的局部变量在proc对象都是可以的,同时这个闭包也会获取self变量(查看元编程block中的闭包说明),如果finalizer以这样的方式保持住resource对象,那么垃圾收集器会一直将它视为访问并永不清除。

总结:不要在一个绑定中创建finalizer proc,该绑定引用了一个注定会被销毁的对象,这会造成垃圾收集器无法释放该对象。

46.认识ruby性能分析工具

查看运行时的工具:profiler, ruby-prof, stackprof, 查看内存使用情况的工具:ruby-prof, stackprof, memory-profiler

使用核心库profiler:查看代码运行的时间以及占比

#使用方法1 使用参数的形式在终端运行
ruby -rprofile demo.rb

#使用方法2 在代码中像一般类库一样引入
require 'profile'

#术语
方法a调用b, 我们认为a是父辈,b是子辈

#执行结果如下
    %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 %time: 执行时间占比
 cumulative seconds: 累计时间(父辈+子辈)
 self seconds: 单独时间(父辈)
 calls: 被调用次数
 self ms/call: 每次被调用单独的时间
 total ms/call: 每次被调用累计的时间

查看具体代码执行的时间,使用ruby-prof,具体使用方法查看ruby

#安装
gem install ruby-prof

#使用示例代码
require 'ruby-prof'

def hi
  100_000.times { }
end

def hello
  10_000.times { }
end

RubyProf.start

hi
hello

result = RubyProf.stop
printer = RubyProf::GraphPrinter.new(result)
printer.print(STDOUT, {})

#rails项目中使用
def perf_test
  if params[:__t]
    RubyProf.start
    yield
    result = RubyProf.stop
    printer = RubyProf::GraphPrinter.new(result)
    printer
  else
    yield
  end
end

使用stackprof

#安装
gem install stackprof

#使用
require 'stackprof'
StackProf.run(out: "./profile.dump") do
  def demo
    1000.times do
      puts "hello world"
    end
  end
end

#查看导出的文件
stackprof profile.dump

47.避免在循环中使用对象字面量

通过下面的代码示例,说明目前在循环生成的对象

errors.any? { |e| %w[F1 F2 F3].include?(e.code)}

#上面的代码会生成4n个对象,如果errors中的参数有n个的话

ruby中每次新对象的创建都会粗发一次部分或者是完全的垃圾回收,很显然,%w[F1 F2 F3]中的元素应该是常量,他们既不会变也不会被赋值给一个变量。通过freeze方法可以将不会变化的对象字面量变成常量。

FATAL_CODES = %w[F1 F2 F3].map(&:freeze).freeze

def fatal?(errors)
  errors.any? { |e| FATAL_CODES.include?(e.code)}
end

冻结常量以防止其被修改,这3个对象只有在代码加载时才会被创建,而不是每次循环都新创建一次。

在ruby2.1及更高的版本中,冻结字符串字面量量,相当于把它作为常量,下面的例子中块只有在第一次被调用时,分配一个字符串。而且这个被冻结的字符串字面量可以被整个程序共享。

注意:这个优化只有在冻结一个字符串字面量的时候才起作用,对于一个随意的字符串对象,它是不会有这种优化效果的。

errors.any? { |e| e.code == "FATAL".freeze }

48.考虑记忆性大开销计算

介绍记忆化技术(memoization),这是一种优化技术,比如常用的获取当前的用户对象的语句

#获取当前登录对象
def current_user
  @current_user ||= User.find(logged_in_user_id)
end

使用记忆化技术的时候需要考虑的问题:

1.这本质上是一种缓存技术,考虑缓存的时间

2.需要生成方法使得@current_user 为nil

3.考虑记忆化的方法返回的是冻结对象

4.考虑记忆化方法的副作用,比如上面的副作用是查询数据库。

results matching ""

    No results matching ""