Java 与 .Net 中的对象生命周期
我正在阅读“CLR via C#”,似乎在这个示例中,最初分配给“obj”的对象将在第 1 行执行后(而不是第 2 行后)有资格进行垃圾回收。
void Foo()
{
Object obj = new Object();
obj = null;
}
这是因为局部变量 lifespan 未定义按定义它的范围,但按您上次阅读它的时间。
所以我的问题是:Java 怎么样?我编写了这个程序来检查这种行为,看起来对象仍然活着。我认为 JVM 在解释字节码时不可能限制变量的生存期,因此我尝试使用“java -Xcomp”运行程序来强制方法编译,但无论如何都不会调用“finalize”。看来Java不是这样,但我希望我能在这里得到更准确的答案。另外,Android 的 Dalvik VM 怎么样?
class TestProgram {
public static void main(String[] args) {
TestProgram ref = new TestProgram();
System.gc();
}
@Override
protected void finalize() {
System.out.println("finalized");
}
}
额外: Jeffrey Richter 在“CLR via C#”中给出了代码示例,如下所示:
public static void Main (string[] args)
{
var timer = new Timer(TimerCallback, null, 0, 1000); // call every second
Console.ReadLine();
}
public static void TimerCallback(Object o)
{
Console.WriteLine("Callback!");
GC.Collect();
}
如果项目目标是“Release”,则 TimerCallback 在 MS .Net 上仅调用一次(GC.Collect() 调用后销毁计时器),如果目标是“Release”,则每秒调用一次“调试”(变量寿命增加,因为程序员可以尝试使用调试器访问对象)。但在 Mono 上,无论你如何编译它,每秒都会调用回调。看起来 Mono 的“定时器”实现存储了对线程池中某处实例的引用。 MS 实现不会这样做。
I was reading "CLR via C#" and it seems that in this example, the object that was initially assigned to 'obj' will be eligible for Garbage Collection after line 1 is executed, not after line 2.
void Foo()
{
Object obj = new Object();
obj = null;
}
That's because local variable lifespan defined not by scope in which it was defined, but by last time you read it.
So my question is: what about Java? I have written this program to check such behavior, and it looks like object stays alive. I don't think that it's possible for the JVM to limit variable lifetime while interpreting bytecode, so I tried to run program with 'java -Xcomp' to force method compilation, but 'finalize' is not called anyway. Looks like that's not true for Java, but I hope I can get a more accurate answer here. Also, what about Android's Dalvik VM?
class TestProgram {
public static void main(String[] args) {
TestProgram ref = new TestProgram();
System.gc();
}
@Override
protected void finalize() {
System.out.println("finalized");
}
}
Added:
Jeffrey Richter gives code example in "CLR via C#", something like this:
public static void Main (string[] args)
{
var timer = new Timer(TimerCallback, null, 0, 1000); // call every second
Console.ReadLine();
}
public static void TimerCallback(Object o)
{
Console.WriteLine("Callback!");
GC.Collect();
}
TimerCallback called only once on MS .Net if projects target is 'Release' (timer destroyed after GC.Collect() call), and called every second if target is 'Debug' (variables lifespan increased because programmer can try to access object with debugger). But on Mono callback called every second no matter how you compile it. Looks like Mono's 'Timer' implementation stores reference to instance somewhere in thread pool. MS implementation doesn't do this.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
请注意,仅仅因为一个对象可以被收集,并不意味着它实际上会在任何给定点被收集 - 因此您的方法可能会给出漏报。如果任何对象的 Finalize 方法被调用,你肯定可以说它是不可访问的,但如果该方法没有被调用,你就无法从逻辑上推断出任何东西。与大多数与 GC 相关的问题一样,垃圾收集器的不确定性使得很难对其具体功能进行测试/保证。
关于可达性/可收集性主题,JLS 表示 (12.6.1):
这或多或少正是您所期望的 - 我认为上面的段落与“一个对象无法访问,如果您绝对不会再使用它”是同构的。
回到原来的情况,您能想到在第 1 行之后被视为不可访问的对象与第 2 行之后被视为不可访问的对象之间有什么实际影响吗?我最初的反应是没有,如果你设法找到这种情况,那很可能是错误/扭曲的代码导致虚拟机陷入困境,而不是语言固有的弱点。
尽管我对反驳持开放态度。
编辑:感谢您提供有趣的例子。
我同意您的评估并看看您要去哪里,尽管问题可能更多是调试模式正在巧妙地改变代码的语义。
在编写的代码中,您将 Timer 分配给一个局部变量,该变量随后不会在其作用域内读取。即使是最简单的转义分析也可以揭示
timer
变量没有在main
方法的其他任何地方使用,因此可以被省略。因此,我认为您的第一行可以被认为完全等同于直接调用构造函数:在后一种情况下,很明显新创建的 Timer 对象在构造后无法立即访问(假设它不访问)不要做任何偷偷摸摸的事情,比如在构造函数中将自身添加到静态字段等);这样一旦 GC 处理完它就会被收集。
现在,在调试情况下,情况略有不同,因为您提到的原因是开发人员可能希望稍后在方法中检查局部变量的状态。因此编译器(和 JIT 编译器)无法优化它们;就好像在方法末尾有一个变量的访问,阻止收集直到该点。
即便如此,我认为这实际上并没有改变语义。 GC 的本质是回收很少得到保证(至少在 Java 中,唯一的保证是如果抛出 OutOfMemoryError,那么所有被认为无法访问的东西都会被立即 GC 掉)。事实上,假设您有足够的堆空间来保存运行时生命周期内创建的每个对象,则无操作 GC 实现是完全有效的。因此,虽然您可能会观察到
Timer
滴答次数的行为变化,但这很好,因为无法保证根据您调用它的方式您会看到什么。 (这在概念上类似于在系统负载下运行整个 CPU 密集型任务的计时器会计时更多次 - 两种结果都没有错误,因为接口不提供这种保证。)在这一点上,我指的是你回到这个答案的第一句话。 :)
Note that just because an object can be collected, doesn't mean that it will actually be collected an any given point - so your method can give false negatives. If any object's
finalize
method is called you can definitely say that it was unreachable, but if the method is not called you can't logically infer anything. As with most GC-related questions, the non-determinism of the garbage collector makes it hard to come up with tests/guarantees about exactly what it will do.On the topic of reachability/collectability, the JLS says (12.6.1):
Which is more or less exactly what you'd expect - I think the above paragraph is isomorphic with "an object is unreachable iff you definitely won't use it any more".
Going back to your original situation, can you think of any practical ramifications between the object being deemed unreachable after line 1 as opposed to line 2? My initial reaction is that there are none, and if you somehow managed to find such a situation it would likely be a mark of bad/twisted code causing the VM to struggle rather than an inherent weakness in the language.
Though I'm open to counter-arguments.
Edit: Thanks for the interesting example.
I agree with your assessment and see where you're going, though the issue is probably more that debug mode is subtly changing the semantics of your code.
In the code as written, you assign the
Timer
to a local variable which is not subsequently read within its scope. Even the most trivial escape analysis can reveal that thetimer
variables isn't used anywhere else in themain
method, and so can be elided. Hence I think your first line can be considered exactly equivalent to just invoking the constructor directly:In this latter case it's clear that the newly created
Timer
object isn't reachable immediately after construction (assuming that it doesn't do anything sneaky like add itself to static fields etc. in its constructor); and so that it would be collected as soon as the GC got round to it.Now in the debug case things are subtly different, for the reason you've mentioned which is that the developer may wish to inspect the state of the local variables later on in the method. Therefore the compiler (and the JIT compiler) can't optimise these away; it's as if there is an access of the variable at the end of the method, preventing collection until that point.
Even so, I don't think this actually changes the semantics. The nature of GC is that collection is seldom guaranteed (in Java at least, the only guarantee you get is that if an OutOfMemoryError was thrown then everything deemed unreachable was GCed immediately beforehand). In fact, assuming you had enough heap space to hold every object created during the lifetime of the runtime, a no-op GC implementation is perfectly valid. So while you might observe behavioural changes in how many times the
Timer
ticks, this is fine as there are no guarantees about what you'll see based on the way you're invoking it. (This is conceptually similar to how a timer that runs throughout a CPU-intensive task would tick more times when the system was under load - neither outcome is wrong because the interface doesn't provide that sort of guarantee.)At this point I refer you back to the first sentence in this answer. :)
一般来说,Java 的行为是,如果该对象在作用域内可达(有一个不是垃圾的引用),那么该对象就不是垃圾。这是递归的,因此如果
a
是对引用了b
的对象的引用,则b
引用的对象不是垃圾。在您仍然可以到达
ref
引用的对象的范围内,(您可以添加一行System.out.println(ref.toString())
),ref
不是垃圾。然而,根据Sun 网站的这个旧来源,大部分取决于JVM的具体实现。
Java, in general, has the behavior that if the object is reachable in the scope (there is a reference to it that is not garbage), then the object is not garbage. This is recursive, so if
a
is a reference to an object which has a reference tob
, the objectb
refers to is not garbage.Within the scope where you can still reach the object referenced by
ref
, (you could add a the lineSystem.out.println(ref.toString())
),ref
is not garbage.However, according to this old source from Sun's site, most depends on the specific implementation of the JVM.