目录
  1. 起因
  2. 问题分析
    1. 业务
    2. 代码
    3. 泄漏原因
      1. 原因概述
      2. 调用栈
      3. 根本原因
        1. demo代码&效果图
      4. 辅助证明:JDK已知bug
  3. 解决方案
    1. 为字体做个缓存
    2. 原因详解
      1. 具体原因
  4. 快速判断此类问题的方法
记一次Font导致JVM堆外内存泄漏分析

起因

双11期间,公司的某个Java服务内存占用达到37g,但是该应用的JVM配置为-Xms6g -Xmx6g

问题分析

业务

主要是涉及到了图片文字合成业务

代码

下面是问题代码的简化版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}
}

泄漏原因

原因概述

每次new Font()之后,调用g.drawString()方法都会在Non-Heap区域分配一块内存且不回收

调用栈

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(...)

根本原因

在调用栈中第二个标红的部分

new T2KFontScaler() 时会调用 T2KFontScaler.initNativeScaler()这个native方法,这个native方法会在Non-Heap部分分配内存,且之后也没有相应的回收机制。

demo代码&效果图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}
}
}

image-20181127183725026

辅助证明:JDK已知bug

JDK-7074159 : run out of memory

解决方案

为字体做个缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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. 方案1: 不使用缓存,就是原先会导致内存泄漏的方案
  2. 方案2: 对字体做缓存

具体原因

现在来看调用栈部分第一个标红的位置,源码如下

image-20181122122215650

快速判断此类问题的方法

1
jmap -histo <pid> | grep FontScaler

如果该对象特别多,那极大可能是由于这个原因导致

文章作者: 谷河
文章链接: https://www.lyytaw.com/java/%E8%AE%B0%E4%B8%80%E6%AC%A1Font%E5%AF%BC%E8%87%B4JVM%E5%A0%86%E5%A4%96%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E5%88%86%E6%9E%90/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 谷河|BLOG