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:
- Every instance of
CellStore (such as MetadataCellStore) and ExcelVmlDrawingCollection is automatically placed in the GC Finalization Queue upon creation.
- When
package.Dispose() is called, these objects are NOT suppressed from finalization.
- Upon Garbage Collection, GC moves these unsuppressed objects to the F-Reachable Queue.
- While queued in the
F-Reachable Queue, MetadataCellStore holds strong references to ExcelMetadata -> ExcelWorkbook, and ExcelVmlDrawingCollection holds ExcelWorksheet -> ExcelPackage -> ExcelWorkbook.
- 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.
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:
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);
}
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);
}
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);
}
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);
}
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, andExcelVmlDrawingCollectiondeclare destructors / finalizers (e.g.~CellStore(),~ExcelVmlDrawingCollection()) that perform no actual unmanaged cleanup—they merely set internal managed array references tonull. Furthermore, theirDispose()methods fail to callGC.SuppressFinalize(this).Impact on Memory:
CellStore(such asMetadataCellStore) andExcelVmlDrawingCollectionis automatically placed in the GC Finalization Queue upon creation.package.Dispose()is called, these objects are NOT suppressed from finalization.F-Reachable Queue,MetadataCellStoreholds strong references toExcelMetadata->ExcelWorkbook, andExcelVmlDrawingCollectionholdsExcelWorksheet->ExcelPackage->ExcelWorkbook.ExcelPackage/ExcelWorkbookmemory, 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.Step to Reproduce (Repro Unit Test)
File to add:
src/EPPlusTest/Core/FinalizerTests.cs(in theEPPlusTestproject):Recommended Fixes (Exact Diffs)
Remove the unnecessary finalizers (since no native/unmanaged handles are owned) and add GC.SuppressFinalize(this) to the Dispose() methods:
src/EPPlus/Core/CellStore/CellStore.cssrc/EPPlus/Drawing/Vml/ExcelVmlDrawingCollection.cssrc/EPPlus/Core/CellStore/ColumnIndex.cssrc/EPPlus/Core/CellStore/PageIndex.cs