ObjectSpace

ObjectSpace

ObjectSpace 모듈은 objspace 라이브러리가 확장해서, 객체·메모리 관리에 대한 내부 통계 정보를 얻는 메서드들을 몇 가지 추가해 줘요. 이 확장 모듈을 쓰려면 require 'objspace'가 필요해요.

일반적으로 말하면, MRI 구현에 대해 잘 모른다면 이 라이브러리를 쓰지 않는 게 좋아요. 주로 (메모리) 프로파일러 개발자나 MRI 메모리 사용량을 알아야 하는 MRI 개발자를 위한 라이브러리거든요. ObjectSpace 모듈은 가비지 컬렉션 기능과 상호작용하는 여러 루틴을 담고 있고, 이터레이터로 살아 있는 모든 객체를 순회할 수 있게 해 줘요.

ObjectSpace는 객체 파이널라이저(finalizer) 지원도 제공해요. 파이널라이저는 특정 객체가 가비지 컬렉션에 의해 파괴된 뒤 호출되는 proc이에요. 이 메서드를 올바르게 쓰는 방법에 대한 중요한 정보는 ObjectSpace.define_finalizer 문서를 꼭 확인하세요.

a = "A"
b = "B"

ObjectSpace.define_finalizer(a, proc {|id| puts "Finalizer one on #{id}" })
ObjectSpace.define_finalizer(b, proc {|id| puts "Finalizer two on #{id}" })

a = nil
b = nil

이 결과는 다음과 같아요.

Finalizer two on 537763470
Finalizer one on 537763480

출처: Ruby 4.0 API

본문

Public Class Methods

allocation_class_path(object) → string

주어진 object에 대한 클래스를 돌려줘요.

class A
  def foo
    ObjectSpace::trace_object_allocations do
      obj = Object.new
      p "#{ObjectSpace::allocation_class_path(obj)}"
    end
  end
end

A.new.foo #=> "Class"

자세한 내용과 예시는 ::trace_object_allocations를 참고하세요.

allocation_generation(object) → integer or nil

주어진 object에 대한 가비지 컬렉터 세대(generation)를 돌려줘요.

class B
  include ObjectSpace

  def foo
    trace_object_allocations do
      obj = Object.new
      p "Generation is #{allocation_generation(obj)}"
    end
  end
end

B.new.foo #=> "Generation is 3"

자세한 내용과 예시는 ::trace_object_allocations를 참고하세요.

allocation_method_id(object) → string

주어진 object에 대한 메서드 식별자를 돌려줘요.

class A
  include ObjectSpace

  def foo
    trace_object_allocations do
      obj = Object.new
      p "#{allocation_class_path(obj)}##{allocation_method_id(obj)}"
    end
  end
end

A.new.foo #=> "Class#new"

자세한 내용과 예시는 ::trace_object_allocations를 참고하세요.

allocation_sourcefile(object) → string

주어진 object가 만들어진 소스 파일을 돌려줘요. 자세한 내용과 예시는 ::trace_object_allocations를 참고하세요.

allocation_sourceline(object) → integer

주어진 object가 만들어진 소스의 원래 라인을 돌려줘요. 자세한 내용과 예시는 ::trace_object_allocations를 참고하세요.

count_imemo_objects([result_hash]) → hash

T_IMEMO 타입별로 객체 수를 세요. 이 메서드는 Ruby 프로그램의 성능과 메모리 사용량에 관심 있는 MRI 개발자를 위한 것이에요. 반환되는 해시는 이렇게 생겼어요.

{:imemo_ifunc=>8,
 :imemo_svar=>7,
 :imemo_cref=>509,
 :imemo_memo=>1,
 :imemo_throw_data=>1}

선택 인자 result_hash가 주어지면 그 값을 덮어쓰고 돌려줘요. 이는 프로브 효과(probe effect)를 피하기 위한 것이에요. 반환 해시의 내용은 구현에 따라 달라지고, 미래에 바뀔 수 있어요. 이 버전에서는 키가 심볼 객체예요. 이 메서드는 C Ruby에서만 동작할 것으로 기대돼요.

count_nodes([result_hash]) → hash

각 노드 타입별로 노드 수를 세요. MRI 개발자를 위한 메서드예요. 반환 해시는 이렇게 생겼어요.

{:NODE_METHOD=>2027, :NODE_FBODY=>1927, :NODE_CFUNC=>1798, ...}

선택 인자 result_hash가 주어지면 덮어쓰고 돌려줘요(프로브 효과 방지). 반환 해시의 내용은 구현에 따라 정의되고 미래에 바뀔 수 있어요. C Ruby에서만 동작할 것으로 기대돼요.

count_objects([result_hash]) → hash

모든 객체를 타입별로 그룹 지어 세요. 반환 해시는 이런 모양이에요.

{
  :TOTAL=>10000,
  :FREE=>3011,
  :T_OBJECT=>6,
  :T_CLASS=>404,
  # ...
}

반환 해시의 내용은 구현에 따라 달라지고 미래에 바뀔 수 있어요. :T_로 시작하는 키는 살아 있는(live) 객체를 뜻해요. 예를 들어 :T_ARRAY는 배열의 개수예요. :FREE는 현재 사용되지 않는 객체 슬롯을 뜻하고, :TOTAL은 위 값들의 합이에요.

선택 인자 result_hash가 주어지면 덮어쓰고 돌려줘요(프로브 효과 방지).

h = {}
ObjectSpace.count_objects(h)
puts h
# => { :TOTAL=>10000, :T_CLASS=>158280, :T_MODULE=>20672, :T_STRING=>527249 }

이 메서드는 C Ruby에서만 동작할 것으로 기대돼요.

count_objects_size([result_hash]) → hash

각 타입별로 객체 크기(바이트)를 세요. 이 정보는 불완전하다는 점에 주의하세요. 그저 HINT(힌트)로만 다뤄야 해요. 특히 T_DATA의 전체 크기는 틀릴 수 있어요. 반환 해시는 이렇게 생겼어요.

{:TOTAL=>1461154, :T_CLASS=>158280, :T_MODULE=>20672, :T_STRING=>527249, ...}

선택 인자 result_hash가 주어지면 덮어쓰고 돌려줘요. 반환 해시의 내용은 구현에 따라 정의되고 미래에 바뀔 수 있어요. C Ruby에서만 동작할 것으로 기대돼요.

count_symbols([result_hash]) → hash

각 Symbol 타입별로 심볼을 세요. MRI 개발자를 위한 메서드예요. 선택 인자 result_hash가 주어지면 덮어쓰고 돌려줘요. 반환 해시의 내용은 구현에 따라 정의되고 미래에 바뀔 수 있어요. C Ruby에서만 동작할 것으로 기대돼요.

이 버전의 MRI에는 심볼 타입이 3가지 있고(총 개수 1가지 더), 다음과 같아요.

* mortal_dynamic_symbol: GC 대상 심볼 (GC에 의해 수집됨)
* immortal_dynamic_symbol: dynamic 심볼에서 승격된 불멸 심볼 (GC에 의해 수집되지 않음)
* immortal_static_symbol: 불멸 심볼 (GC에 의해 수집되지 않음)
* immortal_symbol: 전체 불멸 심볼 (immortal_dynamic_symbol+immortal_static_symbol)

count_tdata_objects([result_hash]) → hash

T_DATA 타입별로 객체를 세요. MRI 개발자를 위한 메서드예요. 반환 해시는 이렇게 생겼어요.

{RubyVM::InstructionSequence=>504, :parser=>5, :barrier=>6,
 :mutex=>6, Proc=>60, RubyVM::Env=>57, Mutex=>1, Encoding=>99,
 ThreadGroup=>1, Binding=>1, Thread=>1, RubyVM=>1, :iseq=>1,
 Random=>1, ARGF.class=>1, Data=>1, :autoload=>3, Time=>2}
# T_DATA objects existing at startup on r32276.

선택 인자 result_hash가 주어지면 덮어쓰고 돌려줘요. 반환 해시의 내용은 구현에 따라 달라지고 미래에 바뀔 수 있어요. 이 버전에서는 키가 Class 객체이거나 Symbol 객체예요. 객체가 일반적인(접근 가능한) 객체라면 키는 Class 객체이고, 내부(internal) 객체라면 rb_data_type_struct가 등록한 심볼 이름이 키예요. C Ruby에서만 동작할 것으로 기대돼요.

define_finalizer(obj, aProc=proc())

aProc를 파이널라이저로 추가해요. obj가 파괴된 뒤 호출돼요. obj의 객체 ID가 aProc의 인자로 전달돼요. aProc이 lambda나 method이면 단일 인자로 호출될 수 있게 해야 해요. 반환 값은 배열 [0, aProc]이에요.

권장 패턴은 두 가지예요. 하나는 필요한 상태를 안전하게 캡처할 수 있는 비-인스턴스 메서드에서 파이널라이저 proc을 만드는 것이고, 다른 하나는 필요한 상태를 인스턴스 변수로 명시적으로 저장하는 커스텀 callable 객체를 쓰는 거예요.

class Foo
  def initialize(data_needed_for_finalization)
    ObjectSpace.define_finalizer(self, self.class.create_finalizer(data_needed_for_finalization))
  end

  def self.create_finalizer(data_needed_for_finalization)
    proc {
      puts "finalizing #{data_needed_for_finalization}"
    }
  end
end

class Bar
 class Remover
    def initialize(data_needed_for_finalization)
      @data_needed_for_finalization = data_needed_for_finalization
    end

    def call(id)
      puts "finalizing #{@data_needed_for_finalization}"
    end
  end

  def initialize(data_needed_for_finalization)
    ObjectSpace.define_finalizer(self, Remover.new(data_needed_for_finalization))
  end
end

주의할 점이 있어요. 파이널라이저가 파이널라이즈 대상 객체를 참조하면 GC에서 절대 실행되지 않아요. 다만 종료(exit) 시에는 여전히 실행돼요. 파이널라이즈 대상 객체를 파이널라이저의 receiver로 캡처하면 경고가 나와요.

class CapturesSelf
  def initialize(name)
    ObjectSpace.define_finalizer(self, proc {
      # this finalizer will only be run on exit
      puts "finalizing #{name}"
    })
  end
end

그리고 파이널라이제이션은 예측할 수 없고, 종료 시를 제외하면 실행이 보장되지 않아요.

each_object([module]) {|obj| ... } → integer / each_object([module]) → an_enumerator

이 Ruby 프로세스 안의 살아 있는, non-immediate 객체마다 블록을 한 번씩 호출해요. module이 지정되면 그 클래스·모듈과 일치하거나(또는 서브클래스) 해당하는 객체에 대해서만 블록을 호출해요. 발견된 객체 수를 돌려줘요. Immediate 객체(Fixnum, 정적 Symbol true, false, nil)는 절대 반환되지 않아요.

블록이 없으면 열거자(enumerator)를 돌려줘요.

Job = Class.new
jobs = [Job.new, Job.new]
count = ObjectSpace.each_object(Job) {|x| p x }
puts "Total count: #{count}"

이 결과는 다음과 같아요.

#<Job:0x000000011d6cbbf0>
#<Job:0x000000011d6cbc68>
 Total count: 2

현재 Ractor 구현 이슈 때문에, 이 메서드는 프로세스가 멀티-Ractor 모드일 때 Ractor-unshareable 객체를 yield 하지 않아요. 멀티-Ractor 모드는 Ractor.new가 처음 호출될 때 활성화돼요. 자세한 내용은 bugs.ruby-lang.org/issues/19387을 참고하세요.

a = 12345678987654321 # shareable
b = [].freeze # shareable
c = {} # not shareable
ObjectSpace.each_object {|x| x } # yields a, b, and c
Ractor.new {} # enter multi-Ractor mode
ObjectSpace.each_object {|x| x } # does not yield c

garbage_collect(full_mark: true, immediate_mark: true, immediate_sweep: true)

GC.start의 별칭이에요.

internal_class_of(obj) → Class or Module (MRI specific feature)

