Aspose.HTML for Java 26.5 — unbounded heap growth on sequential HTML→PDF conversions (static caches not released despite dispose())

Summary

In a long-running JVM service that converts many HTML documents to PDF sequentially, post-GC live heap grows linearly and never plateaus, eventually causing OutOfMemoryError. Every conversion creates a fresh Configuration + HTMLDocument and disposes both in a finally block, exactly as the documentation prescribes, yet retained memory keeps climbing. A class histogram attributes essentially all of the growth to Aspose-internal static caches, not to application objects.

Minimal reproduction

A complete, self-contained repro (single Gradle dependency from your public Maven repo, no proprietary content):

GitHub - matthewisayan-cf/aspose-html-leak-repro · GitHub

./gradlew run --args="2000 100 30"   # <conversions> <sampleEvery> <warmup>

The harness loops Converter.convertHTML over the same document, forces GC, and prints used (post-GC) heap plus a baseline-vs-final class histogram diff. It runs in evaluation mode out of the box (the leak reproduces with or without a license).

Environment

  • Aspose.HTML for Java 26.5 (com.aspose:aspose-html:26.5:jdk17)
  • JDK 17
  • Reproduced on macOS (arm64) and Linux (x86_64)

Conversion code (per the docs — dispose in finally)

Configuration configuration = null;
HTMLDocument document = null;
try {
    configuration = new Configuration();
    document = new HTMLDocument(html, baseUrl, configuration);
    PdfSaveOptions saveOptions = new PdfSaveOptions();
    saveOptions.setTaggedPdf(true);
    saveOptions.getPageSetup().setAnyPage(new Page(new Size(Unit.fromInches(8.5), Unit.fromInches(11))));
    Converter.convertHTML(document, saveOptions, outputStream);
} finally {
    if (document != null) document.dispose();
    if (configuration != null) configuration.dispose();
}

Expected vs. actual

  • Expected: because every HTMLDocument and Configuration is disposed, post-GC live heap should reach a steady state and stay flat across thousands of identical conversions.
  • Actual: post-GC live heap rises linearly with no plateau; the process trends toward OutOfMemoryError.

Observed — post-GC live heap (500-conversion sample, JDK 17)

conversions live heap (post-GC)
0 64.6 MB
100 82.0 MB
200 98.2 MB
300 113.4 MB
400 130.8 MB
500 145.9 MB

+167 KB retained per conversion, linear. An extended run with a larger document showed the same shape over 3000 conversions: 328 MB → 1148 MB (~280 KB/conversion, no plateau). Metaspace / compressed class space stay flat throughout, so this is heap object retention, not class-metadata growth.

Where the memory goes (class histogram diff, live objects)

     deltaKB     deltaInstances   class
     19968.2             851976   com.aspose.html.utils.aGE
     19968.2             851976   com.aspose.html.utils.aGE$b
      8812.3             281992   com.aspose.html.utils.ms.System.Drawing.Color
      6609.2             281992   com.aspose.html.utils.aGD
      6609.2             281992   com.aspose.html.utils.aKL
      6609.2             281992   com.aspose.html.utils.aJR
      3304.6              70498   com.aspose.html.utils.aKK
      2423.3             103394   com.aspose.html.utils.ms.System.Collections.Generic.Dictionary$Link
      2203.1              70498   com.aspose.html.utils.aLh

The growth set is essentially 100% com.aspose.html.utils.* (plus the embedded System.Drawing.Color). Instance counts scale linearly with the number of conversions — i.e. a fixed number of objects is retained per conversion and never released.

What we already tried

  • Dispose HTMLDocument and Configuration on every conversion (above). No effect on the trend.
  • A fresh Configuration per conversion (not a shared/static one).
  • Forcing GC before each measurement, so this is retained (not merely uncollected) memory.

The only thing that fully reclaims the memory is tearing down and rebuilding the classloader that loaded Aspose (or restarting the process), which discards these static caches — consistent with the leak living in Aspose-internal static state.

Questions

  1. Is this a known issue, and is a fix targeted for an upcoming release?
  2. Is there a supported API to clear/flush these internal caches between conversions in a long-running process, so we don’t have to recycle the classloader/process?
  3. If not, what is the recommended pattern for high-volume, long-lived JVM services that must convert many documents without unbounded heap growth?

@matthewisayancf
We have opened the following new ticket(s) in our internal issue tracking system and will deliver their fixes according to the terms mentioned in Free Support Policies.

Issue ID(s): HTMLJAVA-2571

You can obtain Paid Support Services if you need support on a priority basis, along with the direct access to our Paid Support management team.

1 Like