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,可 在此处下载试用版)。有一个名为“调查内存泄漏”的部分。当我在“实时”选项卡上看到这个时,我感到非常惊讶。如果我是正确的,这张图告诉我在代码上创建的所有对象都没有被实际释放:

http://www.zuzsso.com/images/screenshot3.jpg

当我看到另一个屏幕时,惊喜就更大了。据此,所有未处理的对象都是System.Transactions类型,我假设它是在 .Net 库中内部管理的,因为我没有在我的代码上创建任何这种类型的对象。这是否意味着 VB.net 标准库存在错误???:

http://www.zuzsso.com/images/screenshot4.jpg

请注意,在我的代码中,我没有执行任何查询。如果这样做,即使我调用.Close()方法,也不会释放ODBCDataReader对象(令人惊讶的是,这种类型的未释放对象的数量与System.Transactions类型的未释放对象的数量完全相同)

另一个重要的事情是声明GC.Collect()。内存分析器使用它来刷新要显示的信息。如果您从代码中删除它,分析器将不会正确更新实时图表,从而给您一种一切都正确的错误印象。

最后,如果你省略connex.Open()语句,截图 #1 将呈现一条平线(这意味着所有创建的对象都已成功释放),但不幸的是,如果我们无法对数据库进行任何查询,如果连接尚未打开。

有人可以找到对此的合乎逻辑的解释,以及有效释放对象的解决方法吗?

谢谢大家。

尼科

4

2 回答 2

5

Dispose与垃圾收集无关。垃圾收集仅与托管资源(内存)有关。Dispose 根本与内存无关,只与非托管资源相关(数据库连接、文件句柄、gdi 资源、套接字......任何不是内存的东西)。两者之间的唯一关系与对象的最终确定方式有关,因为许多对象通常是这样实现的,即释放它们会抑制最终确定,最终确定它们将调用 .Dispose()。显式地 Disposing() 一个对象永远不会导致它被收集1

显式调用垃圾收集器几乎总是一个坏主意。.Net 使用分代垃圾收集器,因此自己调用它的主要效果是您将保留更长时间的内存,因为通过更早地强制收集,您可能会在项目完全符合收集条件之前对其进行检查,这会将它们发送到不太频繁收集的高阶生成中。否则,这些项目将保留在较低的一代中,并在 GC 下一次自行运行时有资格收集。您现在可能需要为分析器使用 GC.Collect(),但您应该尝试为您的生产代码删除它。

您提到您的应用程序在崩溃前运行了两天,并且没有对您的实际生产代码进行分析(或显示结果),所以我也认为分析器在一定程度上误导了您。您已将代码缩减为导致内存泄漏的代码,但我不确定这是在生产中看到的内存泄漏这部分是因为重现错误的时间不同,但这也是“本能”。我提到这一点是因为根据您的分析器结果,我将建议的一些内容可能不会立即有意义。顺便说一句,我不确定你失去的记忆是怎么回事,但我可以做出一些猜测。

第一个猜测是你的真实代码有 try/catch 块。抛出异常......也许不是在每个连接上,但有时。发生这种情况时,catch 块允许您的程序继续运行,但您跳过了connex.Dispose()行,因此留下了悬空的连接。这些连接最终会为数据库创建拒绝服务的情况,这可以通过多种方式表现出来。这里的更正是确保您始终对您的任何 .Dispose() 使用 finally 块。无论您当前是否有一个 try/catch 块,这都是正确的,而且重要的是我会说您到目前为止发布的代码根本上是错误的:您需要一个 try/finally。有一个快捷方式,通过一个using块。

下一个猜测是,您的一些实际命令最终会相当大,可能涉及大字符串或图像 (byte[]) 数据。在这种情况下,项目最终会出现在称为大对象堆 (LOH) 的特殊垃圾收集器生成中。LOH 很少被收集,几乎从不压缩。将压缩视为类似于对硬盘进行碎片整理时发生的情况。如果您有项目进入 LOH,您最终可能会遇到物理内存本身被释放(收集)但地址空间在您的进程中(通常限制为 2GB)不会被释放(压缩)。您的内存地址空间中存在无法回收的漏洞。物理 RAM 可用于您的系统用于其他进程,但随着时间的推移,这仍然会导致您看到的相同类型的 OutOfMemory 异常。大多数时候这无关紧要:大多数 .Net 程序都是短命的面向用户的应用程序,或者是 ASP.Net 应用程序,在这些应用程序中,在提供页面后整个线程都可以被拆除。由于您正在构建一个应该运行数天的服务,因此您必须更加小心。该修复可能涉及对某些代码进行重大修改,以避免创建大对象。这可能意味着一遍又一遍地重复使用单个或一小组字节数组,或者使用流技术而不是字符串连接或字符串构建器来处理非常大的 sql 查询或 sql 查询数据。这也可能意味着您发现这更容易作为每天运行并在一天结束时自行关闭的计划任务或按需调用的程序来完成。

最后的猜测是,你正在做的事情会导致你的连接对象仍然以某种方式可以被你的程序访问。事件处理程序是此类错误的常见来源,尽管我会发现在您的连接上使用事件处理程序很奇怪,特别是因为这不是您的示例的一部分。

1我想我可以设计一个场景来实现这一点。一种简单的方法是构建一个对象,假定该类型的所有对象都有一个全局集合......对象在构建时将自己添加到集合中,并在处置时将自己移除。这样,在处理之前无法收集对象,因为在此之前它仍然可以访问......但这将是一个非常有缺陷的程序设计。

于 2012-12-03T02:34:43.050 回答
1

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

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

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

汉斯,你也是对的。发布问题后,我更改了上面代码中的库以连接到 MySQL。

旧库(在示例中):

系统.数据.Odbc

新图书馆:

系统数据
Microsoft.Data.Odbc

有了新的,探查器呈现了一条平线,没有对代码进行任何进一步的更改,这是我一直在关注的。所以我的结论和你的一样,那就是旧的可能存在一些内部错误导致这种事情发生,这使他们成为真正的“麻烦制造者”。

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

“新”库不允许 MARS。每个会话/连接我只能创建一个命令(即执行查询)。如果我需要执行另一个,我需要关闭前一个记录集(这实际上是不可能的,因为我正在迭代它),然后进行新的查询。

我必须在这两个问题之间找到平衡。因此,由于内存问题,我最终使用了“新库”,并且我重新编码了我的应用程序以不共享连接(因此每个过程将在需要时创建一个新的),以及减少应用程序可以连接的数量同时不要耗尽连接池。

该解决方案远非理想,因为它在应用程序中引入了虚假逻辑(理想的情况是迁移到 SQL 服务器),但它给了我更好的结果并且应用程序更加稳定,至少在早期阶段新版本。

再次感谢您的建议,我希望您也会发现地雷也很有用。

干杯。

尼科

于 2012-12-09T14:15:59.910 回答