C# DataAdapter.Fill上的System.Data.Odbc内存管理和AccessViolationException异常

C# DataAdapter.Fill上的System.Data.Odbc内存管理和AccessViolationException异常,c#,.net,access-violation,dataadapter,C#,.net,Access Violation,Dataadapter,我已经用C#.NET framework 4.5编写了一个服务器应用程序。它每隔30-120秒进入数据库,使用System.data.Odbc命名空间中定义的工具获取必要数据的更新。数据存储在我创建的容器类列表中,该容器类用于存储所有必需的数据。该列表是Xml序列化的,并通过TCP发送到连接的客户端。应用程序通常运行5-6小时,然后被AccessViolationException急停。调用OdbcDataAdapter.Fill()时似乎会引发异常。接着,我一直面临的主要问题是,我的数据收集功

我已经用C#.NET framework 4.5编写了一个服务器应用程序。它每隔30-120秒进入数据库,使用System.data.Odbc命名空间中定义的工具获取必要数据的更新。数据存储在我创建的容器类列表中,该容器类用于存储所有必需的数据。该列表是Xml序列化的,并通过TCP发送到连接的客户端。应用程序通常运行5-6小时,然后被AccessViolationException急停。调用OdbcDataAdapter.Fill()时似乎会引发异常。接着,我一直面临的主要问题是,我的数据收集功能导致应用程序的工作集在每次运行时增加大约4 MB,有时它可以在2分钟内运行3次。数据收集过程非常繁忙,但简而言之就是如此

编辑:我最近使用Scitech Memory Profiler分析了应用程序。事实证明,托管字节数只是暂时增长到1MB左右,然后重置到400KB左右。因此,与我最初的想法相反,我的应用程序中可能没有内存泄漏。然而,进程工作集的大小仍在迅速增加,对此我无法解释。我已经在AccessViolationException上设置了一个断点,希望使用此探查器拍摄内存快照能够揭示原因

首先,这里是我的容器类的外观

public class Alert
{
    public enum OrderType
    {
        ...
    }
    public enum AlertType
    {
        ...
    }
    //All the members of these structs are managed
    public struct Unreleased
    {
        ...
    }
    public struct AlloData
    {
        ...
    }
    public AlloData AllocationData { get; set; }
    public Unreleased UnreleasedData { get; set; }
    public string OrderNO { get; set; }
    public string PickNO { get; set; }
    public OrderType Type { get; set; }
    public AlertType Code { get; set; }
    public string Customer { get; set; }
    public Int64 ElapsedSeconds { get; set; }
    public BackOrderData BackOrderData { get; set; }
}
下面是获取数据的函数

    private static XmlSerializer XMLS = new XmlSerializer(typeof(List<Alert>))
    [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions()]
    public Byte[] getAlerts()
    {
        try
        {  
            OdbcConnection Conn = new OdbcConnection(SB.ConnectionString);
            Conn.Open();
            List<Alert> Alerts = new List<Alert>();
            String Query = "...";
            var cmd = new OdbcCommand(Query, Conn);
            int cnt = Convert.ToInt32(cmd.ExecuteScalar());
        //After I'm done with OdbcCommand/Data Adapter instances I dispose and null them
            cmd.Dispose(); cmd = null;
            Query = "...";
            cmd = new OdbcCommand(Query, Conn); 
            cnt += Convert.ToInt32(cmd.ExecuteScalar());
            cmd.Dispose(); cmd = null;
            ... 
            var DT = new DataTable();
            var DA = new OdbcDataAdapter(Query, Conn);
            DA.Fill(DT);
            DA.Dispose(); DA = null;
            ... //A ton of data collection etc..
            foreach (DataRow DR in DT.Rows)
            {
                var Alert = new Alert();
                ... //Data Collection
                Alerts.Add(Alert);
            }
            DT.Dispose(); DT = null;
            ... //More
            byte[] bytes = null; 
            MemoryStream MS = new MemoryStream();
            XMLS.Serialize(MS, Alerts);
            bytes = MS.ToArray();
            MS.Dispose(); MS = null;
            Alerts = null; 
            Conn.Close();
            Conn.Dispose();             
            Conn = null;
            return bytes;
         catch(Exception ex) {
             ...
         }
     }   
我认为,如果有任何东西有可能导致
AccessViolationException
异常,那就是这种行为,然而,这是我发现的防止
OutOfMemoryException
在运行一小时后发生的唯一方法

例外情况如下:

System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
at System.Data.Common.UnsafeNativeMethods.SQLExecDirectW(OdbcStatementHandle StatementHandle, String StatementText, Int32 TextLength)
at System.Data.Odbc.OdbcStatementHandle.ExecuteDirect(String commandText)
at System.Data.Odbc.OdbcCommand.ExecuteReaderObject(CommandBehavior behavior, String method, Boolean needReader, Object[] methodArguments, SQL_API odbcApiMethod)
at System.Data.Odbc.OdbcCommand.ExecuteReaderObject(CommandBehavior behavior, String method, Boolean needReader)
at System.Data.Odbc.OdbcCommand.ExecuteReader(CommandBehavior behavior)
at System.Data.Odbc.OdbcCommand.ExecuteDbDataReader(CommandBehavior behavior)
at System.Data.Common.DbCommand.System.Data.IDbCommand.ExecuteReader(CommandBehavior behavior)
at System.Data.Common.DbDataAdapter.FillInternal(DataSet dataset, DataTable[] datatables, Int32 startRecord, Int32 maxRecords, String srcTable, IDbCommand command, CommandBehavior behavior)
at System.Data.Common.DbDataAdapter.Fill(DataTable[] dataTables, Int32 startRecord, Int32 maxRecords, IDbCommand command, CommandBehavior behavior)
at System.Data.Common.DbDataAdapter.Fill(DataTable dataTable)
at PickWatchServer.DBSearch.getAlerts()

在此问题上的任何建议/帮助都将不胜感激,因此请提前感谢!如果您想查看其他代码/数据,请询问。

可能是因为您在大型对象堆上遇到了碎片问题(关于SO的其他有趣问题:,)。这个问题是已知的,在.NET fx 4.5中提供了一些解决方法(例如)

为了减少影响(或完全消除影响),通常会使用预分配的对象池,特别是在多线程应用程序中。预先分配足够大小的数组,并保留一个递增的大小变量,以有效跟踪数组中已使用部分的大小

如果您的代码是单线程的,我建议您将
列表
替换为
Alert[]
,并使其具有足够的大小,以保持静态。添加
int
变量以保持数组中已使用元素的计数,例如

//Somewhere in the class
static Alert[] alertList = new Alert[4096];
static int alertListSize = 0;

//In your method
alertListSize = 0;
foreach (DataRow DR in DT.Rows)
{
    var Alert = new Alert();
    ... //Data Collection
    //REMOVED Alerts.Add(Alert);
    alertList[alertListSize++] = Alert;
}
请注意,您将无法在该数组上使用XmlSerializer,因为它将序列化所有元素(包括任何
null
元素),因此必须使用类似XmlWriter的东西。因为您恰好只编写了一系列元素,所以转换应该不难(尽管超出了这个答案的范围)

您可以对固定大小的
字节[]
执行相同操作,以便在MemoryStream中使用,您可以将其传递给构造函数。记住在初始化后使用
Seek()
将MemoryStream倒回到缓冲区的开头,这样XmlSerializer将从开头覆盖缓冲区中的任何内容

另外,为了获得进一步的帮助,请使用
语句将代码转换为
,这会让大家更清楚地理解(并确保调用
Dispose()
方法时没有错误)


LOH碎片问题是处理序列化(大对象)的长时间运行的C#程序中显然无法解释的内存不足错误的可能原因之一,但可能不是唯一的原因;可能需要进一步研究。

一件坏事,嵌套函数:
Convert.ToInt32(cmd.ExecuteScalar())ExecuteScalar返回NULL时会发生什么?另外,我会使用
using
子句或
finally
块来释放资源。就嵌套函数而言,我在执行SELECT COUNT(*)查询时会使用这段代码,该查询总是返回一个整数>=0,但我肯定会检查我的代码,以查找危险的例子。我尝试了
使用
块,但没有效果。说using块只是为您调用dispose是正确的,还是说还有更多?你是对的。当我在C#中遇到内存问题时,我学到的另一件事是尽量避免手动调用GC,或摆弄Windows API。您可能看到的问题可能与LOH碎片有关,因为您使用的是字节数组,正如我所做的那样。(例如,有关与此问题相关的另一个问题,请参见)。我使用固定数量的预先分配字节数组来解决这个问题,这些字节数组在循环中使用。通过序列化得到的字节数组往往只有大约18-30KB。然而,我的列表对象和流对象可以找到它们通向LOH的路径。你介意解释一下你是如何解决字节数组旋转的问题的吗。我当然想尝试为我的代码实现一个类似的解决方案。@Alexmazariol这听起来可信吗?
//Somewhere in the class
static Alert[] alertList = new Alert[4096];
static int alertListSize = 0;

//In your method
alertListSize = 0;
foreach (DataRow DR in DT.Rows)
{
    var Alert = new Alert();
    ... //Data Collection
    //REMOVED Alerts.Add(Alert);
    alertList[alertListSize++] = Alert;
}