为什么这个未使用的字符串没有被垃圾回收?

4
为什么 unused_variable_2 和 unused_variable_3 会被垃圾回收,但 unused_variable_1 不会?
# leaky_boat.rb
require "memprof"

class Boat
  def initialize(string)
    unused_variable1 = string[0...100]
    puts unused_variable1.object_id
    @string = string
    puts @string.object_id
  end
end

class Rocket
  def initialize(string)
    unused_variable_2 = string.dup
    puts unused_variable_2.object_id
    unused_variable_3 = String.new(string)
    puts unused_variable_3.object_id
    @string = string
    puts @string.object_id
  end
end

Memprof.start

text = "a" * 100
object_id_message = "Object ids of unused_variable_1, @string, unused_variable_2, unused_variable_3, and another @string"
before_gc_message = "Before GC"
after_gc_message = "After GC"
puts object_id_message
boat = Boat.new(text)
rocket = Rocket.new(text)
puts before_gc_message
Memprof.stats
ObjectSpace.garbage_collect
puts after_gc_message
Memprof.stats
Memprof.stop

运行程序:
$ uname -a
Linux [redacted] 3.2.0-25-generic #40-Ubuntu SMP Wed May 23 20:30:51 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux
$ ruby --version # Have to use Ruby 1.8 - memprof doesn't work on 1.9
ruby 1.8.7 (2011-06-30 patchlevel 352) [x86_64-linux]
$ ruby -rubygems leaky_boat.rb 
Object ids of unused_variable_1, @string, unused_variable_2, unused_variable_3, and another @string
70178323299180
70178323299320
70178323299100
70178323299060
70178323299320
Before GC
      2 leaky_boat.rb:6:String
      2 leaky_boat.rb:26:String
      1 leaky_boat.rb:9:String
      1 leaky_boat.rb:7:String
      1 leaky_boat.rb:32:Rocket
      1 leaky_boat.rb:31:Boat
      1 leaky_boat.rb:29:String
      1 leaky_boat.rb:28:String
      1 leaky_boat.rb:27:String
      1 leaky_boat.rb:20:String
      1 leaky_boat.rb:18:String
      1 leaky_boat.rb:17:String
      1 leaky_boat.rb:16:String
      1 leaky_boat.rb:15:String
After GC
      1 leaky_boat.rb:6:String
      1 leaky_boat.rb:32:Rocket
      1 leaky_boat.rb:31:Boat
      1 leaky_boat.rb:29:String
      1 leaky_boat.rb:28:String
      1 leaky_boat.rb:27:String
      1 leaky_boat.rb:26:String

安德鲁:上周我的回答是否没有令你满意地解决了你的问题? - dbenhur
@dbenhur,它没有解释为什么unused_variable_2unused_variable_3会被垃圾回收 - 它们难道没有特殊情况来保存内存分配吗? - Andrew Grimm
它们没有特殊的共享分配。String#dup和String.new都保证您获得一个独立的新对象。我会在我的答案中添加对代码路径的引用。 - dbenhur
1个回答

7
这个行为是因为你的Ruby版本的字符串实现在substr上有一个特殊情况,当你取一个子串是源字符串的尾部且字符串长度足够大,无法将字符串值存储在基本对象结构中时,它会节省内存分配。
如果你跟踪代码,你会发现范围下标string[0...100]将通过rb_str_substr中的这个子句。因此,新字符串将通过str_new3分配,该函数分配一个新的对象结构(因此具有不同的object_id),但将字符串值ptr字段设置为指向源对象的扩展存储,并设置ELTS_SHARED标志以表示新对象与另一个对象共享存储。
在您的代码中,您将此新子字符串对象分配给实例变量@string,当您运行垃圾回收时,它仍然是一个活动引用。由于原始字符串已经分配存储器的原因,它不能被回收。
在ruby trunk中,与兼容的尾子串共享存储的优化似乎仍然存在。
另外两个变量unused_variable_2和unused_variable_3不存在这个扩展存储共享问题,因为它们是通过可以确保不同存储的机制设置的,所以当它们的引用超出范围时,会按预期进行垃圾回收。
String#dup运行rb_str_replace(通过initialize_copy binding)来替换源字符串的内容,并确保不共享存储空间。
String#new(source_str)通过rb_str_init遍历初始值并类似地确保不共享存储。

@NiklasB。不是我理解的那样。beg + len == RSTRING(str)->len 表示子字符串切片的起始位置加上长度等于源字符串的长度,也就是说它们在字符串的末尾对齐。这样的情况发生是因为代码必须在字符串的末尾有一个空字符,所以只有具有相同字符串末尾的子字符串切片才能使用这个共享存储空间。 - dbenhur
哦,抱歉,我的错误。我被 string[0...100] 中的 0 部分误导了。在这种情况下,它既是前缀又是后缀,但共享确实是基于后缀的。 - Niklas B.
"String#dup 运行 rb_str_replace(通过 initialize_copy 绑定)来替换源字符串的内容,确保存储空间不共享,并使用源字符串的副本。换句话说,它有意避免了子字符串方法所使用的优化吗?" - Andrew Grimm
此外,一篇名为“Seeing double”的博客文章声称String#dup和String.new(source_str)使用了该优化,尽管我不确定该文章的作者有多可靠。 - Andrew Grimm
@AndrewGrimm 嗯,我依赖于阅读实际的代码,而不是博客文章。rb_str_replace明显会生成独立的字符串,除非源字符串是共享的。如果接收器字符串是共享的,则调用str_make_independent来打破共享,如果它已经是独立的,则调整大小并进行内存复制。 - dbenhur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接