причина
Во время Double 11 использование памяти службой Java в компании достигло 37 г, но конфигурация JVM приложения была-Xms6g -Xmx6g
анализ проблемы
бизнес
В основном связан с бизнесом по синтезу изображений и текста.
код
Ниже приведена упрощенная версия рассматриваемого кода.
public class FontMain {
public static void main(String[] args) throws IOException, FontFormatException, InterruptedException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
File file = new File("/Users/cayun/PingFang.ttc");
while (true) {
run(file);
Thread.sleep(1);
}
}
private static void run(File file) throws IOException, FontFormatException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
BufferedImage blankImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
Graphics2D g = blankImage.createGraphics();
Font font = Font.createFont(Font.TRUETYPE_FONT, file);
font = font.deriveFont(12.0f);
g.setFont(font);
g.drawString("hello", 12, 12);
}
}
Утечка
Обзор причин
После каждого нового Font() вызов метода g.drawString() будет выделять часть памяти в области, отличной от кучи, а не освобождать ее.
стек вызовов
Стек вызовов g.drawString() выглядит следующим образом:
SunGraphics2D.drawString(String, int, int)
-> ValidatePipe.drawString(SunGraphics2D, String, double, double)
-> SunGraphics2D.getFontInfo()
-> SunGraphics2D.checkFontInfo
-> Font2D.getStrike(Font, AffineTransform, AffineTransform, int, int)
-> Font2D.getStrike(FontStrikeDesc, boolean)
->FileFont.createStrike(FontStrikeDesc)
-> ... -> T2KFontScaler.<init>(Font2D, int, boolean, int)
-> T2KFontScaler.initNativeScaler(...)
основная причина
Вторая часть стека вызовов отмечена красным
Новый метод t2kfontscaler() вызывает собственный метод T2kfontscaler.initnativeScaler(), этот собственный метод будет выделять память в разделе, отличном от HEAP, и нет соответствующего механизма повторного использования.
демо-код и визуализация
public class FontMain {
public static void main(String[] args) throws IOException, FontFormatException, InterruptedException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, InstantiationException {
File file = new File("/System/Library/Fonts/AquaKana.ttc");
Font font = Font.createFont(Font.TRUETYPE_FONT, file);
font = font.deriveFont(12.0f);
BufferedImage blankImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
Graphics2D g = blankImage.createGraphics();
g.setFont(font);
// T2KFontScaler无法通过new的方式创建,此处使用反射创建
Class clazz = Class.forName("sun.font.T2KFontScaler");
Constructor constructor = clazz.getConstructor(Font2D.class, int.class, boolean.class, int.class);
constructor.setAccessible(true);
while (true) {
constructor.newInstance(((SunGraphics2D) g).getFontInfo().font2D, 0, true, 80005872);
Thread.sleep(1);
}
}
}
Вспомогательное доказательство: известная ошибка JDK
JDK-7074159 : run out of memory
решение
кеш для шрифтов
public class FontMain {
private static Font font = null;
private static Object lock = new Object();
public static void main(String[] args) throws IOException, FontFormatException, InterruptedException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
File file = new File("/Users/cayun/PingFang.ttc");
while (true) {
run(file);
Thread.sleep(1);
}
}
private static void run(File file) throws IOException, FontFormatException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
BufferedImage blankImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
Graphics2D g = blankImage.createGraphics();
if (font == null) {
synchronized (lock) {
if (font == null) {
font = Font.createFont(Font.TRUETYPE_FONT, file);
}
}
}
font = font.deriveFont(12.0f);
g.setFont(font);
g.drawString("hello", 12, 12);
}
}
Подробное объяснение причин
Это решение выглядит немного странно, и легко может возникнуть такой вопрос: именно метод g.drawString() вызывает утечку памяти, а зачем Font кешировать?
Чтобы просто объяснить причину, мы сначала определим две схемы
- Вариант 1: не использовать кеш, который является оригинальным решением, вызывающим утечку памяти.
- Решение 2. Кэш-шрифты
конкретная причина
Теперь давайте посмотрим на первую позицию, отмеченную красным цветом в части стека вызовов, исходный код выглядит следующим образом.
Быстрый способ диагностики таких проблем
jmap -histo <pid> | grep FontScaler
Если объект особенно большой, это, скорее всего, связано с этой причиной