垃圾回收与非垃圾回收编程语言

17

如果我理解正确,垃圾回收会自动释放程序不再使用的对象,就像Java中的垃圾收集器。

我听说在不支持垃圾回收的语言(比如C语言)中,程序可能存在内存泄漏,并最终耗尽内存。

那么,在不支持垃圾回收的语言(比如C语言)中,程序员会犯哪些错误呢?我猜测是没有在不再使用对象后释放它们。但是,由于缺少垃圾收集器,这是我们唯一可能犯的错误吗?


1
在垃圾回收环境中,您仍然可能会出现“过度根对象”的泄漏。也就是说,这些对象仍然被强引用,但您不再需要它们。 - bbum
11个回答

19
  • 释放你需要的东西

  • 不释放你不再需要的东西(因为你没有很好地跟踪分配/使用/释放)

  • 重新分配已存在的实例(未正确跟踪的副作用)

  • 释放已经释放过的内容

  • 释放不存在的内容(空指针)

可能还有其他问题。关键是:管理内存很麻烦,最好使用某种跟踪机制和分配/释放抽象来处理。因此,最好将其构建到语言中,以使其变得简单易用。手动内存管理并不是世界末日—当然可以实现—但如今,除非您正在编写实时代码、硬件驱动程序或(也许)最新游戏的超级优化核心代码,否则除了作为学术练习外,手动管理内存并不值得付出努力。


1
哦,还有一个问题:手动管理数组长度(调整数组大小以加载更多要放入其中的项目等)最多是繁琐的。每次增加一个项目效率低下,因此您倾向于开始跟踪已使用的插槽、实际分配的插槽、所需的插槽等。而且,您真的不想从该数组中间开始释放东西,然后缩小它。与像Python这样的现代语言的轻松数组和字典相比,这真的是低级别的东西。 - Lee B
9
关于你上一个观点的提及。释放 NULL 是完全没有问题的。 标准保证 free(NULL) 是完全安全的。 - Evan Teran
1
低级语言如C不仅仅是用于超级优化的代码或驱动程序。有些事情在C中更有效率,这也是它至今仍然是最受欢迎的语言(至少对于开源项目而言)的原因。 - Kasper
7
当然,但所有这些事情都可以更高效地通过手工汇编完成,绕过操作系统,由精通处理器、内存、芯片组、硬盘几何等方面的专家来完成。问题不在于是否更高效,而在于是否合理花费额外时间去跟踪所有这些内容,以换取相对较小的效率提升,考虑到使用更高级别的工具可以更快速地进行开发。 - Lee B

13
在我看来,垃圾回收语言与非垃圾回收语言存在互补问题。对于每个问题,都存在一个非垃圾回收特征错误和一个垃圾回收特征错误 - 非垃圾回收程序员的责任和垃圾回收程序员的责任。
垃圾回收程序员可能认为他们不需要负责释放对象,但是对象除了内存之外还持有其他资源 - 这些资源通常需要及时释放,以便它们可以在其他地方获取 - 例如文件句柄、记录锁、互斥量等。
当非垃圾回收程序员遇到悬空引用时(很多情况下这并不是一个错误,因为某些标志或其他状态会将其标记为不可使用),垃圾回收程序员则会出现内存泄漏。因此,非垃圾回收程序员负责确保适当调用free/delete,而垃圾回收程序员负责确保不需要的引用被适当地置空或处理掉。
这里有一个说法,即智能指针无法处理垃圾循环。这并不一定正确 - 存在可以打破循环并确保及时处理垃圾内存的引用计数方案,至少有一个Java实现使用(可能仍在使用)一种引用计数方案,该方案可以很容易地在C++中实现为智能指针方案。

引用计数系统中的并发循环收集

当然,这通常不会被实现 - 部分原因是可以直接使用带垃圾回收功能的语言,但在我的看法中也部分是因为这将打破C++中的关键约定。你知道,许多C++代码(包括标准库)严重依赖资源分配即初始化(RAII)约定,而这需要可靠和及时的析构函数调用。在任何处理循环引用的GC中,你根本就不能保证它们的存在。在解决垃圾循环时,你无法确定要先调用哪个析构函数,因为可能会有更多的循环依赖关系,不仅仅是内存引用,甚至不可能解决。解决方案-在Java等语言中,不存在确保最终器会被调用的保证。垃圾回收只收集一种非常特定的垃圾 - 内存。所有其他资源都必须手动清理,就像在Pascal或C中一样,并且没有可靠的C++样式的析构函数作为优势。

最终结果是,在C++中“自动化”清理的许多工作必须在Java、C#等语言中手动完成。当然,“自动化”需要加引号,因为程序员负责确保对于任何堆分配对象都适当地调用delete - 但是在GC语言中,有不同但互补的程序员责任。无论哪种方式,如果程序员未能正确处理这些责任,就会出现错误。
[编辑 - 在某些情况下,Java、C#等显然可以可靠地(如果不一定及时)进行清理,文件就是一个例子。这些是不可能发生引用循环的对象 - 要么是因为(1)它们根本不包含引用,(2)有一些静态证明它所包含的引用不能直接或间接地导致另一个相同类型的对象,或者(3)运行时逻辑确保虽然链/树/任何可能的循环是可能的,但不会发生循环。情况(1)和(2)对于资源管理对象而不是数据结构节点来说非常普遍 - 可能是普遍的。编译器本身不能合理地保证(3)。因此,尽管编写最重要的资源类的标准库开发人员可以确保这些资源的可靠清理,但通常规则仍然是,对于GC,无法保证非内存资源的可靠清理,这可能会影响应用程序定义的资源。]

坦白地说,从非GC(垃圾回收)切换到GC(或反之亦然)并不是什么魔法棒。这可能会让通常的问题消失,但这只意味着您需要掌握新的技能来防止(和调试)一组全新的问题。

一个优秀的程序员应该超越“你站哪边”的无用争论,学会处理两种情况。


我希望框架设计者能够为具有明确所有者的实体提供确定性生命周期管理,同时对于无所有者的对象使用GC。请注意,即使是具有所有者的实体也应该被包装在GC收集的对象中,以确保只要存在对死对象的引用,它将继续是对同一死对象的引用。 GC在确保程序安全和正确性方面具有巨大优势,但通常正确使用可变对象(即使是没有资源的对象)需要清晰的所有权概念。像List<T>这样的东西可能不需要... - supercat
如果代码持有对 List<T> 的引用,而它期望某个其他对象将某些东西放入其中,而该其他对象已经放弃了该列表并开始使用另一个列表,那么这种情况可能更容易被检测到,如果拥有该列表的对象在放弃它之前使其无效,则可以避免此类问题。因此,应该将不再需要的对象“处理”掉。 - supercat

8

好的,你可能会犯以下错误:

  • 没有释放你不需要的东西
  • 释放你需要的东西

还有其他错误可以犯,但这些是与垃圾回收相关的错误。


3
除了silky所说的,你还可以对某些内容进行双重释放。

2
在C语言中,使用malloc分配内存后,需要手动调用free来释放内存。尽管这听起来并不那么糟糕,但在处理指向相同数据的单独数据结构(例如链表)时,情况可能变得非常混乱。您可能会访问已释放的内存或重复释放内存,这两者都会导致错误,并可能引入安全漏洞。
此外,在C++中,您需要小心混合使用new[]/deletenew/delete[]
例如,内存管理是需要程序员准确了解原因的东西。
const char *getstr() { return "Hello, world!" }

很好,但是

const char *getstr() {
    char x[BUF_SIZE];
    fgets(x, BUF_SIZE, stdin);
    return x;
}

这是一个非常糟糕的事情。


2
请记住,任何有经验的C++程序员都会对使用原始内存感到不安。封装它,摒弃它。 - GManNickG

2
除了其他评论之外,手动内存管理使某些高性能并发算法更加困难。

1
垃圾回收使得其他高性能并发算法更加困难,尽管性能问题往往被归咎于垃圾回收器,因为它必须搜索垃圾,而这些垃圾本来可以通过内存管理轻松处理。撇开宗教战争不谈,实际上这是一种权衡取舍。 - user180247
哎呀 - 当然是“可以使用手动内存管理轻松处理”的意思。 - user180247

2
一些非GC语言提供了称为引用计数智能指针的结构。这些尝试通过自动化一些管理功能来解决一些问题,例如忘记释放内存或尝试访问无效内存等。
正如一些人所说,您必须对“智能指针”保持“聪明”。智能指针有助于避免整个类别的问题,但引入了它们自己的问题类别。
许多智能指针可以通过以下方式创建内存泄漏:
- 循环引用(A指向B,B指向A)。 - 智能指针实现中的错误(在成熟库(如Boost)上很少见)。 - 将原始指针与智能指针混合使用。 - 线程安全性。 - 不正确地附加或分离原始指针。
在完全GC环境中不应遇到这些问题。

1
一些GC语言也是如此:http://blogs.msdn.com/b/bclteam/archive/2005/03/16/396900.aspx - gbjbaanb

0
另一个常见的错误是在释放内存后读取或写入它(已经被重新分配并且现在正在用于其他目的的内存,或者尚未被重新分配的内存,因此当前仍由堆管理器拥有而不是您的应用程序)。

0
通常,具有垃圾回收功能的编程语言会限制程序员对内存的访问,并依赖于一种内存模型,其中对象包含以下内容:
  • 引用计数器 - 垃圾回收器使用此计数器来确定对象何时未被使用
  • 类型和大小信息 - 以消除缓冲区溢出(并帮助减少其他错误)。
与非垃圾回收语言相比,该模型和受限制的访问方式可以减少/消除两类错误:
  1. 内存模型错误,例如:

    • 内存泄漏(未在完成后释放),
    • 释放超过一次的内存,
    • 释放未分配的内存(如全局或堆栈变量),
  2. 指针错误,例如:

    • 未初始化的指针,带有先前使用的“剩余”位,
    • 在释放后访问内存,特别是写入内存(非常恶心!)
    • 缓冲区溢出错误,
    • 将内存用作错误类型(通过强制转换)。
还有更多,但这些是最重要的。

0
C ++中的内存泄漏不是语言固有的问题,而是由程序错误导致的。 Valgrind是一个很好的工具,可以找到这些错误,使修复变得容易。智能指针是语言中的内置工具,在超出范围时帮助自动删除内容,但循环引用可能会阻止从父级引用到成员和该成员到其父级的最后引用被删除。重要的是,如果避免使用智能指针进行循环引用(对于这些情况使用RAW指针或“&”引用),并且通过Valgrind运行您的C ++代码,则不会发生内存泄漏。您的代码将比Java快大约2倍,而不需要做任何特殊处理。这不是为了支持C ++而推荐Java,Java具有其他有用的功能,如反射。除了语言特性之外,Java和C ++之间决定性因素通常是开发人员时间或计算资源更加“昂贵”,无论您选择使用哪种度量标准。 Java是工业级应用/架构语言。 C ++是性能高效的计算语言,可以从几乎任何其他语言调用。我正在处理的项目主要是Java架构,其中包含一些C ++组件(遗留代码)和需要高性能的内容(我是团队上的C ++人员,正在编写需要高性能的数学密集型代码)

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