How to count existing instances of a class in ruby?
Asked Answered
C

2

4

Here's an idea from this question: Upon object creation, increment a class variable. When object gets collected, decrement it. As you can observe, finalizer is called, and @@no_foo gets decremented. But when I query it a moment later, decrement is gone. Seems that value is going only up, never down (if I create two objects, it will show 2). Am I missing something obvious?

class Foo
  @@no_foo = 0

  def initialize
    puts 'creating object'
    @@no_foo += 1
    ObjectSpace.define_finalizer(self, proc { self.delete })
  end

  def delete
    puts 'deleting object'
    @@no_foo # => 1
    @@no_foo -= 1
    @@no_foo # => 0
  end

  def self.no_foo
    @@no_foo # => 0, 1
  end
end

Foo.no_foo # => 0
f = Foo.new
f = nil

GC.start
Foo.no_foo # => 1

# >> creating object
# >> deleting object
Custombuilt answered 28/5, 2013 at 14:40 Comment(9)
Hm. I do not see the output deleting object at all. Also, ObjectSpace.each_object(Foo).count gives me 1.Roesler
Are you doing it in irb? Probably it holds the references to all objects.Exceedingly
Hmm, that's strange. I do see it. What ruby do you use? I'm on 1.9.3Custombuilt
Might it have to do with thread-safety? Or would the GVL take care of this...Evelinaeveline
Yeah @DNNX, IRB was the cause.Roesler
@PinnyM: you think? As far as I know, ruby employs stop-the-world GC. No thread issues should rise from here.Custombuilt
When i change the last line to puts Foo.no_foo, i see deleting object after the puts, probably at exit. So i guess GC.start does not actually collect the instance and the count of 1 is correct at that time.Roesler
Sorry, I added an answer and just saw all the comments. Yes, it definitely looks like it is not cleaning up until irb exits.Tetracycline
The documentation is contradictory. Under the class description, it says define_finalizer is called right before deletion, and under the description of the method define_finalizer, it says it is called right after. How can this both be true? Does anyone have clear idea on this?Sahib
E
6

It can work, but there's circular reference in finalization. Your finalizer depends on the binding of an object that should be collected. See this solution.

class Foo
  @@no_foo = 0

  def initialize
    @@no_foo += 1
    ObjectSpace.define_finalizer(self, Foo.method(:delete))
  end

  def self.delete id # also this argument seems to be necessary
    @@no_foo -= 1
  end

  def self.no_foo
    @@no_foo
  end
end

Foo.no_foo # => 0
1000.times{Foo.new}
Foo.no_foo # => 1000

GC.start
Foo.no_foo # => 0
Extremely answered 28/5, 2013 at 17:25 Comment(0)
T
3

Finalization is not happening when you think it should in the code you provided.

For example, if you change that one line to:

ObjectSpace.define_finalizer(self, proc do; puts "self is type #{self.class.name} and equals #{self.inspect}"; self.delete; end)

Then notice how it does nothing (even if I sit there and wait a while) until I kill irb:

... (entered class definition from above with that define_finalizer)
1.9.3-p392 :021 > Foo.no_foo # => 0
 => 0 
1.9.3-p392 :022 > f = Foo.new
creating object
 => #<Foo:0x007fb5730f3e00> 
1.9.3-p392 :023 > f = nil
 => nil 
1.9.3-p392 :024 > 
1.9.3-p392 :025 > GC.start
 => nil 
1.9.3-p392 :026 > Foo.no_foo # => 1
 => 1 
1.9.3-p392 :027 > ^D
self is type Foo and equals #<Foo:0x007fb5730f3e00>
deleting object

So the first assumption may be that GC was not invoked. But, lets look at it using GC::Profiler:

1.9.3p392 :001 > GC::Profiler.enable
... (entered class definition from above)
1.9.3p392 :022 > puts GC::Profiler.result
GC 17 invokes.
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
 => nil 
1.9.3p392 :023 > Foo.no_foo # => 0
 => 0 
1.9.3p392 :024 > f = Foo.new
creating object
 => #<Foo:0x007fe2fc806808> 
1.9.3p392 :025 > puts GC::Profiler.result
GC 17 invokes.
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
 => nil 
1.9.3p392 :026 > f = nil
 => nil 
1.9.3p392 :027 > puts GC::Profiler.result
GC 17 invokes.
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
 => nil 
1.9.3p392 :028 > GC.start
 => nil 
1.9.3p392 :029 > puts GC::Profiler.result
GC 18 invokes.
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
    1               0.161               997280              2257680                56442         3.96199999999999352696
 => nil 
1.9.3p392 :030 > Foo.no_foo # => 1
 => 1 
1.9.3p392 :031 > ^D
deleting object

So, it looks like the GC is getting invoked when you ask it to, but it is not finalizing the Foo instance until irb exit.

Tetracycline answered 28/5, 2013 at 14:52 Comment(10)
@SergioTulentsev: Perhaps passing self to the finalizer is defeating the garbage collection (since it's still in scope)? Try delegating this activity to a separate instance.Evelinaeveline
@PinnyM: maybe, but doesn't seem to make a difference: pastie.org/private/ewwvhyby1t5rxnsjpq4waCustombuilt
@SergioTulentsev: this code doesn't call anything that decrements :)Evelinaeveline
@PinnyM: yes, but it prints "hook" after the line "it should have been deleted"Custombuilt
The point of GC.start is to give the execution engine a hint that this might be a good time to run GC, provided that the runtime thinks it makes sense to do so. If the runtime doesn't think it makes sense to run GC, then it won't. And why would it? You seem to have plenty of unused memory left, why do you want to waste precious CPU time to reclaim a tiny amount of memory that you probably don't even need?Sponger
@JörgWMittag: the method could be more appropriately named then. Right now it gives an illusion that you are in control. I like to be in control and I don't like when I command start and it doesn't start. :)Custombuilt
I updated my answer to show that GC::Profiler tracks an invocation of GC.Tetracycline
@SergioTulentsev Most garbage collectors don't have explicit runtime controls like start/end (none that I've ever encountered). You can only suggest to them. The fact is that they probably have a better idea of what's going on than you do, and if you're looking to erase variables, then you should be setting them to nil -- not asking the GC to do so.Settle
@joslinm: .NET GC was working in a synchronous manner, IIRC. You command, it stops the world and collects. It's been 5+ years, though, since my last .NET job, so my memory might be corrupted.Custombuilt
@SergioTulentsev Interesting.. I don't think it's that way anymore though. The last C# project I worked on was where I learned about how the GC doesn't really listen to you ;]Settle

© 2022 - 2024 — McMap. All rights reserved.