CrackedRuby logo

CrackedRuby

Finalizers

A comprehensive guide to Ruby's finalizer mechanism for executing cleanup code during garbage collection.

Core Modules ObjectSpace Module
3.5.3

Overview

Ruby's finalizer system executes cleanup code when objects become eligible for garbage collection. The ObjectSpace module provides the interface for registering and managing finalizers through the define_finalizer and undefine_finalizer methods. Finalizers run in a separate thread during garbage collection cycles, making them suitable for resource cleanup operations like closing file handles, network connections, or releasing external resources.

The finalizer mechanism operates independently of object references. Ruby stores finalizers in a separate data structure, preventing them from keeping target objects alive. When the garbage collector identifies an unreachable object with registered finalizers, Ruby queues the finalizer for execution before reclaiming the object's memory.

class ResourceHandler
  def initialize(filename)
    @file = File.open(filename)
    
    # Register finalizer for cleanup
    ObjectSpace.define_finalizer(self, self.class.finalizer(@file))
  end
  
  def self.finalizer(file)
    proc { file.close if file && !file.closed? }
  end
end

handler = ResourceHandler.new("data.txt")
handler = nil  # Remove reference
GC.start      # Force garbage collection

Finalizers receive the object ID of the target object, not the object itself. This design prevents finalizers from inadvertently keeping references to the target object, which would prevent garbage collection. The finalizer runs after the object becomes unreachable but before memory deallocation occurs.

Basic Usage

Register finalizers using ObjectSpace.define_finalizer, which accepts a target object and a callable finalizer. The finalizer typically contains cleanup logic that does not reference the target object directly. Ruby calls the finalizer with the object ID when garbage collection occurs.

# File resource management
file = File.open("example.txt")
ObjectSpace.define_finalizer(file, proc { |id| puts "File #{id} finalized" })

# Database connection cleanup
class DatabaseConnection
  def initialize(connection_string)
    @connection = establish_connection(connection_string)
    ObjectSpace.define_finalizer(self, cleanup_proc(@connection))
  end
  
  private
  
  def cleanup_proc(connection)
    proc do |object_id|
      connection.close if connection.respond_to?(:close)
      puts "Connection #{object_id} cleaned up"
    end
  end
  
  def establish_connection(string)
    # Connection logic here
    OpenStruct.new(close: -> { puts "Connection closed" })
  end
end

conn = DatabaseConnection.new("postgresql://localhost")
conn = nil
GC.start

Remove finalizers using ObjectSpace.undefine_finalizer when cleanup becomes unnecessary. This method accepts the target object and removes all associated finalizers. Manual cleanup should include finalizer removal to prevent unnecessary finalizer execution.

class ManagedResource
  def initialize(resource)
    @resource = resource
    @finalizer = proc { |id| cleanup_resource(resource) }
    ObjectSpace.define_finalizer(self, @finalizer)
  end
  
  def close
    cleanup_resource(@resource)
    ObjectSpace.undefine_finalizer(self)
  end
  
  private
  
  def cleanup_resource(resource)
    resource.cleanup if resource.respond_to?(:cleanup)
  end
end

resource = ManagedResource.new(some_resource)
resource.close  # Manual cleanup removes finalizer

Advanced Usage

Complex finalizer patterns involve multiple objects, cascading cleanup operations, and conditional finalizer behavior. Advanced implementations often require careful management of object relationships and finalizer ordering to prevent resource leaks or premature cleanup.

