You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@poi.apache.org by fa...@apache.org on 2026/10/03 19:29:54 UTC

(poi) branch trunk updated: Let the CellFormat cache release formats that are no longer used (#1359)

This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/poi.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 2ba32543a4 Let the CellFormat cache release formats that are no longer used (#1359)
2ba32543a4 is described below

commit 2ba32543a4e476699a5f5ff1c1b1419282a2b9e5
Author: Bernd Engelhardt <be...@softvision.de>
AuthorDate: Sat Oct 3 21:29:48 2026 +0200

    Let the CellFormat cache release formats that are no longer used (#1359)
    
    CellFormat.getInstance caches every parsed format in a WeakHashMap keyed
    by the format string. The cached CellFormat refers to its own format
    string, so the weak key stays strongly reachable through the value and
    no entry is ever released. Every distinct multi-section number format a
    DataFormatter formats therefore stays in memory for the life of the JVM:
    20,000 distinct three-section formats left 45 MB behind (JDK 21), which
    a long-running service converting untrusted workbooks accumulates.
    
    Hold the cached CellFormat through a WeakReference, so an entry is
    released with its key once nothing else refers to the CellFormat.
    DataFormatter does not keep it, so in practice an entry lasts until the
    next garbage collection; getInstance already documents that two calls
    may or may not return the same object. Measured: retained memory at the
    level of a run without multi-section formats, and formatting throughput
    unchanged despite the shorter-lived entries.
    
    Co-authored-by: Bernd Engelhardt <29...@users.noreply.github.com>
    Co-authored-by: Claude Opus 5.5 <no...@anthropic.com>
---
 .../java/org/apache/poi/ss/format/CellFormat.java     | 18 +++++++++++++-----
 .../java/org/apache/poi/ss/format/TestCellFormat.java | 19 +++++++++++++++++++
 2 files changed, 32 insertions(+), 5 deletions(-)

diff --git a/poi/src/main/java/org/apache/poi/ss/format/CellFormat.java b/poi/src/main/java/org/apache/poi/ss/format/CellFormat.java
index 5d404e3dbd..e708317a8c 100644
--- a/poi/src/main/java/org/apache/poi/ss/format/CellFormat.java
+++ b/poi/src/main/java/org/apache/poi/ss/format/CellFormat.java
@@ -17,6 +17,7 @@
 
 package org.apache.poi.ss.format;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -131,8 +132,13 @@ public class CellFormat {
         };
     }
 
-    /** Maps a format string to its parsed version for efficiencies sake. */
-    private static final Map<Locale, Map<String, CellFormat>> formatCache =
+    /**
+     * Maps a format string to its parsed version for efficiencies sake.
+     * <p>
+     * The parsed version is held by a weak reference: a {@code CellFormat} refers to its own format string,
+     * so holding it strongly would keep the weak key, and with it the entry, reachable forever.
+     */
+    private static final Map<Locale, Map<String, WeakReference<CellFormat>>> formatCache =
             new WeakHashMap<>();
 
     /**
@@ -157,14 +163,16 @@ public class CellFormat {
      * @return A CellFormat that applies the given format.
      */
     public static synchronized CellFormat getInstance(Locale locale, String format) {
-        Map<String, CellFormat> formatMap = formatCache.computeIfAbsent(locale, k -> new WeakHashMap<>());
-        CellFormat fmt = formatMap.get(format);
+        Map<String, WeakReference<CellFormat>> formatMap =
+                formatCache.computeIfAbsent(locale, k -> new WeakHashMap<>());
+        WeakReference<CellFormat> cached = formatMap.get(format);
+        CellFormat fmt = cached == null ? null : cached.get();
         if (fmt == null) {
             if (format.equals("General") || format.equals("@"))
                 fmt = createGeneralFormat(locale);
             else
                 fmt = new CellFormat(locale, format);
-            formatMap.put(format, fmt);
+            formatMap.put(format, new WeakReference<>(fmt));
         }
         return fmt;
     }
diff --git a/poi/src/test/java/org/apache/poi/ss/format/TestCellFormat.java b/poi/src/test/java/org/apache/poi/ss/format/TestCellFormat.java
index e19b6c5f90..f7e2bb7f19 100644
--- a/poi/src/test/java/org/apache/poi/ss/format/TestCellFormat.java
+++ b/poi/src/test/java/org/apache/poi/ss/format/TestCellFormat.java
@@ -18,10 +18,12 @@ package org.apache.poi.ss.format;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.awt.Color;
 import java.io.IOException;
+import java.lang.ref.WeakReference;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -1173,4 +1175,21 @@ class TestCellFormat {
         assertNotNull(part);
         assertEquals("0h 0m 9s", part.apply(0.0001).text);
     }
+
+    /**
+     * The format cache must not keep a format string nobody refers to any more: a cached {@code CellFormat}
+     * refers to its own format string, which used to keep the weak key of its entry reachable forever.
+     */
+    @Test
+    void testCacheReleasesUnusedFormats() throws InterruptedException {
+        String format = "0.00;-0.00;\"cache-" + System.nanoTime() + "\"";
+        WeakReference<String> key = new WeakReference<>(format);
+        assertNotNull(CellFormat.getInstance(Locale.ROOT, format));
+        format = null;
+        for (int i = 0; i < 50 && key.get() != null; i++) {
+            System.gc();
+            Thread.sleep(10);
+        }
+        assertNull(key.get(), "the format cache keeps a format string that is no longer used");
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@poi.apache.org
For additional commands, e-mail: commits-help@poi.apache.org