[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

debian/patches/jdk-freetypeScaler-crash.diff causes a memory leak



Greetings,

The changes in jdk-freetypeScaler-crash.diff cause a memory leak in openjdk-8 runtimes. Normally the object graph is like:

FileFont:
- hard reference to FreetypeFontScaler

FreetypeFontScaler:
- weak reference to FileFont
- registered as DisposerRecord of FileFont in Disposer

Disposer:
- weak reference to FileFont
- hard reference to FreetypeFontScaler

Which overall means that FileFont keeps FreetypeFontScaler alive, but not the other way around.

The patch makes FreetypeFontScaler native code create a global reference to the FileFont, and would remove that global reference during native disposal. Except disposal never happens, because it's dependent on the FileFont being GC'd first and the global reference keeps the FileFont alive.

As a result, a system that creates temporary fonts (like, say, a graphical PDF processor because most interesting files have embedded fonts) leaks on- and off-heap memory when run on Debian-originated OpenJDKs.


Attached is a test program that illustrates the problem. Run it with `java -XX:SoftRefLRUPolicyMSPerMB=0 FontLeak /path/to/some/ttf/file`. The soft reference setting is there because there are some soft references that would normally hold up font disposal until near-OOME, which would unnecessarily complicate the test.

The number of FreetypeFontScaler instances drops to near-zero after each test round on non-Debian OpenJDKs (tested w/ Fedora and self-compiled standard OpenJDK), whereas on Debian they accumulate as the test goes on.


Based on history of https://salsa.debian.org/java-team/openjdk-8/commits/master/debian/patches/jdk-freetypeScaler-crash.diff the patch was added during OpenJDK 6 era.

It would be good to know what problem the patch was supposed to fix, as that would tell if the patch is still necessary with OpenJDK 8+. Unfortunately comments in the patch file, nor the commit where it was added mention bug IDs. @doko got any recollection about this?

https://bugs.openjdk.java.net/browse/JDK-8132985 is possibly related in that a double free doesn't happen if you never free at all.


 -- Heikki Aitakangas
import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.io.File;
import java.lang.reflect.Field;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.logging.Logger;

public class FontLeak
{
  private static final Logger log = Logger.getGlobal();
  private static Field disposerRecords;

  static
  {
    try
    {
      Class<?> disposerClass = Class.forName("sun.java2d.Disposer");
      disposerRecords = disposerClass.getDeclaredField("records");

      disposerRecords.setAccessible(true);
    }
    catch(Exception e)
    {
      e.printStackTrace();
      System.exit(1);
    }
  }

  public static void createFont(String fontPath) throws Throwable
  {
    Font awtFont = Font.createFont(Font.TRUETYPE_FONT,new File(fontPath));

    // Scalers are created lazily, requesting metrics forces it to happen
    FontRenderContext frc =
      new FontRenderContext(new AffineTransform(),false,true);
    awtFont.getLineMetrics("abcdefghijklmnopqrstuvwxyz",frc);
  }

  public static void showDisposerRecords() throws Throwable
  {
    Hashtable<?, ?> records =
      (Hashtable<?, ?>)disposerRecords.get(null);

    HashMap<Class<?>,int[]> classes = new HashMap<>();

    synchronized(records)
    {
      log.info("Examining Disposer records. Total: " + records.size() + ", distribution:");

      for(Object record: records.values())
      {

        Class<?> klass = record.getClass();
        int[] count = classes.get(klass);
        if(count == null)
        {
          count = new int[1];
          classes.put(klass,count);
        }
        count[0] += 1;
      }
    }

    for(Map.Entry<Class<?>,int[]> entry: classes.entrySet())
      log.info("Records of class " + entry.getKey() + ": " + entry.getValue()[0]);
  }

  public static void waitUntilDisposerStable() throws Throwable
  {
    Hashtable<?,?> records = (Hashtable<?,?>)disposerRecords.get(null);

    Instant start = Instant.now();
    int count = records.size();

    log.info("Disposer records count at start of wait: " + count);

    while(true)
    {
      Thread.sleep(100);

      if(records.size() == count)
      {
        System.gc();
        Thread.sleep(100);
        if(records.size() == count)
          break;
      }

      count = records.size();
    }

    Instant end = Instant.now();
    Duration duration = Duration.between(start,end);

    log.info("Disposer is stable at: " + count + ", took " + duration);
  }

  public static void main(String... args) throws Throwable
  {
    if(args.length < 1)
    {
      System.out.println("Usage: FontLeak path-to-a-ttf");
      System.exit(1);
    }

    final String fontPath = args[0];

    for(int round = 0; round < 10; round++)
    {
      log.info("Starting round " + round);

      for(int i = 0; i < 10000; i++)
        createFont(fontPath);

      showDisposerRecords();
      System.gc();
      waitUntilDisposerStable();
      showDisposerRecords();
    }
  }
}

Reply to: