2025年3月31日 星期一

[研究]ASP.NET,System.OutOfMemoryException 問題改善

[研究]ASP.NET,System.OutOfMemoryException 問題改善

2025-03-28x

環境:Visual Studio 2022 + ASP.NET + WebForm + Web Application + C# + SQL Server 2019 + SQL Server Management Studio (SSMS) 19

ASP.NET, C#, WebForm, 下面程式在正式機出現 System.OutOfMemoryException,在測試機正常可以執行完,工作管理員檢查正式機還有 8 GB RAM,請教比較可能出現 記憶體用盡的地方和原因 ?

		try
            {
                Response.Clear();
                Response.Buffer = true;
                Response.Charset = "";

                #region === Export ===

                using (MemoryStream myMemoryStream = new MemoryStream())
                {
                    string dataText = "";

                    var wb = new XLWorkbook();
                    var ws = wb.Worksheets.Add("工作表1");

                    string[] columnNameArray = { "流水號", "產品ID"};
                    for (int i = 0; i < columnNameArray.Count(); i++)
                    {
                        ws.Cell(1, i + 1).Value = columnNameArray[i];
                    }
                    string queryString = @"SELECT * FROM Table1";
                    using (SqlConnection connection = new SqlConnection(
                        WebConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString))
                    {
                        SqlCommand command = new SqlCommand(queryString, connection);
                        connection.Open();
                        using (SqlDataReader reader = command.ExecuteReader())
                        {
                            int rowIndex = 1;   // 1 是標題列
                            while (reader.Read())
                            {
                                rowIndex++;
                                for (int i = 0; i < reader.FieldCount; i++)
                                {
                                    dataText = reader[i].ToString();

                                    dataText = dataText.Replace("\r", "").Replace("\n", "").Replace("\t", "").Replace(",", ",").Replace("'", "′").Replace(@"""", "”");
                                    dataText = dataText.Replace("\a", "").Replace("\b", "").Replace("\t", "").Replace("\v", "").Replace("\f", "").Replace("\\", "");

                                    ws.Cell(rowIndex, i + 1).Value = dataText;
                                }
                            }
                        }

                        Response.Clear();
                        Response.Buffer = true;
                        Response.ContentEncoding = Encoding.GetEncoding(950);  
                        Response.AddHeader("Content-Disposition", "attachment;filename=\"" + outFileName);
                        Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

                        wb.SaveAs(myMemoryStream);
                        Response.BinaryWrite(myMemoryStream.ToArray());
                        Response.Flush();
                        Response.Close();
                        wb.SaveAs(dirFileName);                 
                        Response.End();

                        Label_MSG1.ForeColor = System.Drawing.Color.Green;
                        Label_MSG1.Text = "匯出成功。";
                    }
                }
                #endregion Export ODS
            }
            catch (Exception)
            {
                throw;
            }
        }

********************************************************************************

ChatGPT 說:

你的程式在正式機出現 System.OutOfMemoryException,但測試機可以正常執行,這可能與正式機的記憶體使用情況、資料量大小或 GC (Garbage Collection) 行為不同有關。以下是幾個可能的記憶體耗盡點與原因:

1. 讀取大量資料導致記憶體爆掉

你的 SQL 查詢 SELECT * FROM Table1 沒有限制返回的行數,這可能會導致:

SqlDataReader 讀取過多資料,特別是 Table1 資料量很大時。

XLWorkbook (wb) 物件持有過多資料,佔用大量記憶體。

解決方案:限制查詢結果

string queryString = @"SELECT TOP 10000 * FROM Table1"; // 減少資料量   

改用分批處理 例如,使用 OFFSET + FETCH NEXT 來分批載入資料。

********************************************************************************

2. MemoryStream 佔用過大記憶體

你的程式碼嘗試將 Excel 儲存到 MemoryStream (myMemoryStream),再將其轉為 Response.BinaryWrite(myMemoryStream.ToArray());,這會:

將整個 Excel 檔案載入記憶體。

當 myMemoryStream.ToArray() 被呼叫時,會分配一個新的 byte 陣列,可能導致額外的記憶體開銷。

解決方案:避免 ToArray(),改用 WriteTo(Response.OutputStream); 直接將 MemoryStream 內容寫入 Response:

wb.SaveAs(Response.OutputStream);   
Response.Flush();
Response.End();

********************************************************************************

3. Response.End() 可能導致未處理的物件佔用記憶體

Response.End(); 會拋出 ThreadAbortException,這可能導致:

物件 (wb, MemoryStream 等) 無法正常釋放。

GC 無法即時回收記憶體。

🔹 解決方案 改用 CompleteRequest():


Response.Flush();   
HttpContext.Current.ApplicationInstance.CompleteRequest();

********************************************************************************

4. XLWorkbook 物件沒有被正確釋放

EPPlus / ClosedXML (XLWorkbook) 會佔用大量記憶體,因為它在記憶體中維護整個 Excel 物件。

🔹 解決方案

顯式釋放 XLWorkbook

wb.Dispose();   

完整修正建議:

using (XLWorkbook wb = new XLWorkbook())
{
    // Excel 生成過程
    using (MemoryStream myMemoryStream = new MemoryStream())
    {
        wb.SaveAs(myMemoryStream);
        myMemoryStream.Position = 0;

        Response.Clear();
        Response.Buffer = true;
        Response.ContentEncoding = Encoding.GetEncoding(950);
        Response.AddHeader("Content-Disposition", $"attachment; filename={outFileName}");
        Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        myMemoryStream.WriteTo(Response.OutputStream);
        
        Response.Flush();
        HttpContext.Current.ApplicationInstance.CompleteRequest();
    }
} // wb 物件會自動 Dispose()


這樣可以減少記憶體佔用並降低 OutOfMemoryException 發生的機率。

********************************************************************************

總結

正式機發生 OutOfMemoryException 可能的原因:

  • SQL 回傳過大資料集
  • MemoryStream.ToArray() 造成記憶體額外分配
  • Response.End() 造成資源未釋放
  • XLWorkbook 沒有被正確釋放

解法: 

  • 限制 SQL 回傳筆數或改為分批處理
  • 直接寫入 Response.OutputStream,避免 ToArray()
  • 改用 CompleteRequest() 而非 Response.End()
  • 確保 XLWorkbook 使用 using 或 Dispose() 釋放資源

********************************************************************************

完整改過後 CODE

try
{
    Response.Clear();
    Response.Buffer = true;
    Response.Charset = "";

    #region === Export ===
    using (MemoryStream myMemoryStream = new MemoryStream())
    {
        using (XLWorkbook wb = new XLWorkbook())
        {
            var ws = wb.Worksheets.Add("工作表1");

            // 設定欄位標題
            string[] columnNameArray = { "流水號", "產品ID" };
            for (int i = 0; i < columnNameArray.Length; i++)
            {
                ws.Cell(1, i + 1).Value = columnNameArray[i];
            }

            string queryString = @"SELECT * FROM Table1";
            using (SqlConnection connection = new SqlConnection(
                WebConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString))
            {
                using (SqlCommand command = new SqlCommand(queryString, connection))
                {
                    connection.Open();

                    using (SqlDataReader reader = command.ExecuteReader())
                    {
                        int rowIndex = 1; // 1 是標題列
                        while (reader.Read())
                        {
                            rowIndex++;
                            for (int i = 0; i < reader.FieldCount; i++)
                            {
                                string dataText = reader[i]?.ToString() ?? "";
                                // 過濾不必要的特殊字元
                                dataText = dataText.Replace("\r", "")
                                                   .Replace("\n", "")
                                                   .Replace("\t", "")
                                                   .Replace(",", ",")
                                                   .Replace("'", "′")
                                                   .Replace(@"""", "”")
                                                   .Replace("\\", "");

                                ws.Cell(rowIndex, i + 1).Value = dataText;
                            }
                        }
                    }
                }
            }

            // 儲存到 MemoryStream
            wb.SaveAs(myMemoryStream);
        } // wb.Dispose() 自動釋放資源

        myMemoryStream.Position = 0;

        // 設定 HTTP 回應標頭
        Response.ContentEncoding = Encoding.GetEncoding(950);
        Response.AddHeader("Content-Disposition", $"attachment;filename=\"{outFileName}\"");
        Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

        // 直接寫入 Response.OutputStream,避免 ToArray() 佔用記憶體
        myMemoryStream.WriteTo(Response.OutputStream);

        // 確保資料完全發送
        Response.Flush();
        HttpContext.Current.ApplicationInstance.CompleteRequest();
    } // myMemoryStream.Dispose() 自動釋放資源

    Label_MSG1.ForeColor = System.Drawing.Color.Green;
    Label_MSG1.Text = "匯出成功。";

    #endregion
}
catch (Exception ex)
{
    // 記錄錯誤日誌(可選)
    Label_MSG1.ForeColor = System.Drawing.Color.Red;
    Label_MSG1.Text = "匯出失敗:" + ex.Message;
}

********************************************************************************

實際測試,效能似乎有改善,但是產生的 .xlsx 疑似有問題


Microsoft Excel

我們發現了...的部分內容有問題。您要我們盡可能嘗試復原嗎?如果您信任此活頁簿的來源,請按一下[是]。

待查測、、、

(完)

相關

沒有留言:

張貼留言