为什么要收集垃圾?为什么编译器不自动插入 free() 呢?
Why garbage collection? Why not compilers auto-insert free() instead?
为什么我们不让编译器在适当的地方自动插入 free(),而不是 运行在 运行 时间定期进行垃圾检测?这样,我们只需在编译时支付一次价格。
编译器知道变量超出范围或重新分配给不同对象的位置。因此,它可以找出对象是否不再可达,并在那里自动插入 free()。
不能吗?为什么?
如果是因为多线程,我们可以用single-threaded/green-threaded种语言来做吗?
您可以在此 Wikipedia article on Reference Counting 中阅读更多相关内容,引用计数优于垃圾回收有两个主要缺点:
- 维护引用的开销:从性能的角度来看,这是相当可观的。每次任何其他对象引用另一个对象或引用超出范围时,JVM 都需要递增 and/or 递减计数。此外,它会产生更多 space,以维持引用计数;每个对象将至少消耗一个额外的四字节整数。
- 循环引用的问题:这个好像是最大的原因。 Java 中使用的许多肉类和土豆数据结构都具有循环引用。想到的第一个用例是链表中的节点。对于现实世界的问题和更深奥的数据结构,问题变得更加复杂。
The compiler knows the places where a variable goes out of scope or gets reassigned to different object.
当然可以 - 对于变量。但是你不清除变量 - 你清除它们指向的内存。仅仅因为一个变量超出了范围,并不意味着指向的内存不再可达。
例如:
y = ...
{
x = new X();
if (todayIsTuesday()) {
y = x;
}
} // x just went out of scope
您无法compile-time决定是否应在该段的最后一行释放 x 指向的内存,因为这取决于该代码是星期几运行.
所以要解决这个问题,这个决定必须委托给 run-time,通过插入适当的逻辑,例如:
Y* y = ...
{
X* mem = new X();
X* x = mem;
markPointer(&x, mem);
if (todayIsTuesday()) {
y = x;
markPointer(&y, mem);
}
markNoLongerPointer(&x, mem);
} // x just went out of scope
如果 internally-maintained 数据结构告诉它 x
是对该内存的唯一引用,则 markNoLongerPointer()
清除作为第二个参数给出的内存...换句话说,这是 reference-counting 逻辑的粗略起点。
编译器当然可以将这样的 reference-counting 逻辑添加到编译后的代码中,有些确实如此,但正如其他人提到的,引用计数有一些缺点:高开销、循环问题,加上它有时会导致显着在不方便的时候暂停,当对大型数据结构的根的唯一引用超出范围时。虽然有一些方法可以解决其中的一些缺点,但这超出了这个答案的范围:-)
从根本上说,在 non-trivial 资源管理情况下,程序员无论如何都必须 free
内存(和其他资源)。它可能不完全类似于对 free
的调用;它可能正在从列表中删除一个 object 引用以便垃圾收集器可以收集它,或者将其设置为空引用,或者释放一个智能指针,或者从列表中删除一个 object,或者对 free
的真正呼吁。然而,在 non-trivial 情况下,程序员仍必须以某种方式显式指定它,以避免逻辑泄漏。
这无法在 compile-time 确定,直到我们有编译器可以开始读懂我们的想法并弄清楚我们正在制作什么类型的软件,或者直到我们开始以完全不同的方式编写代码。因为想象一下,比方说,像 Photoshop 这样的图像编辑器。什么时候应该释放图像?当用户关闭它时。这对我们人类来说是显而易见的,但这不是编译器所拥有的信息。
数字音频工作站的类似案例。什么时候应该从内存中释放音频剪辑?当用户将其从剪辑编辑器中删除时,例如同样,这对人类设计师来说是显而易见的,但对编译器来说却不是。我的 80 年代音乐 collection 何时应该从我的硬盘中删除以保存 space?当我(用户)明确删除它们时。在软件可以开始可靠地读懂我的想法之前,它不会比这少 manual/explicit。
因此,对于这些 non-trivial 情况,无论您有垃圾 collection 或 RAII 还是其他任何东西,都没有自动资源管理。在这种情况下,程序员必须始终显式 free
资源以响应适当的 inputs/events 而不管,因为人类的想法和设计非常特定于应用程序的领域,决定何时应释放这些资源。
当然,对于一些琐碎的情况,例如分配给函数的给定范围的本地内存,编译器可以自动释放它,而不会像垃圾 collection 那样大,他们确实这样做了。即使在 C:
void some_func(...)
{
int some_array[64];
...
// some_array's memory will be automatically freed from
// the stack when exiting the function.
}
... 并且 C 基本上在做与您所建议的类比等效的事情。它将生成指向 increment/decrement 堆栈指针的指令,例如,有效地分配和释放内存并使释放的内存可用于其他地方。使用 C++,它甚至可以在使用 RAII 时为 heap-allocated 内存执行此操作,前提是拥有该内存的 object 在退出函数范围时变得无法访问。但是这些类型的临时资源,其生命周期与具有 stack-like allocation/deallocation push/pop 模式的函数范围相关联是微不足道的情况。 non-trivial 案例将始终要求程序员明确指定何时不再需要某种资源,因为 non-trivial 案例处理持久状态,其生命周期与任何给定范围无关功能。
为什么我们不让编译器在适当的地方自动插入 free(),而不是 运行在 运行 时间定期进行垃圾检测?这样,我们只需在编译时支付一次价格。
编译器知道变量超出范围或重新分配给不同对象的位置。因此,它可以找出对象是否不再可达,并在那里自动插入 free()。
不能吗?为什么?
如果是因为多线程,我们可以用single-threaded/green-threaded种语言来做吗?
您可以在此 Wikipedia article on Reference Counting 中阅读更多相关内容,引用计数优于垃圾回收有两个主要缺点:
- 维护引用的开销:从性能的角度来看,这是相当可观的。每次任何其他对象引用另一个对象或引用超出范围时,JVM 都需要递增 and/or 递减计数。此外,它会产生更多 space,以维持引用计数;每个对象将至少消耗一个额外的四字节整数。
- 循环引用的问题:这个好像是最大的原因。 Java 中使用的许多肉类和土豆数据结构都具有循环引用。想到的第一个用例是链表中的节点。对于现实世界的问题和更深奥的数据结构,问题变得更加复杂。
The compiler knows the places where a variable goes out of scope or gets reassigned to different object.
当然可以 - 对于变量。但是你不清除变量 - 你清除它们指向的内存。仅仅因为一个变量超出了范围,并不意味着指向的内存不再可达。
例如:
y = ...
{
x = new X();
if (todayIsTuesday()) {
y = x;
}
} // x just went out of scope
您无法compile-time决定是否应在该段的最后一行释放 x 指向的内存,因为这取决于该代码是星期几运行.
所以要解决这个问题,这个决定必须委托给 run-time,通过插入适当的逻辑,例如:
Y* y = ...
{
X* mem = new X();
X* x = mem;
markPointer(&x, mem);
if (todayIsTuesday()) {
y = x;
markPointer(&y, mem);
}
markNoLongerPointer(&x, mem);
} // x just went out of scope
如果 internally-maintained 数据结构告诉它 x
是对该内存的唯一引用,则 markNoLongerPointer()
清除作为第二个参数给出的内存...换句话说,这是 reference-counting 逻辑的粗略起点。
编译器当然可以将这样的 reference-counting 逻辑添加到编译后的代码中,有些确实如此,但正如其他人提到的,引用计数有一些缺点:高开销、循环问题,加上它有时会导致显着在不方便的时候暂停,当对大型数据结构的根的唯一引用超出范围时。虽然有一些方法可以解决其中的一些缺点,但这超出了这个答案的范围:-)
从根本上说,在 non-trivial 资源管理情况下,程序员无论如何都必须 free
内存(和其他资源)。它可能不完全类似于对 free
的调用;它可能正在从列表中删除一个 object 引用以便垃圾收集器可以收集它,或者将其设置为空引用,或者释放一个智能指针,或者从列表中删除一个 object,或者对 free
的真正呼吁。然而,在 non-trivial 情况下,程序员仍必须以某种方式显式指定它,以避免逻辑泄漏。
这无法在 compile-time 确定,直到我们有编译器可以开始读懂我们的想法并弄清楚我们正在制作什么类型的软件,或者直到我们开始以完全不同的方式编写代码。因为想象一下,比方说,像 Photoshop 这样的图像编辑器。什么时候应该释放图像?当用户关闭它时。这对我们人类来说是显而易见的,但这不是编译器所拥有的信息。
数字音频工作站的类似案例。什么时候应该从内存中释放音频剪辑?当用户将其从剪辑编辑器中删除时,例如同样,这对人类设计师来说是显而易见的,但对编译器来说却不是。我的 80 年代音乐 collection 何时应该从我的硬盘中删除以保存 space?当我(用户)明确删除它们时。在软件可以开始可靠地读懂我的想法之前,它不会比这少 manual/explicit。
因此,对于这些 non-trivial 情况,无论您有垃圾 collection 或 RAII 还是其他任何东西,都没有自动资源管理。在这种情况下,程序员必须始终显式 free
资源以响应适当的 inputs/events 而不管,因为人类的想法和设计非常特定于应用程序的领域,决定何时应释放这些资源。
当然,对于一些琐碎的情况,例如分配给函数的给定范围的本地内存,编译器可以自动释放它,而不会像垃圾 collection 那样大,他们确实这样做了。即使在 C:
void some_func(...)
{
int some_array[64];
...
// some_array's memory will be automatically freed from
// the stack when exiting the function.
}
... 并且 C 基本上在做与您所建议的类比等效的事情。它将生成指向 increment/decrement 堆栈指针的指令,例如,有效地分配和释放内存并使释放的内存可用于其他地方。使用 C++,它甚至可以在使用 RAII 时为 heap-allocated 内存执行此操作,前提是拥有该内存的 object 在退出函数范围时变得无法访问。但是这些类型的临时资源,其生命周期与具有 stack-like allocation/deallocation push/pop 模式的函数范围相关联是微不足道的情况。 non-trivial 案例将始终要求程序员明确指定何时不再需要某种资源,因为 non-trivial 案例处理持久状态,其生命周期与任何给定范围无关功能。