Skip to content

Memory leak & deferred GC retention of ExcelWorkbook due to unnecessary finalizers in CellStore<T>, ColumnIndex<T>, PageIndex and ExcelVmlDrawingCollection #2456

Description

@smad2005

EPPlus usage

Noncommercial use

Environment

windows

Epplus version

8.6.3

Spreadsheet application

Excel

Description

In EPPlus 8.6.3, the internal classes CellStore<T>, ColumnIndex<T>, PageIndex, and ExcelVmlDrawingCollection declare destructors / finalizers (e.g. ~CellStore(), ~ExcelVmlDrawingCollection()) that perform no actual unmanaged cleanup—they merely set internal managed array references to null. Furthermore, their Dispose() methods fail to call GC.SuppressFinalize(this).
Impact on Memory:

  1. Every instance of CellStore (such as MetadataCellStore) and ExcelVmlDrawingCollection is automatically placed in the GC Finalization Queue upon creation.
  2. When package.Dispose() is called, these objects are NOT suppressed from finalization.
  3. Upon Garbage Collection, GC moves these unsuppressed objects to the F-Reachable Queue.
  4. While queued in the F-Reachable Queue, MetadataCellStore holds strong references to ExcelMetadata -> ExcelWorkbook, and ExcelVmlDrawingCollection holds ExcelWorksheet -> ExcelPackage -> ExcelWorkbook.
  5. This prevents the GC from immediately reclaiming the ExcelPackage / ExcelWorkbook memory, promoting the entire object graph to Gen 1 / Gen 2 GC generations and delaying memory release until the finalizer thread executes and a subsequent GC run occurs.
Image

Step to Reproduce (Repro Unit Test)

File to add: src/EPPlusTest/Core/FinalizerTests.cs (in the EPPlusTest project):

using System;
using System.Runtime.CompilerServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml;
using OfficeOpenXml.Core.CellStore;
using OfficeOpenXml.Drawing.Vml;
namespace EPPlusTest.Core
{
    [TestClass]
    public class FinalizerTests
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void CreateAndDisposePackage(out WeakReference packageRef, out WeakReference workbookRef, out WeakReference cellStoreRef, out WeakReference vmlRef)
        {
            var package = new ExcelPackage();
            var ws = package.Workbook.Worksheets.Add("Sheet1");
            ws.Cells["A1"].Value = "Test";
            var vml = ws.VmlDrawings;
            vml.AddComment(ws.Cells["A1"]);
            // Pass trackResurrection: true to detect objects retained in the F-Reachable queue
            packageRef = new WeakReference(package, true);
            workbookRef = new WeakReference(package.Workbook, true);
            cellStoreRef = new WeakReference(ws._values, true);
            vmlRef = new WeakReference(vml, true);
            package.Dispose();
        }
        [TestMethod]
        public void DisposedPackageComponentsShouldBeGarbageCollectedWithoutWaitingForFinalizers()
        {
            CreateAndDisposePackage(out var packageRef, out var workbookRef, out var cellStoreRef, out var vmlRef);
            // Force GC without waiting for pending finalizers
            GC.Collect(2, GCCollectionMode.Forced, true, true);
            Assert.IsFalse(packageRef.IsAlive, "ExcelPackage should be collected immediately after Dispose.");
            Assert.IsFalse(workbookRef.IsAlive, "ExcelWorkbook should be collected immediately after Dispose.");
            Assert.IsFalse(cellStoreRef.IsAlive, "CellStore should be collected immediately after Dispose.");
            Assert.IsFalse(vmlRef.IsAlive, "ExcelVmlDrawingCollection should be collected immediately after Dispose.");
        }
    }
}

Recommended Fixes (Exact Diffs)

Remove the unnecessary finalizers (since no native/unmanaged handles are owned) and add GC.SuppressFinalize(this) to the Dispose() methods:

  1. src/EPPlus/Core/CellStore/CellStore.cs
@@ -62,7 +62,0 @@
         public CellStore()
         {
             _columnIndex = new ColumnIndex<T>[CellStoreSettings.ColSizeMin];
         }
-        ~CellStore()
-        {
-            _columnIndex = null;
-        }
         internal bool HasValues
@@ -1160,6 +1160,7 @@
         public void Dispose()
         {
             if (_columnIndex == null) return;
             lock (_syncRoot)
             {
                 for (var c = 0; c < ColumnCount; c++)
                 {
                     if (_columnIndex[c] != null)
                     {
                         ((IDisposable)_columnIndex[c]).Dispose();
                     }
                 }
                 _columnIndex = null;
             }
+            GC.SuppressFinalize(this);
         }
  1. src/EPPlus/Drawing/Vml/ExcelVmlDrawingCollection.cs
@@ -52,5 +52,0 @@
-        ~ExcelVmlDrawingCollection()
-        {
-            _drawingsCellStore?.Dispose();
-            _drawingsCellStore = null;
-        }
         protected internal void AddDrawingsFromXml(ExcelWorksheet ws)
@@ -753,4 +753,5 @@
         void IDisposable.Dispose()
         {
-            _drawingsCellStore.Dispose();
+            _drawingsCellStore?.Dispose();
+            GC.SuppressFinalize(this);
         }
  1. src/EPPlus/Core/CellStore/ColumnIndex.cs
@@ -30,4 +30,0 @@
-        ~ColumnIndex()
-        {
-            _pages = null;
-        }
         internal int GetPagePosition(int Row)
@@ -269,6 +269,7 @@
         public void Dispose()
         {
             if (_pages == null) return;
             for (int p = 0; p < PageCount; p++)
             {
                 (_pages[p] as IDisposable)?.Dispose();
             }
             _pages = null;
             if (_values != null) _values.Clear();
+            GC.SuppressFinalize(this);
         }
  1. src/EPPlus/Core/CellStore/PageIndex.cs
@@ -48,4 +48,0 @@
-        ~PageIndex()
-        {
-            Rows = null;
-        }
         internal int Offset = 0;
@@ -144,3 +144,4 @@
         public void Dispose()
         {
             Rows = null;
+            GC.SuppressFinalize(this);
         }

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions