VB.net 垃圾收集器不释放对象
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/13671551/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
VB.net Garbage collector not releasing objects
提问by Nicolas
First of all, thanks in advance for your help.
首先,提前感谢您的帮助。
I've decided to ask for help in forums like this one because after several months of hard working, I couldn't find a solution for my problem.
我决定在这样的论坛上寻求帮助,因为经过几个月的努力,我找不到解决问题的方法。
This can be described as 'Why an object created in VB.net isn't released by the GC when it is disposed even when the GC was forced to be launched?"
这可以描述为'为什么在 VB.net 中创建的对象即使在 GC 被迫启动的情况下被释放时也不会被 GC 释放?”
Please consider the following piece of code. Obviously my project is much more complex, but I was able to isolate the problem:
请考虑以下代码。显然我的项目要复杂得多,但我能够隔离问题:
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
This simulates a multithreaded application making connections to a MySQL database. As you can see, the connection is created as a new object, then released. Finally, the GC was forced to be launched. I've seen this algorithm in several forums but also in the MSDN online help, so as far as I am concerned, I am not doing anything wrong.
这模拟了一个连接到 MySQL 数据库的多线程应用程序。如您所见,连接被创建为一个新对象,然后被释放。最后,GC被迫启动。我已经在几个论坛和 MSDN 在线帮助中看到过这个算法,所以就我而言,我没有做错任何事情。
The problem begins when the application is launched. The object created is disposed within the code, but after a while, the availiable memory is exhausted and the application crashes.
当应用程序启动时,问题就开始了。创建的对象在代码中被处理,但一段时间后,可用内存耗尽,应用程序崩溃。
Of course, this problem is hard to see in this little version, but on the real project, the application runs out of memory very quickly (due to the amount of connections made over the time) and as result, the uptime is only two days. Then I need to restart the application again.
当然,这个问题在这个小版本中是很难看到的,但是在实际项目中,应用程序很快就会耗尽内存(由于连接数随着时间的推移而发生),因此正常运行时间只有两天. 然后我需要再次重新启动应用程序。
I installed a memory profiler on my machine (Scitech .Net Memory profiler 4.5, downloadable trial version here). There is a section called 'Investigate memory leaks'. I was absolutely astonished when I saw this on the 'Real Time' tab. If I am correct, this graphic is telling me that none of the objects created on the code have been actually released:
我在我的机器上安装了内存分析器(Scitech .Net Memory profiler 4.5,可在此处下载试用版 )。有一个部分称为“调查内存泄漏”。当我在“实时”选项卡上看到这个时,我感到非常惊讶。如果我是对的,这个图形告诉我在代码上创建的任何对象都没有被实际释放:


The surprise was even bigger when I saw this other screen. According to this, all undisposed objects are System.Transactionstype, which I assume are internally managed within the .Net libraries as I am not creating any object of this type on my code. Does it mean there is a bug on the VB.net Standard libraries???:
当我看到另一个屏幕时,惊喜更大。据此,所有未处理的对象都是System.Transactions类型,我假设它们是在 .Net 库中内部管理的,因为我没有在我的代码中创建任何这种类型的对象。这是否意味着 VB.net 标准库存在错误???:


Please notice that in my code, I am not executing any query. If I do, the ODBCDataReaderobject won't be released either, even if I call the .Close()method (surprisingly enough, the number of unreleased objects of this type is exactly the same as the unreleased objects of type System.Transactions)
请注意,在我的代码中,我没有执行任何查询。如果我这样做,ODBCDataReader对象也不会被释放,即使我调用.Close()方法(令人惊讶的是,这种类型的未释放对象的数量与System.Transactions类型的未释放对象的数量完全相同)
Another important thing is the statement GC.Collect(). This is used by the memory profiler to refresh the information to be displayed. If you remove it from the code, the profiler wont' update the real time diagram properly, giving you the false impression that everything is correct.
另一个重要的事情是语句GC.Collect()。内存分析器使用它来刷新要显示的信息。如果您将其从代码中删除,分析器将不会正确更新实时图表,从而给您错误的印象,即一切都是正确的。
Finally, if you ommit the connex.Open()statement, the screenshot #1 will render a flat line (that means all the objects created have been successfully released), but unfortunatelly, we can't make any query against the database if the connection hasn't been opened.
最后,如果省略connex.Open()语句,截图 #1 将呈现一条扁平线(这意味着所有创建的对象都已成功释放),但不幸的是,如果出现以下情况,我们无法对数据库进行任何查询连接尚未打开。
Can someone find a logical explanation to this and also, a workaround for effectively releasing the objects?
有人可以找到对此的合乎逻辑的解释,以及有效释放对象的解决方法吗?
Thank you all folks.
谢谢大家。
Nico
妮可
回答by Joel Coehoorn
Dispose has nothingto do with garbage collection. Garbage collection is exclusively about managedresources (memory). Dispose has no bearing on memory at all, and is only relevant for unmanaged resources(database connections, file handles, gdi resource, sockets... anything notmemory). The onlyrelationship between the two has to do with how an object is finalized, because many objects are often implemented such that disposing them will suppress finalization and finalizing them will call .Dispose(). Explicitly Disposing() an object will nevercause it to be collected1.
Dispose与垃圾收集无关。垃圾收集仅与托管资源(内存)有关。Dispose 与内存完全无关,仅与非托管资源相关(数据库连接、文件句柄、gdi 资源、套接字......任何非内存)。在只有两个之间的关系有一个对象是怎么做的敲定,因为许多对象常常实现,使得它们配置将抑制定稿,并最后确定他们会打电话.Dispose()。明确地 Disposing() 一个对象永远不会导致它被收集1。
Explicitly calling the garbage collector is almost always a bad idea. .Net uses a generational garbage collector, and so the main effect of calling it yourself is that you'll hold onto memory longer, because by forcing the collection earlier you're likely to check the items before they are eligible for collection at all, which sends them into a higher-order generation that is collected less often. These items otherwise would have stayed in the lower generation and been eligible for collection when the GC next ran on it's own. You may need to use GC.Collect() now for the profiler, but you should try to remove it for your production code.
显式调用垃圾收集器几乎总是一个坏主意。.Net 使用分代垃圾收集器,因此自己调用它的主要效果是您将保留更长时间的内存,因为通过提前强制收集,您可能会在项目完全符合收集条件之前对其进行检查,将它们发送到收集频率较低的高阶生成中。否则,这些项目将留在较低的代中,并在 GC 下一次自行运行时有资格收集。您现在可能需要将 GC.Collect() 用于分析器,但您应该尝试为您的生产代码删除它。
You mention your app runs for two days before crashing, and are not profiling (or showing results for) your actual production code, so I also think the profiler is in part misleading you here. You've pared down the code to something that produced amemory leak, but I'm not sure it's thememory leak you are seeing in production. This is partly because of the difference in time to reproduce the error, but it's also "instinct". I mention that because some of what I'm going to suggest might not make sense immediately in light of your profiler results. That out of the way, I don't know for sure what is going on with your lost memory, but I can make a few guesses.
你提到你的应用程序在崩溃前运行了两天,并且没有分析(或显示结果)你的实际生产代码,所以我也认为分析器在一定程度上误导了你。你相比下来的代码是炮制出来的一个内存泄漏,但我不知道它的内存泄漏在生产中所看到的。这部分是因为重现错误的时间不同,但这也是“本能”。我之所以提到这一点,是因为根据您的分析器结果,我要提出的某些建议可能不会立即有意义。顺便说一下,我不确定你失去的记忆是怎么回事,但我可以做一些猜测。
The first guess is that your real code has try/catch block. An exception is thrown... perhaps not on every connection, but sometimes. When that happens, the catch block allows your program to keep running, but you skipped over the connex.Dispose()line, and therefore leave open connections hanging around. These connections will eventually create a denial of service situation for the database, which can manifest itself in a number of ways. The correction here is to make sure you always use a finally block for anything you .Dispose(). This is true whether or not you currently have a try/catch block, and it's important enough that I would say the code you've posted so far is fundamentally wrong: you need a try/finally. There is a shortcut for this, via a usingblock.
第一个猜测是你的真实代码有 try/catch 块。抛出异常......也许不是在每个连接上,但有时。发生这种情况时,catch 块允许您的程序继续运行,但您跳过了该connex.Dispose()行,因此留下了悬而未决的开放连接。这些连接最终会造成数据库拒绝服务的情况,这可以通过多种方式表现出来。这里的更正是确保您始终对 .Dispose() 的任何内容使用 finally 块。无论您当前是否有 try/catch 块,这都是正确的,而且重要的是我会说您到目前为止发布的代码从根本上是错误的:您需要 try/finally。有一个快捷方式,通过一个using块。
The next guess is that some of your real commands end up fairly large, possibly with large strings or image (byte[]) data involved. In this case, items end up on a special garbage collector generation called the Large Object Heap (LOH). The LOH is rarely collected, and almost never compacted. Think of compaction as analogous to what happens when you defrag a hard drive. If you have items going to the LOH, you can end up in a situation where the physical memory itself is freed (collected), but the address spacewithin your process (you are normally limited to 2GB) is not freed (compacted). You have holes in your memory address space that will not be reclaimed. The physical RAM is available to your system for other processes, but over time this still results in the same kind of OutOfMemory exception you're seeing. Most of the time this doesn't matter: most .Net programs are short-lived user-facing apps, or ASP.Net apps where the entire thread can be torn down after a page is served. Since you're building something like a service that should run for days, you have to be more careful. The fix may involve significantly re-working some code, to avoid creating the large objects at all. That may mean re-using a single or small set of byte arrays over and over, or using streaming techniques instead of string concatenation or string builders for very large sql queries or sql query data. It may also mean you find this easier to do as a scheduled task that runs daily and shuts itself down at the end of the day, or a program that is invoked on demand.
下一个猜测是,您的一些实际命令最终会相当大,可能涉及大字符串或图像 (byte[]) 数据。在这种情况下,项目最终会出现在称为大对象堆 (LOH) 的特殊垃圾收集器代中。LOH 很少被收集,几乎从不压实。将压缩视为类似于对硬盘驱动器进行碎片整理时发生的情况。如果您有项目进入 LOH,您最终可能会遇到这样的情况:物理内存本身被释放(收集),但地址空间在您的进程中(您通常限制为 2GB)不会被释放(压缩)。您的内存地址空间中有无法回收的漏洞。物理 RAM 可用于您的系统用于其他进程,但随着时间的推移,这仍然会导致您看到的相同类型的 OutOfMemory 异常。大多数时候这无关紧要:大多数 .Net 程序都是短期的面向用户的应用程序,或者是 ASP.Net 应用程序,在这些应用程序中,可以在提供页面后拆除整个线程。由于您正在构建类似应该运行数天的服务之类的东西,因此您必须更加小心。修复可能涉及大量重新编写一些代码,以避免创建大对象。这可能意味着一遍又一遍地重复使用单个或一小组字节数组,或者使用流技术而不是字符串连接或字符串构建器来处理非常大的 sql 查询或 sql 查询数据。这也可能意味着您发现这更容易作为每天运行并在一天结束时自行关闭的计划任务或按需调用的程序来执行。
A final guess is that something you are doing results in your connection objects still being in some way reachableby your program. Event handlers are a common source of mistakes of this sort, though I would find it strange to have event handlers on your connections, especially as this is not part of your example.
最后的猜测是您正在做的事情导致您的程序仍然可以以某种方式访问您的连接对象。事件处理程序是此类错误的常见来源,尽管我会发现在您的连接上使用事件处理程序很奇怪,特别是因为这不是您的示例的一部分。
1I suppose I could contrive a scenario that would make this happen. A simple way would be to build an object assumes a global collection for all objects of that type... the objects add themselves to the collection at construction and remove themselves at disposal. In this way, the object could not be collected before disposal, because before that point it would still be reachable... but that would be a very flawed program design.
1我想我可以设计一个场景来实现这一点。一种简单的方法是构建一个对象,假定该类型的所有对象都有一个全局集合……这些对象在构造时将自己添加到集合中,并在处理时删除自己。这样,对象在处置之前就无法收集,因为在那之前它仍然可以访问……但这将是一个非常有缺陷的程序设计。
回答by Nicolas
Thank you all guys for your very helpful answers.
感谢大家提供非常有帮助的答案。
Joel, you're right. This code produces 'a leak' which is not necesarily the same as 'the leak' problem I have on my real project, though they reproduce the same symptoms, that is, the number of unreleased objects keep growing (and eventually will exhaust the memory) on the code mentioned above. So I wonder what's wrong with it as everything seems to be properly coded. I don't understand why they are not disposed/collected. But according to the profiler, they are still in memory and eventually will prevent to create new objects.
乔尔,你说得对。这段代码产生了“泄漏”,这与我在实际项目中遇到的“泄漏”问题不一定相同,尽管它们重现了相同的症状,即未释放对象的数量不断增长(最终将耗尽内存) 上面提到的代码。所以我想知道它有什么问题,因为一切似乎都被正确编码。我不明白为什么不处理/收集它们。但是根据分析器,它们仍然在内存中,最终将阻止创建新对象。
One of your guesses about my 'real' project hit the nail on the head. I've realized that my 'catch' blocks didn't call for object disposal, and this has been now fixed. Thanks for your valuable suggestion. However, I implemented the 'using' clause in the code in my example above and didn't actually fix the problem.
你对我的“真实”项目的一个猜测一针见血。我已经意识到我的“catch”块不需要对象处理,现在已经解决了这个问题。感谢您的宝贵建议。但是,我在上面示例中的代码中实现了“using”子句,实际上并没有解决问题。
Hans, you are also right. After posting the question, I've changed the libraries on the code above to make connections to MySQL.
汉斯,你也说得对。发布问题后,我更改了上面代码中的库以连接到 MySQL。
The old libraries (in the example):
旧库(在示例中):
System.Data.Odbc
The new libraries:
新图书馆:
System.Data Microsoft.Data.Odbc
Whith the new ones, the profiler rendered a flat line, whithout any further changes on the code, which it was what I've been looking after. So my conclussion is the same as yours, that is there may be some internal error in the old ones that makes that thing to happen, which makes them a real 'troublemaker'.
对于新的,分析器呈现了一条平坦的线,代码没有任何进一步的变化,这就是我一直在关注的。所以我的结论和你的一样,那就是旧的可能存在一些内部错误导致这件事发生,这使他们成为真正的“麻烦制造者”。
Now I remember that I originally used the new ones on my project (the System.Dataand Microsoft.Data.Odbc) but I soon changed for the old ones (the System.Data.Odbc) because the new ones doesn't allow Multiple Active Recordsets (MARS) opened. My application makes a huge amount of queries against the MySQL database, but unfortunately, the number of connections are limited. So I initially implemented my real code in such a way that it made only a few connections, but they were shared accross the code (passing the connection between functions as parameter). This was great because (for example) I needed to retrieve a recordset (let's say clients), and make a lot of checks at the same time (example, the client has at least one invoice, the client has a duplicated email address, etc, which involves a lot of side queries). Whith the 'old' libraries, the same connection allowed to create multiple commands and execute different queries.
现在我记得我最初在我的项目中使用了新的(System.Data和Microsoft.Data.Odbc)但我很快就改变了旧的(System.Data.Odbc) 因为新的不允许打开多个活动记录集 (MARS)。我的应用程序对 MySQL 数据库进行了大量查询,但不幸的是,连接数有限。所以我最初以这样一种方式实现了我的真实代码,它只建立了几个连接,但它们在代码中共享(将函数之间的连接作为参数传递)。这很棒,因为(例如)我需要检索记录集(假设客户),并同时进行大量检查(例如,客户至少有一张发票,客户有重复的电子邮件地址等,这涉及很多侧面查询)。对于“旧”库,相同的连接允许创建多个命令并执行不同的查询。
The 'new' libraries don't allow MARS. I can only create one command (that is, to execute a query) per session/connection. If I need to execute another one, I need to close the previous recordset (which isn't actually possible as I am iterating over it), and then to make the new query.
“新”库不允许使用 MARS。我只能为每个会话/连接创建一个命令(即执行查询)。如果我需要执行另一个,我需要关闭前一个记录集(这实际上是不可能的,因为我正在迭代它),然后进行新的查询。
I had to find the balance between both problems. So I end up using the 'new libraries' because of the memory problems, and I recoded my application to not share the connections (so each procedure will create a new one when needed), as well as reducing the number of connections the application can do at the same time to not exhaust the connection pool.
我必须在这两个问题之间找到平衡。因此,由于内存问题,我最终使用了“新库”,并且我重新编码了我的应用程序以不共享连接(因此每个过程将在需要时创建一个新的),并减少应用程序可以连接的数量同时不要耗尽连接池。
The solution is far to ideal as it introduces spurious logic on the application (the ideal case scenario would be to migrate to SQL server), but it is giving me better results and the application is being more stable, at least in the early stages of the new version.
该解决方案远不理想,因为它在应用程序中引入了虚假逻辑(理想的情况是迁移到 SQL 服务器),但它给了我更好的结果并且应用程序更加稳定,至少在早期阶段新版本。
Thanks again for your suggestions, I hope you will find mines usefult too.
再次感谢您的建议,我希望您也会发现地雷有用。
Cheers.
干杯。
Nico
妮可

