这是一个非常基本的问题。我将使用C ++和Java来表达它,但它确实与语言无关。 考虑一下C ++中一个众所周知的问题:
struct Obj
{
boost::shared_ptr<Obj> m_field;
};
{
boost::shared_ptr<Obj> obj1(new Obj);
boost::shared_ptr<Obj> obj2(new Obj);
obj1->m_field = obj2;
obj2->m_field = obj1;
}
这是内存泄漏,每个人都知道:)。解决方案也是众所周知的:应该使用弱指针来打破“refcount interlocking”。还已知原则上不能自动解决该问题。解决它只是程序员的责任。
但是有一个积极的事情:程序员可以完全控制refcount值。我可以在调试器中暂停我的程序并检查obj1,obj2的引用计数并理解存在问题。我也可以在对象的析构函数中设置断点并观察破坏时刻(或者发现该对象没有被破坏)。
我的问题是关于Java,C#,ActionScript和其他“垃圾收集”语言。我可能会遗漏一些东西,但在我看来,他们
我经常听说这些语言不允许程序员泄漏内存,这就是为什么它们很棒。据我了解,他们只是隐藏了内存管理问题,并且很难解决它们。
最后,问题本身:
爪哇:
public class Obj
{
public Obj m_field;
}
{
Obj obj1 = new Obj();
Obj obj2 = new Obj();
obj1.m_field = obj2;
obj2.m_field = obj1;
}
答案 0 :(得分:8)
托管内存系统建立在您不希望首先跟踪内存泄漏问题的假设之上。而不是让它们更容易解决,你试着确保它们永远不会发生在一起。
Java对于“内存泄漏”确实有一个失败的术语,这意味着内存中的任何增长可能会影响您的应用程序,但是管理内存无法清除所有内存。
由于多种原因,JVM不使用引用计数
虽然JLS不禁止使用引用计数,但它并未在任何JVM AFAIK中使用。
相反,Java会跟踪许多根上下文(例如每个线程堆栈),并且可以跟踪哪些对象需要保留,哪些对象可以根据这些对象是否可以高度访问而被丢弃。它还为弱引用提供了工具(只要对象没有被清理就会被保留)和软引用(通常不会被清理但可以由垃圾收集者自行决定)
答案 1 :(得分:5)
AFAIK,Java GC的工作原理是从一组明确定义的初始引用开始,并计算可以从这些引用中获得的对象的传递闭包。任何无法访问的内容都是“泄露”的,可以进行GC编辑。
答案 2 :(得分:2)
Java具有独特的内存管理策略。所有东西(除了一些特定的东西)都在堆上分配,并且在GC开始工作之前不会被释放。
例如:
public class Obj {
public Object example;
public Obj m_field;
}
public static void main(String[] args) {
int lastPrime = 2;
while (true) {
Obj obj1 = new Obj();
Obj obj2 = new Obj();
obj1.example = new Object();
obj1.m_field = obj2;
obj2.m_field = obj1;
int prime = lastPrime++;
while (!isPrime(prime)) {
prime++;
}
lastPrime = prime;
System.out.println("Found a prime: " + prime);
}
}
C通过要求您手动释放'obj'的内存来处理这种情况,并且C ++计算对'obj'的引用并在它们超出范围时自动销毁它们。 Java确实不释放这个内存,至少在开始时没有。
Java运行时等待一段时间,直到感觉有大量内存被使用。之后,垃圾收集器开始运作。
让我们说java垃圾收集器决定在外循环的第10,000次迭代后清理。到目前为止已经创建了10,000个对象(这些对象已经在C / C ++中被释放)。
虽然外循环有10,000次迭代,但代码可能只引用新创建的obj1和obj2。
这些是GC'根',java用它来查找可能被引用的所有对象。垃圾收集器然后以递归方式向下迭代对象树,将“example”标记为对垃圾收集器根添加成活动。
垃圾收集器会破坏所有其他对象。 这确实会带来性能损失,但此过程已经过大量优化,对大多数应用程序而言并不重要。
与C ++不同,您不必担心所有的引用周期,因为只有从GC根可以访问的对象才会存在。
使用java应用程序,你做必须担心内存(思考列表保留所有迭代中的对象),但它没有其他语言那么重要。
至于调试:Java调试高内存值的想法是使用一个特殊的“内存分析器”来找出仍然在堆上的对象,不担心什么是引用什么。< / p>
答案 3 :(得分:1)
关键区别在于Java等你根本没有涉及处理问题。这可能感觉像一个非常可怕的位置,但令人惊讶的是赋予权力。您过去必须做出的关于谁负责处理已创建对象的所有决定都已消失。
它确实有意义。系统更了解什么是可达的,什么不是你。它还可以在何时拆除结构等方面做出更加灵活和明智的决策。
基本上 - 在这种环境中,您可以以更复杂的方式处理对象,而不必担心丢弃对象。你现在唯一需要担心的是,如果你不小心将一个胶水粘在天花板上。
作为一名前C程序员,我感到很痛苦。
重新 - 你的最后一个问题 - 这不是内存泄漏。当GC开始时,除了可到达的内容之外,所有内容都会被丢弃。在这种情况下,假设您已发布obj1
和obj2
两者都不可访问,因此它们都将被丢弃。
答案 4 :(得分:1)
垃圾收集不是简单的重新计算。
您演示的循环引用示例不会出现在垃圾收集的托管语言中,因为垃圾收集器会希望将分配引用一直追溯到堆栈上的某些内容。 如果某处没有堆栈引用,那就是垃圾。像shared_ptr
这样的引用计数系统并不那么聪明,并且可能(就像你演示的那样)在堆中的某个地方放置两个对象,以防止彼此被删除。
答案 5 :(得分:0)
垃圾收集语言不允许您检查refcounter,因为它们没有任何人。垃圾收集与refcounted内存管理完全不同。真正的区别在于决定论。
{
std::fstream file( "example.txt" );
// do something with file
}
// ... later on
{
std::fstream file( "example.txt" );
// do something else with file
}
在C ++中,您可以保证在关闭第一个块之后关闭example.txt,或者抛出异常。用Java比较它
{
try
{
FileInputStream file = new FileInputStream( "example.txt" );
// do something with file
}
finally
{
if( file != null )
file.close();
}
}
// ..later on
{
try
{
FileInputStream file = new FileInputStream( "example.txt" );
// do something with file
}
finally
{
if( file != null )
file.close();
}
}
如您所见,您已经为所有其他资源管理交换了内存管理。这是真正的差异,refcounted对象仍然保持确定性破坏。在垃圾收集语言中,您必须手动释放资源,并检查异常。有人可能会争辩说显式内存管理可能很乏味且容易出错,但在现代C ++中,它可以通过智能指针和标准容器来缓解。你仍然有一些责任(例如循环引用),但想想有多少catch / finally阻止你可以避免使用确定性破坏以及输入多少Java / C#/等。程序员必须做(因为他们必须手动关闭/释放除内存以外的资源)。我知道在C#中使用了语法(在最新的Java中有类似的东西),但它只涵盖了块范围的生命周期,而不是更普遍的共享所有权问题。