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 |