class ResourcePool
  def initialize
    @resources = []
    @cleanup_procs = []
    register_pool_finalizer
  end
  
  def acquire_resource(type)
    resource = create_resource(type)
    @resources << resource
    
    # Register individual resource finalizer
    cleanup_proc = create_cleanup_proc(resource, type)
    @cleanup_procs << cleanup_proc
    ObjectSpace.define_finalizer(resource, cleanup_proc)
    
    resource
  end
  
  def release_resource(resource)
    index = @resources.index(resource)
    return unless index
    
    # Remove from tracking and undefine finalizer
    @resources.delete_at(index)
    cleanup_proc = @cleanup_procs.delete_at(index)
    ObjectSpace.undefine_finalizer(resource)
    
    # Perform immediate cleanup
    cleanup_proc.call(resource.object_id)
  end
  
  private
  
  def register_pool_finalizer
    resources_ref = @resources
    cleanup_procs_ref = @cleanup_procs
    
    pool_finalizer = proc do |pool_id|
      resources_ref.zip(cleanup_procs_ref).each do |resource, cleanup_proc|
        cleanup_proc.call(resource.object_id) if resource
      end
    end
    
    ObjectSpace.define_finalizer(self, pool_finalizer)
  end
  
  def create_resource(type)
    case type
    when :file
      File.open("/tmp/resource_#{SecureRandom.hex(8)}", "w+")
    when :connection
      OpenStruct.new(close: -> { puts "Connection closed" })
    end
  end
  
  def create_cleanup_proc(resource, type)
    proc do |resource_id|
      case type
      when :file
        resource.close unless resource.closed?
      when :connection
        resource.close if resource.respond_to?(:close)
      end
      puts "Cleaned up #{type} resource #{resource_id}"
    end
  end
end

pool = ResourcePool.new
file_resource = pool.acquire_resource(:file)
conn_resource = pool.acquire_resource(:connection)

pool.release_resource(file_resource)  # Manual release
pool = nil  # Pool finalizer handles remaining resources

Finalizer inheritance patterns allow base classes to define cleanup behavior while subclasses extend or modify finalizer logic. This approach provides consistent cleanup interfaces across class hierarchies.

class BaseResource
  def initialize
    register_base_finalizer
    after_initialize
  end
  
  protected
  
  def register_base_finalizer
    ObjectSpace.define_finalizer(self, base_cleanup_proc)
  end
  
  def base_cleanup_proc
    proc do |object_id|
      perform_base_cleanup(object_id)
      perform_subclass_cleanup(object_id)
    end
  end
  
  def perform_base_cleanup(object_id)
    puts "Base cleanup for object #{object_id}"
  end
  
  def perform_subclass_cleanup(object_id)
    # Override in subclasses
  end
  
  def after_initialize
    # Override in subclasses
  end
end

class FileResource < BaseResource
  def after_initialize
    @file_handle = File.open("/tmp/file_#{object_id}", "w+")
  end
  
  protected
  
  def perform_subclass_cleanup(object_id)
    @file_handle.close if @file_handle && !@file_handle.closed?
    puts "File cleanup for object #{object_id}"
  end
end

Error Handling & Debugging

Finalizer debugging presents unique challenges because finalizers run asynchronously in separate threads during garbage collection. Standard debugging techniques often fail because finalizer execution timing depends on memory pressure and garbage collection scheduling rather than program flow.

class DebuggableResource
  @@finalizer_count = 0
  @@finalizer_errors = []
  
  def initialize(resource_name)
    @resource_name = resource_name
    @created_at = Time.now
    
    finalizer = create_debug_finalizer(@resource_name, @created_at)
    ObjectSpace.define_finalizer(self, finalizer)
  end
  
  private
  
  def create_debug_finalizer(name, created_time)
    proc do |object_id|
      begin
        @@finalizer_count += 1
        
        # Log finalizer execution
        puts "[FINALIZER #{@@finalizer_count}] Cleaning up #{name} (ID: #{object_id})"
        puts "[FINALIZER #{@@finalizer_count}] Object lived for #{Time.now - created_time} seconds"
        
        # Simulated cleanup that might fail
        cleanup_resource(name, object_id)
        
        puts "[FINALIZER #{@@finalizer_count}] Successfully cleaned up #{name}"
        
      rescue => error
        # Capture finalizer errors for later analysis
        @@finalizer_errors << {
          object_id: object_id,
          resource_name: name,
          error: error,
          timestamp: Time.now
        }
        
        # Write to log file since puts might not work in finalizer thread
        File.open("/tmp/finalizer_errors.log", "a") do |f|
          f.puts "ERROR in finalizer for #{name} (#{object_id}): #{error.message}"
          f.puts error.backtrace.join("\n")
          f.puts "-" * 50
        end
      end
    end
  end
  
  def cleanup_resource(name, object_id)
    # Simulate potential cleanup failures
    case name
    when /file/
      # File might already be closed by another process
      raise "File already closed" if rand < 0.3
    when /connection/
      # Network cleanup might timeout
      raise "Connection timeout during cleanup" if rand < 0.2
    end
  end
  
  def self.finalizer_statistics
    {
      total_finalizers_run: @@finalizer_count,
      errors_encountered: @@finalizer_errors.size,
      recent_errors: @@finalizer_errors.last(5)
    }
  end
end

# Test error handling
resources = []
10.times do |i|
  resources << DebuggableResource.new("resource_#{i}")
end

resources = nil
GC.start

# Check results after GC
puts DebuggableResource.finalizer_statistics

Finalizer testing requires special techniques because finalizers only run during garbage collection, which occurs unpredictably. Testing finalizer behavior often involves forcing garbage collection and verifying cleanup effects rather than direct finalizer observation.

RSpec.describe "Finalizer behavior" do
  it "cleans up resources when object is garbage collected" do
    cleanup_called = false
    cleanup_proc = proc { |id| cleanup_called = true }
    
    # Create object in separate scope
    obj = nil
    expect {
      obj = Object.new
      ObjectSpace.define_finalizer(obj, cleanup_proc)
      obj = nil  # Remove reference
    }.not_to raise_error
    
    # Force garbage collection multiple times
    3.times do
      GC.start
      sleep 0.01  # Allow finalizer thread to run
    end
    
    expect(cleanup_called).to be true
  end
  
  it "handles finalizer exceptions gracefully" do
    error_logged = false
    
    failing_finalizer = proc do |id|
      File.open("/tmp/finalizer_test.log", "a") { |f| f.puts "Finalizer error logged" }
      error_logged = true
      raise "Finalizer intentionally failed"
    end
    
    obj = Object.new
    ObjectSpace.define_finalizer(obj, failing_finalizer)
    obj = nil
    
    expect { 
      3.times { GC.start; sleep 0.01 }
    }.not_to raise_error
    
    sleep 0.1  # Allow file writing to complete
    expect(error_logged).to be true
  end
end

Performance & Memory

Finalizers introduce performance overhead through additional garbage collection bookkeeping and finalizer thread execution. Each registered finalizer adds memory overhead for storing finalizer references and increases garbage collection complexity as Ruby must track objects with associated finalizers separately from regular objects.

require 'benchmark'

class BenchmarkResource
  def initialize(use_finalizer = true)
    @data = "x" * 1000  # 1KB of data per object
    
    if use_finalizer
      ObjectSpace.define_finalizer(self, self.class.cleanup_proc)
    end
  end
  
  def self.cleanup_proc
    proc { |id| }  # Empty finalizer for overhead measurement
  end
end

def measure_allocation_performance(object_count, use_finalizers)
  objects = nil
  
  time = Benchmark.realtime do
    objects = Array.new(object_count) { BenchmarkResource.new(use_finalizers) }
  end
  
  memory_before_gc = ObjectSpace.count_objects[:T_OBJECT]
  
  gc_time = Benchmark.realtime do
    objects = nil  # Remove references
    3.times { GC.start }
  end
  
  memory_after_gc = ObjectSpace.count_objects[:T_OBJECT]
  
  {
    allocation_time: time,
    gc_time: gc_time,
    objects_before_gc: memory_before_gc,
    objects_after_gc: memory_after_gc,
    use_finalizers: use_finalizers
  }
end

# Compare performance with and without finalizers
results_without = measure_allocation_performance(10000, false)
results_with = measure_allocation_performance(10000, true)

puts "Without finalizers:"
puts "  Allocation time: #{results_without[:allocation_time]:.4f}s"
puts "  GC time: #{results_without[:gc_time]:.4f}s"

puts "With finalizers:"
puts "  Allocation time: #{results_with[:allocation_time]:.4f}s"  
puts "  GC time: #{results_with[:gc_time]:.4f}s"

overhead = ((results_with[:gc_time] / results_without[:gc_time]) - 1) * 100
puts "GC overhead: #{overhead.round(2)}%"

Memory usage patterns with finalizers differ significantly from regular object cleanup. Finalizers themselves consume memory and remain allocated until execution completion. Objects with finalizers may persist longer in memory because Ruby must coordinate garbage collection with finalizer execution.

class MemoryTracker
  @@tracked_objects = {}
  
  def initialize(name, size_kb = 1)
    @name = name
    @data = "x" * (size_kb * 1024)
    @created_at = Time.now
    
    # Track object creation
    @@tracked_objects[object_id] = {
      name: name,
      size_kb: size_kb,
      created_at: @created_at
    }
    
    finalizer = create_tracking_finalizer(object_id, name, size_kb)
    ObjectSpace.define_finalizer(self, finalizer)
  end
  
  private
  
  def create_tracking_finalizer(obj_id, name, size_kb)
    proc do |id|
      if @@tracked_objects[id]
        object_info = @@tracked_objects.delete(id)
        lifespan = Time.now - object_info[:created_at]
        
        puts "Finalized #{name}: lived #{lifespan.round(3)}s, freed #{size_kb}KB"
      end
    end
  end
  
  def self.memory_report
    total_objects = @@tracked_objects.size
    total_memory = @@tracked_objects.values.sum { |info| info[:size_kb] }
    
    puts "Active tracked objects: #{total_objects}"
    puts "Estimated tracked memory: #{total_memory}KB"
    puts "Ruby object count: #{ObjectSpace.count_objects[:T_OBJECT]}"
    puts "Process memory usage: #{`ps -o rss= -p #{Process.pid}`.to_i}KB"
  end
end

# Create objects with varying lifespans
short_lived = Array.new(100) { MemoryTracker.new("short_lived", 5) }
medium_lived = Array.new(50) { MemoryTracker.new("medium_lived", 10) }
long_lived = Array.new(10) { MemoryTracker.new("long_lived", 50) }

puts "After creation:"
MemoryTracker.memory_report

# Release short-lived objects
short_lived = nil
GC.start
sleep 0.1

puts "\nAfter releasing short-lived objects:"
MemoryTracker.memory_report

# Release medium-lived objects  
medium_lived = nil
GC.start
sleep 0.1

puts "\nAfter releasing medium-lived objects:"
MemoryTracker.memory_report

Thread Safety & Concurrency

Finalizers execute in Ruby's dedicated finalizer thread, separate from the main program thread. This execution model creates concurrency concerns when finalizers interact with shared resources, global variables, or thread-unsafe operations. Finalizer code must handle concurrent access patterns and potential race conditions.

class ThreadSafeResourceManager
  @@resource_registry = {}
  @@registry_mutex = Mutex.new
  @@cleanup_count = 0
  @@cleanup_mutex = Mutex.new
  
  def initialize(resource_id)
    @resource_id = resource_id
    @thread_id = Thread.current.object_id
    
    # Register resource in thread-safe manner
    @@registry_mutex.synchronize do
      @@resource_registry[@resource_id] = {
        created_in_thread: @thread_id,
        created_at: Time.now,
        status: :active
      }
    end
    
    finalizer = create_thread_safe_finalizer(@resource_id, @thread_id)
    ObjectSpace.define_finalizer(self, finalizer)
  end
  
  private
  
  def create_thread_safe_finalizer(resource_id, creator_thread_id)
    proc do |object_id|
      begin
        # Synchronize access to shared data structures
        @@cleanup_mutex.synchronize do
          @@cleanup_count += 1
          current_count = @@cleanup_count
          
          puts "[Thread #{Thread.current.object_id}] Finalizer #{current_count} starting"
          puts "[Thread #{Thread.current.object_id}] Cleaning up resource #{resource_id}"
          puts "[Thread #{Thread.current.object_id}] Created in thread #{creator_thread_id}"
        end
        
        # Update registry with cleanup status
        @@registry_mutex.synchronize do
          if @@resource_registry[resource_id]
            @@resource_registry[resource_id][:status] = :finalized
            @@resource_registry[resource_id][:finalized_at] = Time.now
            @@resource_registry[resource_id][:finalizer_thread] = Thread.current.object_id
          end
        end
        
        # Simulate cleanup work that might take time
        sleep(rand * 0.01)  # 0-10ms of work
        
        @@cleanup_mutex.synchronize do
          puts "[Thread #{Thread.current.object_id}] Finalizer for resource #{resource_id} completed"
        end
        
      rescue => error
        @@cleanup_mutex.synchronize do
          puts "[Thread #{Thread.current.object_id}] ERROR in finalizer: #{error.message}"
        end
      end
    end
  end
  
  def self.registry_status
    @@registry_mutex.synchronize do
      {
        total_resources: @@resource_registry.size,
        active: @@resource_registry.count { |_, info| info[:status] == :active },
        finalized: @@resource_registry.count { |_, info| info[:status] == :finalized },
        cleanup_operations: @@cleanup_count
      }
    end
  end
  
  def self.detailed_registry
    @@registry_mutex.synchronize do
      @@resource_registry.dup
    end
  end
