首先,先感谢您的帮助。
我决定在像这样的论坛寻求帮助,因为经过几个月的努力工作,我无法找到解决问题的方法。
这可以被描述为'为什么即使在GC被强制启动时,GC也不会释放在VB.net中创建的对象?“
请考虑以下代码。显然我的项目要复杂得多,但我能够解决问题:
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,可下载的试用版here)。有一个名为“调查内存泄漏”的部分。当我在“实时”标签上看到这个时,我感到非常惊讶。如果我是对的,这个图形告诉我代码上创建的对象都没有实际发布:
当我看到其他屏幕时,惊喜甚至更大。根据这个,所有未处理的对象都是 System.Transactions 类型,我假设它是在.Net库中内部管理的,因为我没有在我的代码上创建这种类型的任何对象。这是否意味着VB.net标准库上有一个错误???:
请注意,在我的代码中,我没有执行任何查询。如果我这样做, ODBCDataReader 对象也不会被释放,即使我调用 .Close()方法(令人惊讶的是,这种类型的未释放对象的数量)与 System.Transactions )
类型的未发布对象完全相同另一个重要的事情是声明 GC.Collect()。内存分析器使用它来刷新要显示的信息。如果您从代码中删除它,分析器将无法正确更新实时图表,给您错误的印象,即一切都是正确的。
最后,如果你省略 connex.Open()语句,屏幕截图#1将呈现一条扁平线(这意味着所有创建的对象都已成功发布),但不幸的是,我们可以如果尚未打开连接,则不对数据库进行任何查询。
有人可以找到对此的逻辑解释,还有一个有效释放对象的解决方法吗?
谢谢所有人。
尼科
答案 0 :(得分:5)
Dispose与无与垃圾收集有关。垃圾收集专门用于托管资源(内存)。 Dispose根本不影响内存,仅与非托管资源(数据库连接,文件句柄,gdi资源,套接字......任何不内存)相关。两者之间的唯一关系与最终确定的对象有关,因为许多对象经常被实现,因此处理它们将抑制最终化并最终确定它们将调用.Dispose ()。明确地处置()一个对象永远不会导致它被收集 1 。
明确地调用垃圾收集器几乎总是一个坏主意。 .Net使用世代垃圾收集器,因此自己调用它的主要作用是你将更长时间保留在内存,因为通过强制收集你可能会更早检查在它们有资格收集之前的项目,它们将它们发送到较不常收集的高阶代。否则这些项目将留在下一代,并且当GC接下来运行它时,它们有资格收集。您现在可能需要为分析器使用GC.Collect(),但您应该尝试将其删除以用于生产代码。
你提到你的应用程序在崩溃之前运行了两天,并且没有对你的实际生产代码进行分析(或显示结果),所以我也认为分析器在某种程度上误导了你。您已将代码减少到产生 a 内存泄漏的内容,但我不确定您看到 内存泄漏在生产中。这部分是因为重现错误的时间不同,但它也是“本能”#34;。我提到这一点,因为根据你的探查器结果,我提出的一些建议可能没有意义。除此之外,我不确定你遗失的记忆是什么,但我可以做一些猜测。
第一个猜测是你的真实代码有try / catch块。抛出一个异常...也许不是每个连接都有,但有时候。当发生这种情况时,catch块允许您的程序继续运行,但是您跳过connex.Dispose()
行,因此保持打开的连接。这些连接最终将为数据库创建拒绝服务的情况,这可以通过多种方式表现出来。这里的修正是为了确保你总是使用finally块来处理你.Dispose()。无论您目前是否有try / catch块都是如此,而且重要的是我要说到目前为止您发布的代码基本上错误:您需要试试/终于。通过using
块有一个快捷方式。
下一个猜测是你的一些实际命令最终会相当大,可能涉及大字符串或图像(byte [])数据。在这种情况下,项目最终会产生一个称为大对象堆(LOH)的特殊垃圾收集器生成。 LOH很少收集,几乎从未压实过。将压缩视为类似于对磁盘驱动器进行碎片整理时发生的情况。如果您有物品进入LOH,您最终可能会释放(收集)物理内存本身,但您的进程中的地址空间(通常限制为2GB)是没有被释放(压缩)。您的内存地址空间中有漏洞无法回收。物理RAM可供您的系统用于其他进程,但随着时间的推移,这仍然会导致您看到的同一种OutOfMemory异常。大多数情况下这并不重要:大多数.Net程序是短暂的面向用户的应用程序,或ASP.Net应用程序,其中整个线程可以在页面提供后被拆除。既然您正在构建类似于应该运行数天的服务,那么您必须更加小心。修复可能涉及显着重新处理一些代码,以避免创建大对象。这可能意味着反复使用单个或一小组字节数组,或者使用流技术而不是字符串连接或字符串构建器来处理非常大的sql查询或sql查询数据。这也可能意味着您发现这更容易作为每日运行并在一天结束时自行关闭的计划任务,或者按需调用的程序。
最后一个猜测是,您正在做的事情会导致您的连接对象仍然以某种方式可由您的程序访问。事件处理程序是此类错误的常见来源,但我发现在您的连接上使用事件处理程序很奇怪,尤其是因为这不是您示例的一部分。
1 我想我可以设想一个能够实现这一目标的方案。一种简单的方法是构建一个对象,假定该类型的所有对象都有一个全局集合......对象在构造时将自己添加到集合中,并在处置时将其自行删除。通过这种方式,在处理之前无法收集对象,因为在此之前它仍然可以访问......但这将是一个非常有缺陷的程序设计。
答案 1 :(得分:1)
感谢所有人提供的非常有帮助的答案。
乔尔,你是对的。这段代码产生“泄漏”,这与我在实际项目中的“泄漏”问题不一定相同,尽管它们会重现相同的症状,即未发布的对象数量不断增长(并最终耗尽内存) )关于上面提到的代码。所以我想知道它有什么问题,因为一切似乎都被正确编码了。我不明白他们为什么不被处置/收集。但根据分析器,它们仍然在内存中,最终会阻止创建新对象。
你对我的“真实”项目的一个猜测击中了头部。我意识到我的“捕获”块并没有要求对象处理,现在已经修复了。感谢您宝贵的建议。但是,我在上面的示例中的代码中实现了“using”子句,并没有真正解决问题。
汉斯,你也是对的。在发布问题之后,我已经更改了上面代码中的库以建立与MySQL的连接。
旧库(在示例中):
System.Data.Odbc
新库:
System.Data Microsoft.Data.Odbc
与新的一样,分析器渲染了一条扁平线,而不对代码进行任何进一步的更改,这是我一直在关注的。所以我的结论与你的相同,也就是旧的那些内部错误使得事情发生,这使得他们成为真正的“麻烦制造者”。
现在我记得我最初在我的项目中使用了新的( System.Data 和 Microsoft.Data.Odbc ),但我很快换了旧的( System.Data.Odbc )因为新的不允许打开多个活动记录集(MARS)。我的应用程序对MySQL数据库进行了大量查询,但不幸的是,连接数量有限。所以我最初以这样的方式实现我的真实代码,它只做了几个连接,但它们是在代码中共享的(将函数之间的连接作为参数传递)。这很好,因为(例如)我需要检索记录集(比如说客户端),并同时进行大量检查(例如,客户端至少有一张发票,客户端有重复的电子邮件地址等等,这涉及很多侧面查询)。对于“旧”库,同一连接允许创建多个命令并执行不同的查询。
'新'图书馆不允许使用MARS。我只能为每个会话/连接创建一个命令(即执行查询)。如果我需要执行另一个,我需要关闭以前的记录集(这实际上不可能,因为我在迭代它),然后进行新的查询。
我必须找到两个问题之间的平衡点。所以我最终因为内存问题而使用'新库',并且我重新编写了我的应用程序以不共享连接(因此每个过程将在需要时创建一个新的),以及减少应用程序可以连接的数量同时不要耗尽连接池。
该解决方案远非理想,因为它在应用程序中引入了虚假逻辑(理想的情况是迁移到SQL服务器),但它给了我更好的结果,应用程序更稳定,至少在新版本的早期阶段。
再次感谢您的建议,我希望您也能找到地雷。
干杯。
尼科