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();
}
}
}