VB.net垃圾收集器无法释放对象

7
首先,预先感谢你的帮助。
几个月的艰苦工作后,我决定在像这样的论坛上寻求帮助,因为我找不到解决我的问题的方法。
这可以描述为“为什么在VB.net中创建的对象在释放时即使强制启动GC,也不会被GC释放?”
请考虑以下代码片段。显然,我的项目要复杂得多,但是我能够隔离出问题:
Imports System.Data.Odbc
Imports System.Threading
Module Module1
    Sub Main()
        'Declarations-------------------------------------------------      
            Dim connex As OdbcConnection 'Connection to the DB
            Dim db_Str As String         'ODBC connection String      
        'Sentences----------------------------------------------------
            db_Str = "My ODBC connection String to my MySQL database"
            While True
                'Condition: Infinite loop.
                connex = New OdbcConnection(db_Str)
                connex.Open()
                connex.Close()

                'Release created objects
                connex.Dispose()

                'Force the GC to be launched
                GC.Collect()

                'Send the application to sleep half a second
                System.Threading.Thread.Sleep(500)
            End While
    End Sub
End Module

这是模拟一个多线程应用程序与MySQL数据库建立连接的过程。可以看到连接是作为新对象创建,然后释放掉。最后,强制启动了GC。我在几个论坛以及MSDN在线帮助中都看到了这个算法,因此就我而言,我没有做错任何事情。
问题出现在应用程序启动时。虽然在代码中已经释放了创建的对象,但是过一段时间后,可用内存被耗尽,导致应用程序崩溃。
当然,在这个小版本中很难看出这个问题,但是在实际项目中,由于随着时间推移建立的连接数量增加,应用程序很快就会耗尽内存,结果只能运行两天。然后我需要再次重启应用程序。
我在我的机器上安装了一个内存分析工具(Scitech .Net Memory profiler 4.5,可从此处下载试用版),其中有一个名为“调查内存泄漏”的部分。当我在“实时”选项卡上看到这个图形时,我感到非常惊讶。如果我没记错的话,这个图示告诉我,代码中创建的所有对象都没有被释放:
(插入了一张图片)
当我看到这张图片时,我的惊讶甚至更大了。根据这个图示,所有未释放的对象都是System.Transactions类型,我认为这些对象在.NET库中被内部管理,因为我没有在代码中创建任何这种类型的对象。这是否意味着VB.net标准库中存在一个bug???
(再插入一张图片)
请注意,在我的代码中,我没有执行任何查询。如果我这样做的话,即使调用.Close()方法,ODBCDataReader对象也不会被释放(令人惊讶的是,这种类型的未释放对象数量与类型System.Transactions的未释放对象数量完全相同)。
另一个重要的事情是声明GC.Collect()。这是内存分析工具用来刷新要显示的信息的。如果你从代码中删除它,分析器将无法正确更新实时图,给你错误的印象,认为一切都正确。
最后,请注意如果省略 connex.Open() 语句,则截图#1将呈现平坦的线条(这意味着所有已创建的对象都已成功释放),但不幸的是,如果连接未打开,我们就无法对数据库进行任何查询。
有人能找到一个合乎逻辑的解释,并有效地释放对象吗?谢谢大家。

3
MySQL提供程序是一个众所周知的麻烦制造者。只要这些对象被收集,它们没有被处理也没关系。情况就是这样,你已经没有剩下的了。搜索"mysql内存泄漏"以获得相关信息。 - Hans Passant
嗨,汉斯,谢谢你的回答。我在我的自动回复中加入了一些评论。干杯。 - Nicolas
2个回答

5
Dispose与垃圾回收没有任何关系。垃圾回收仅涉及托管资源(内存)。Dispose与内存完全无关,仅与非托管资源相关(数据库连接、文件句柄、gdi资源、套接字……任何不是内存的东西)。两者之间唯一的关系是对象如何被终结,因为许多对象经常被实现为处理它们将抑制终结并且终结它们将调用.Dispose()。显式地Dispose()一个对象永远不会导致它被回收1
显式调用垃圾回收器几乎总是一个坏主意。.Net使用分代垃圾回收器,因此自己调用的主要影响是你将保留内存更长时间,因为通过强制较早进行收集,你可能会检查项目在其根本不符合收集条件时,这会将它们发送到较高级别的代中,这些代不太可能被收集。否则,这些项会留在较低的代中,并在GC下次自行运行时变得符合收集条件。你现在可能需要为分析器使用GC.Collect(),但你应该尝试将其从生产代码中删除。
你提到你的应用程序在崩溃前运行了两天,并且没有对你的实际生产代码进行分析(或显示结果),因此我认为分析器在某种程度上会误导你。你已经将代码削减到可以产生一个内存泄漏,但我不确定它是否是你在生产中看到的内存泄漏。这部分是由于重现错误所需的时间差异,但也是出于“直觉”。我之所以提到这一点,是因为我要建议的一些内容可能在考虑了你的分析器结果后立即变得不合理。那么,我不确定你失去的内存是怎么回事,但我可以猜测几个。
第一个猜测是你真正的代码有try/catch块。抛出异常……也许不是每个连接都会发生,但有时会发生。当这种情况发生时,catch块允许你的程序继续运行,但你跳过了connex.Dispose()行,因此留下了未关闭的连接。这些连接最终将为数据库创建拒绝服务情况,这可能会表现为多种方式。这里的更正是确保您始终使用finally块来处理任何您.Dispose()的东西。无论您当前是否有try/catch块,这都是正确的,并且非常重要,以至于我会说你目前发布的代码基本上是错误的:你需要一个try/finally。这里有一个快捷方式,可以通过using块来实现。
下一个猜测是,您的一些真实命令最终变得相当大,可能涉及大量字符串或图像(byte [])数据。在这种情况下,项目会出现在称为大对象堆(LOH)的特殊垃圾回收代中。LOH很少被收集,并且几乎从不被压缩。将压缩视为类似于对硬盘进行碎片整理时发生的情况。如果您的项目进入LOH,则可能会出现物理内存本身被释放(收集),但您的进程内(通常限制为2GB)的地址空间未被释放(压缩)的情况。您的内存地址空间中有未被回收的空洞。物理RAM可用于系统的其他进程,但随着时间的推移,这仍然会导致您看到的OutOfMemory异常。大多数情况下,这并不重要:大多数.NET程序都是短暂的面向用户的应用程序,或者是ASP.Net应用程序,在提供页面后可以关闭整个线程。由于您正在构建类似于应该运行数天的服务,因此必须更加小心。修复可能涉及显着重新编写一些代码,以避免完全创建大型对象。这可能意味着反复使用单个或一小组字节数组,或者对于非常大的SQL查询或SQL查询数据,使用流技术而不是字符串连接或字符串构建器。这也可能意味着您将其作为每天运行并在一天结束时关闭自身的计划任务或按需调用的程序来执行。
最后一个猜测是,您正在进行的某些操作导致您的连接对象仍然以某种方式被程序“reachable”。事件处理程序是此类错误的常见来源,尽管我认为在连接上具有事件处理程序很奇怪,特别是因为这不是您示例的一部分。
1 我想我可以编造一种情况来使这种情况发生。一种简单的方法是构建一个对象,假定该类型的所有对象都有全局集合...对象在构造时将自己添加到集合中,并在处理后将自己删除。通过这种方式,对象在处理之前无法被回收,因为在那个点之前它仍然是可达的...但这将是一个非常有缺陷的程序设计。

订阅长寿对象事件的对象通常在取消订阅事件之前不会被回收。在许多情况下,“取消订阅”的唯一逻辑位置是在Dispose中。一旦这样的对象被Dispose,它将变得可回收;除非或直到事件发布者本身变得可回收,否则在未被处理之前将不可回收。 - supercat
嗨[supercat]。不确定你的意思是什么。我在我发布的示例中没有实现任何事件。该应用程序纯粹是DOS控制台(因此没有加载Windows Forms库)。无论如何感谢。 - Nicolas

1

感谢大家提供的非常有帮助的答案。

Joel,你是对的。这段代码会产生“泄漏”,但不一定与我在真实项目中遇到的“泄漏”问题相同,尽管它们会产生相同的症状,即未释放对象的数量不断增加(最终将耗尽内存)。所以我想知道它有什么问题,因为一切似乎都编码正确。我不明白为什么它们没有被处理/收集。但根据分析器,它们仍然在内存中,并最终会阻止创建新对象。

你对我的“真实”项目的猜测之一正中要害。我意识到我的“catch”块没有调用对象处理,现在已经修复了这个问题。感谢您宝贵的建议。但是,我在上面的示例代码中实现了“using”子句,但实际上并没有解决问题。

Hans,你也是对的。在发布问题后,我已更改了上述代码中的库,以连接到MySQL。

旧库(在示例中):

System.Data.Odbc

新的库:

System.Data
Microsoft.Data.Odbc

使用新的分析器,分析结果呈现为一条平直的线,而且在代码上没有做任何更改,这正是我一直在寻找的。因此我的结论与你的相同,即旧的分析器可能存在某些内部错误,导致出现这种情况,使它们成为真正的“麻烦制造者”。

现在我记得最初在我的项目中使用了新的库(System.DataMicrosoft.Data.Odbc),但很快就改用了旧的库(System.Data.Odbc),因为新的库不允许打开多个活动记录集(MARS)。我的应用程序对MySQL数据库进行了大量查询,但不幸的是,连接数有限。因此,我最初以这样的方式实现了我的真实代码,使其只建立了少量连接,但它们在代码中共享(通过将连接作为参数在函数之间传递)。这非常好,因为(例如)我需要检索一个记录集(比如客户),并同时进行很多检查(例如,客户至少有一个发票,客户有重复的电子邮件地址等,这涉及到很多侧面查询)。使用“旧”的库,同一连接可以创建多个命令并执行不同的查询。

"新的"库不允许MARS。我只能在每个会话/连接中创建一个命令(即执行查询)。如果我需要执行另一个查询,我需要关闭先前的记录集(实际上无法做到,因为我正在迭代它),然后进行新的查询。
我必须找到两个问题之间的平衡点。所以我最终使用了“新的库”,因为内存问题,并重新编码我的应用程序,以便不共享连接(因此每个过程在需要时都会创建一个新连接),并减少应用程序可以同时进行的连接数,以避免耗尽连接池。
这个解决方案还远非理想,因为它在应用程序中引入了杂乱的逻辑(理想情况是迁移到SQL服务器),但它给我带来了更好的结果,而且应用程序在新版本的早期阶段更加稳定。
再次感谢您的建议,希望您也会发现我的建议有用。
祝好,
Nico

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