end

# Test concurrent finalizer execution
threads = []
all_resources = []

# Create resources from multiple threads
5.times do |thread_num|
  threads << Thread.new do
    thread_resources = []
    20.times do |i|
      resource_id = "thread_#{thread_num}_resource_#{i}"
      thread_resources << ThreadSafeResourceManager.new(resource_id)
    end
    
    # Keep resources alive briefly in this thread
    sleep(0.1)
    thread_resources = nil  # Release references
  end
end

# Wait for all threads to complete
threads.each(&:join)

puts "Initial registry status:"
pp ThreadSafeResourceManager.registry_status

# Force garbage collection and allow finalizers to run
3.times do
  GC.start
  sleep(0.1)  # Allow finalizer thread time to work
end

puts "\nFinal registry status:"
pp ThreadSafeResourceManager.registry_status

puts "\nDetailed registry (first 5 entries):"
detailed = ThreadSafeResourceManager.detailed_registry
detailed.first(5).each do |resource_id, info|
  puts "#{resource_id}: #{info}"
end

Deadlock prevention in finalizers requires careful design because finalizers cannot acquire locks that might be held by the main program. Finalizer cleanup operations should avoid synchronization with main program execution or use timeout-based locking mechanisms.

class DeadlockSafeResource
  @@shared_resource = nil
  @@shared_mutex = Mutex.new
  @@finalizer_mutex = Mutex.new
  
  def initialize(name)
    @name = name
    
    # Safely initialize shared resource
    @@shared_mutex.synchronize do
      @@shared_resource ||= {}
      @@shared_resource[@name] = { active: true, data: "Resource data for #{@name}" }
    end
    
    ObjectSpace.define_finalizer(self, deadlock_safe_finalizer(@name))
  end
  
  def use_resource
    @@shared_mutex.synchronize do
      return nil unless @@shared_resource[@name]
      @@shared_resource[@name][:data]
    end
  end
  
  private
  
  def deadlock_safe_finalizer(name)
    proc do |object_id|
      # Use try_lock with timeout to avoid deadlocks
      cleanup_completed = false
      
      begin
        # Try to acquire lock with timeout
        if @@shared_mutex.try_lock
          begin
            if @@shared_resource && @@shared_resource[name]
              @@shared_resource.delete(name)
              cleanup_completed = true
            end
          ensure
            @@shared_mutex.unlock
          end
        end
        
        # Log finalizer status thread-safely
        @@finalizer_mutex.synchronize do
          if cleanup_completed
            puts "Successfully cleaned up #{name} in finalizer"
          else
            puts "Could not acquire lock for #{name} cleanup - resource may leak"
          end
        end
        
      rescue => error
        @@finalizer_mutex.synchronize do
          puts "Error in finalizer for #{name}: #{error.message}"
        end
      end
    end
  end
  
  def self.shared_resource_status
    @@shared_mutex.synchronize do
      @@shared_resource ? @@shared_resource.dup : {}
    end
  end
end

Common Pitfalls

Finalizer closures that capture references to the target object prevent garbage collection entirely. This circular reference issue occurs when finalizers access instance variables or methods of the object being finalized, keeping the object alive indefinitely.

# INCORRECT - Finalizer keeps object alive
class ProblematicResource
  def initialize(name)
    @name = name
    @file = File.open("/tmp/#{name}")
    
    # BAD: Finalizer captures 'self' through instance variable access
    ObjectSpace.define_finalizer(self, proc { |id| @file.close })
  end
end

# INCORRECT - Finalizer references instance method
class AnotherProblematicResource  
  def initialize(name)
    @name = name
    
    # BAD: Finalizer captures 'self' through method call
    ObjectSpace.define_finalizer(self, proc { |id| cleanup })
  end
  
  def cleanup
    puts "Cleaning up #{@name}"
  end
end

# CORRECT - Finalizer uses external reference
class CorrectResource
  def initialize(name)
    @name = name
    @file = File.open("/tmp/#{name}")
    
    # GOOD: Finalizer captures file reference, not self
    file_ref = @file
    ObjectSpace.define_finalizer(self, proc { |id| file_ref.close if file_ref })
  end
end

# CORRECT - Class method approach
class BetterResource
  def initialize(name)
    @name = name
    @file = File.open("/tmp/#{name}")
    
    # GOOD: Class method doesn't capture instance
    ObjectSpace.define_finalizer(self, self.class.finalizer(@file))
  end
  
  def self.finalizer(file)
    proc { |id| file.close if file && !file.closed? }
  end
end

Finalizer timing assumptions create unreliable cleanup behavior. Finalizers run when Ruby performs garbage collection, not when objects go out of scope. Programs that depend on immediate finalizer execution or specific finalizer ordering encounter unpredictable behavior.

# Demonstrates unpredictable finalizer timing
class TimingDemo
  @@execution_order = []
  @@execution_mutex = Mutex.new
  
  def initialize(name)
    @name = name
    
    finalizer = proc do |id|
      @@execution_mutex.synchronize do
        @@execution_order << "#{@name} finalized at #{Time.now.strftime('%H:%M:%S.%3N')}"
      end
    end
    
    ObjectSpace.define_finalizer(self, finalizer)
  end
  
  def self.show_execution_order
    @@execution_mutex.synchronize do
      @@execution_order.each { |msg| puts msg }
      @@execution_order.clear
    end
  end
end

# Create objects in specific order
puts "Creating objects in order: A, B, C"
obj_a = TimingDemo.new("Object A")
obj_b = TimingDemo.new("Object B") 
obj_c = TimingDemo.new("Object C")

# Remove references in different order
puts "Removing references in order: C, A, B"
obj_c = nil
obj_a = nil
obj_b = nil

# Force garbage collection
puts "Forcing garbage collection..."
GC.start
sleep 0.01

puts "Finalizer execution order:"
TimingDemo.show_execution_order

# Results show finalizers may not run in expected order

Exception handling within finalizers requires special consideration because finalizer exceptions do not propagate to the main program. Failed finalizers terminate silently, potentially causing resource leaks or incomplete cleanup operations.

class ExceptionHandlingDemo
  @@successful_cleanups = 0
  @@failed_cleanups = 0
  @@cleanup_mutex = Mutex.new
  
  def initialize(name, should_fail = false)
    @name = name
    @should_fail = should_fail
    
    finalizer = proc do |id|
      begin
        if @should_fail
          raise "Simulated cleanup failure for #{@name}"
        end
        
        # Simulate successful cleanup
        perform_cleanup(@name)
        
        @@cleanup_mutex.synchronize do
          @@successful_cleanups += 1
        end
        
      rescue => error
        @@cleanup_mutex.synchronize do
          @@failed_cleanups += 1
        end
        
        # Log error since exceptions don't propagate
        begin
          File.open("/tmp/finalizer_failures.log", "a") do |f|
            f.puts "#{Time.now}: Finalizer failed for #{@name}: #{error.message}"
          end
        rescue
          # Even logging might fail - finalizer environment is restricted
        end
      end
    end
    
    ObjectSpace.define_finalizer(self, finalizer)
  end
  
  private
  
  def perform_cleanup(name)
    # Simulated cleanup operation
    sleep(0.001)
  end
  
  def self.cleanup_statistics
    @@cleanup_mutex.synchronize do
      {
        successful: @@successful_cleanups,
        failed: @@failed_cleanups,
        total: @@successful_cleanups + @@failed_cleanups
      }
    end
  end
end

# Create mix of objects that will succeed and fail
objects = []
10.times { |i| objects << ExceptionHandlingDemo.new("good_#{i}", false) }
5.times { |i| objects << ExceptionHandlingDemo.new("bad_#{i}", true) }

objects = nil
GC.start
sleep 0.1

puts "Cleanup results:"
stats = ExceptionHandlingDemo.cleanup_statistics
puts "Successful: #{stats[:successful]}"
puts "Failed: #{stats[:failed]}"
puts "Total: #{stats[:total]}"

# Check log file for failure details
if File.exist?("/tmp/finalizer_failures.log")
  puts "\nFailure log contents:"
  puts File.read("/tmp/finalizer_failures.log")
end

Reference

Core Methods

Method Parameters Returns Description
ObjectSpace.define_finalizer(obj, proc) obj (Object), proc (Proc/Method) Array Registers finalizer proc for object
ObjectSpace.undefine_finalizer(obj) obj (Object) Object Removes all finalizers for object
ObjectSpace.count_objects(hash = {}) hash (Hash, optional) Hash Returns object counts by type
GC.start None nil Forces garbage collection cycle
GC.disable None Boolean Disables automatic garbage collection
GC.enable None Boolean Enables automatic garbage collection

Finalizer Proc Signature

finalizer = proc do |object_id|
  # object_id is Integer - the object ID of finalized object
  # No access to the actual object - it's being garbage collected
  # Perform cleanup operations here
end

ObjectSpace Constants

Constant Value Description
ObjectSpace::INVALID_OBJECT_ID Platform-dependent Invalid object ID value

GC Tuning Parameters

Method Parameters Description
GC.stat hash (Hash, optional) Returns garbage collection statistics
GC.count None Returns total GC runs since process start
GC.stress None Returns current GC stress testing status
GC.stress=(flag) flag (Boolean) Enables/disables GC stress testing

Common Finalizer Patterns

# File cleanup pattern
file_finalizer = proc do |id|
  file.close if file && !file.closed?
end

# Connection cleanup pattern  
connection_finalizer = proc do |id|
  connection.disconnect if connection.respond_to?(:disconnect)
  connection.close if connection.respond_to?(:close)
end

# Resource pool cleanup pattern
pool_finalizer = proc do |id|
  resources.each { |resource| resource.cleanup if resource.respond_to?(:cleanup) }
  resources.clear
end

# Debug logging pattern
debug_finalizer = proc do |id|
  logger.info "Object #{id} finalized at #{Time.now}" if logger
end

Error Handling Patterns

# Safe finalizer with error logging
safe_finalizer = proc do |id|
  begin
    perform_cleanup_operation
  rescue => error
    log_error_safely(error, id)
  end
end

def log_error_safely(error, object_id)
  File.open("/tmp/finalizer_errors.log", "a") do |f|
    f.puts "#{Time.now}: Finalizer error for object #{object_id}: #{error.message}"
    f.puts error.backtrace.join("\n") if error.backtrace
  end
rescue
  # Even error logging might fail in finalizer context
end

Thread Safety Considerations

Pattern Description Example
Mutex synchronization Protect shared state access mutex.synchronize { shared_data.update }
Try-lock with timeout Avoid deadlocks in finalizers if mutex.try_lock; cleanup; mutex.unlock; end
Thread-local cleanup Avoid cross-thread dependencies Store cleanup data in thread-local variables
Atomic operations Use atomic operations when possible AtomicReference or similar constructs

Memory Impact Guidelines

Scenario Memory Overhead Performance Impact
Objects without finalizers Minimal Standard GC performance
Objects with simple finalizers Low (finalizer proc storage) 5-15% GC overhead
Objects with complex finalizers Moderate (closure captures) 15-30% GC overhead
Many objects with finalizers High (finalizer queue growth) 30%+ GC overhead