obj의 내부 클래스를 돌려줘요. objInternalObjectWrapper의 인스턴스일 수 있어요. 애플리케이션에서 이 메서드를 쓰면 안 된다는 점에 주의하세요.

internal_super_of(cls) → Class or Module (MRI specific feature)

cls(Class 또는 Module)의 내부 슈퍼클래스를 돌려줘요. objInternalObjectWrapper의 인스턴스일 수 있어요. 애플리케이션에서 쓰면 안 되는 메서드예요.

memsize_of(obj) → Integer

obj가 소비하는 메모리 크기를 바이트로 돌려줘요. 반환 크기는 불완전하다는 점에 유의하세요. HINT로만 다뤄야 해요. 특히 T_DATA의 크기는 정확하지 않을 수 있어요. 이 메서드는 CRuby에서만 동작할 것으로 기대돼요.

Ruby 3.2부터 Variable Width Allocation 덕분에, 실제 사용된 슬롯 크기에 더해 슬롯 밖에 추가로 할당된 메모리(외부 문자열·배열·해시 테이블 등)까지 합쳐서 돌려줘요.

memsize_of_all([klass]) → Integer

모든 살아 있는 객체가 소비하는 메모리 크기를 바이트로 돌려줘요. klass(Class 객체여야 함)가 주어지면 그 클래스의 인스턴스들의 총 메모리 크기를 돌려줘요. 반환 크기는 불완전한데, HINT로만 다뤄야 해요. 특히 T_DATA의 크기는 정확하지 않을 수 있어요. 이 메서드는 전체 malloc된 메모리 크기를 돌려주지 않는 것에 유의하세요.

이 메서드는 다음 Ruby 코드로 정의할 수 있어요.

def memsize_of_all klass = false
  total = 0
  ObjectSpace.each_object{|e|
    total += ObjectSpace.memsize_of(e) if klass == false || e.kind_of?(klass)
  }
  total
end

C Ruby에서만 동작할 것으로 기대돼요.

reachable_objects_from(obj) → array or nil (MRI specific feature)

obj에서 도달 가능한(reachable) 모든 객체를 돌려줘요. obj가 같은 객체 x에 참조가 둘 이상 있으면, 반환 배열에는 x 객체가 하나만 포함돼요. objtrue, false, nil, 심볼, Fixnum(그리고 Flonum) 같은 non-markable(비힙 관리) 객체이면 그냥 nil을 돌려줘요. obj가 내부 객체에 대한 참조를 갖고 있으면 ObjectSpace::InternalObjectWrapper 클래스의 인스턴스를 돌려줘요. 이 객체는 내부 객체에 대한 참조를 담고 있고, type 메서드로 내부 객체의 타입을 확인할 수 있어요. objObjectSpace::InternalObjectWrapper의 인스턴스이면, obj가 가리키는 내부 객체에서 도달 가능한 모든 객체를 돌려줘요. 이 메서드로 메모리 누수를 찾을 수 있어요. C Ruby에서만 동작할 것으로 기대돼요.

예시:

ObjectSpace.reachable_objects_from(['a', 'b', 'c'])
#=> [Array, 'a', 'b', 'c']

ObjectSpace.reachable_objects_from(['a', 'a', 'a'])
#=> [Array, 'a', 'a', 'a'] # all 'a' strings have different object id

ObjectSpace.reachable_objects_from([v = 'a', v, v])
#=> [Array, 'a']

ObjectSpace.reachable_objects_from(1)
#=> nil # 1 is not markable (heap managed) object

reachable_objects_from_root → hash (MRI specific feature)

root에서 도달 가능한 모든 객체를 돌려줘요.

trace_object_allocations { block }

ObjectSpace 확장 모듈에서 객체 할당 추적을 시작해요.

require 'objspace'

class C
  include ObjectSpace

  def foo
    trace_object_allocations do
      obj = Object.new
      p "#{allocation_sourcefile(obj)}:#{allocation_sourceline(obj)}"
    end
  end
end

C.new.foo #=> "objtrace.rb:8"

이 예제는 읽기 쉽게 ObjectSpace 모듈을 include 했지만, ::trace_object_allocations 표기법을 쓰는 것도 가능해요(권장). 이 기능은 성능을 크게 떨어뜨리고 메모리를 크게 소비한다는 점에 유의하세요.

trace_object_allocations_clear

기록된 추적 정보를 지워요.

trace_object_allocations_debug_start

GC 디버깅을 위해 객체 할당 추적을 시작해요. 애플리케이션에서 "… is T_NONE" 같은 BUG를 만나면, 앱 시작 부분에서 이 메서드를 시도해 보세요.

trace_object_allocations_start

객체 할당 추적을 시작해요.

trace_object_allocations_stop

객체 할당 추적을 멈춰요. ::trace_object_allocations_start가 n번 호출되면, ::trace_object_allocations_stop도 n번 호출한 후에 추적이 멈춘다는 점에 유의하세요.

undefine_finalizer(obj)

obj에 대한 모든 파이널라이저를 제거해요.

Public Instance Methods

dump(obj, output: :string)

ruby 객체의 내용을 JSON으로 덤프해요. output:stdout, :file, :string, 또는 IO 객체 중 하나일 수 있어요.

  • :file은 tempfile로 덤프하고 그에 해당하는 File 객체를 돌려주는 것
  • :stdout은 덤프를 출력하고 nil을 돌려주는 것
  • :string은 덤프를 담은 문자열을 돌려주는 것
  • IO 객체의 인스턴스가 주어지면 그곳으로 출력하고 객체를 돌려주는 것

이 메서드는 C Ruby에서만 동작할 것으로 기대돼요. 실험적 메서드라 변경될 수 있어요. 특히 함수 시그니처와 출력 형식은 미래의 Ruby 버전에서 호환된다는 보장이 없어요.

dump_all(output: :file, full: false, since: nil, shapes: true)

ruby 힙의 내용을 JSON으로 덤프해요. output 인자는 dump와 같아요. full은 불리언이어야 해요. true이면 빈 슬롯(T_NONE)까지 포함해 모든 힙 슬롯을 덤프해요. since는 음수가 아닌 정수 또는 nil이어야 해요. since가 양의 정수이면 그 세대와 그보다 새로운 세대의 객체만 덤프돼요. 현재 세대는 GC::count로 얻을 수 있어요. 객체 할당 추적이 활성화되지 않은 채 할당된 객체는 무시돼요. ::trace_object_allocations를 참고하세요. since를 생략하거나 nil이면 모든 객체를 덤프해요.

shapes는 불리언 또는 음수가 아닌 정수여야 해요. shapes가 양의 정수이면 주어진 shape id보다 새로운 shape만 덤프돼요. 현재 shape_idRubyVM.stat(:next_shape_id)로 얻을 수 있어요. shapesfalse이면 shape을 덤프하지 않아요.

특정 시점 이후에만 할당된 객체를 덤프하려면 sinceshapes를 조합할 수 있어요.

ObjectSpace.trace_object_allocations
GC.start
gc_generation = GC.count
shape_generation = RubyVM.stat(:next_shape_id)
call_method_to_instrument
ObjectSpace.dump_all(since: gc_generation, shapes: shape_generation)

C Ruby에서만 동작할 것으로 기대돼요. 실험적 메서드라 변경될 수 있고, 시그니처와 출력 형식이 미래 버전과 호환된다는 보장이 없어요.

dump_shapes(output: :file, since: 0)

ruby shape 트리의 내용을 JSON으로 덤프해요. output 인자는 dump와 같아요. since가 양의 정수이면 주어진 shape id보다 새로운 shape만 덤프돼요. 현재 shape_idRubyVM.stat(:next_shape_id)로 얻을 수 있어요. C Ruby에서만 동작할 것으로 기대돼요. 실험적 메서드라 변경될 수 있고, 시그니처와 출력 형식이 미래 버전과 호환된다는 보장이 없어요.

Private Instance Methods

garbage_collect(full_mark: true, immediate_mark: true, immediate_sweep: true)

GC.start의 별칭이에요.