1. jmap
jmap
工具可以很好地去分析一个Java进程堆内存的情况,并生成dump文件。
常用的有:
jmap -heap <pid>
: 对PID进程进行堆分析,可以看到堆内存分配的配置,以及堆内存各个区域的占用情况
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
28
29
30
31
32
33
34
35
36
37
38
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4263510016 (4066.0MB)
NewSize = 89128960 (85.0MB)
MaxNewSize = 1420820480 (1355.0MB)
OldSize = 179306496 (171.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 1103101952 (1052.0MB)
used = 43295592 (41.289894104003906MB)
free = 1059806360 (1010.7101058959961MB)
3.924894876806455% used
From Space:
capacity = 110100480 (105.0MB)
used = 109700880 (104.61891174316406MB)
free = 399600 (0.3810882568359375MB)
99.63705880301339% used
To Space:
capacity = 167247872 (159.5MB)
used = 0 (0.0MB)
free = 167247872 (159.5MB)
0.0% used
PS Old Generation
capacity = 385875968 (368.0MB)
used = 251045376 (239.41552734375MB)
free = 134830592 (128.58447265625MB)
65.05856721297555% used
36764 interned Strings occupying 4003016 bytes.
jmap -dump:format=b, file=<filename> <pid>
: 对PID进程堆内存区域进行dump,生成二进制的转储文件,一般还需要压缩,它可以被用于MAT工具以进一步分析jmap -histo[:live] <pid>
: 对PID进程进行堆内存(可选活跃对象)分析,生成统计文本,一般像这样:
1
2
3
4
5
6
num #instances #bytes class name
----------------------------------------------
1: 1597147 86346272 [Ljava.lang.Object;
2: 1383173 55326920 java.util.TreeMap$Entry
3: 585564 37193120 [C
4: 1495556 35893344 java.util.ArrayList
2. 使用 Memory Analyzer 分析问题
MAT可提供多种分析维度,如:
-
Histogram: 列出内存中的对象,对象的个数以及大小
- Dominator Tree: 列出大小最大(biggest)的一些对象,并显示出什么让其存活
- Top Consumers:通过图形列出哪些对象消耗最大(most expensive)
- Leak Suspects: 分析内存泄漏
以线上一个Full GC Dump的例子,说明一下:
2.1. Histogram
进入这个界面后,可以看到byte[]
对象占用了非常大的空间:
右键List objects -> with incomming references可以看到这个对象的实例,发现一个75MB的大对象:
这里(可参考这里Shallow & Retained Heap):
- Shallow Heap :一个对象内存的消耗大小,不包含对其他对象的引用;
- Retained Heap :是Shallow Heap的总和,也就是该对象被GC之后所能回收的内存大小;
我们需要查看其GC Root,右键Path to GC Roots -> exclude all phantom/weak/soft etc. references。可以看到,这个东西被BufferedImage
引用,然后被包到一个对象BrandGoodInfo
中,被缓存了起来。
因此我们怀疑,在配置内存缓存的时候,配置不当,造成BufferedImage
对象过多的缓存,内存爆掉,引发Full GC。
2.2. Dominator Tree
结果如下,下面的哪个类是我们用的第二方包,暂时先不考虑,第一个很明显,缓存占了大头,那个70多MB的东西也在那里面,加深了2.1.节的怀疑。
2.3. Top Consumer
结果和2.2类似,如下图,缓存这里占了大多数的内存。
2.4. Leak Suspects
内存泄漏分析只能做一个参考,并不能代表其真正内存泄漏了。如下图,可以看出,问题出在:
SummerCacheManager
:这是用于管理缓存的,我们这里用它来管理缓存的RestServiceProviderImpl
: 项目中我没有看到其自定义配置项,只是引用了外部的配置文件(只读),所以忽略
所以极大的可能出在了缓存问题,尤其是BufferedImage
的缓存
3. 修改配置
在上文中,BrandGoodInfo
被缓存起来,在Caffeine中给了很多的槽,显然是过多了:
后来我减少了内存缓存数量,之后直接取消内存缓存,使用磁盘缓存。
而为什么要这么做呢?
下面要说缓存BufferedImage
的一些坑:
- 项目中,
BufferedImage
的缓存流程是:下载图片 -> 通过ImageIO.read()
转成BufferedImage
。很有可能的是,下载的图片过大,那么转成BufferedImage
的对象也会很大。试想一下,1000个70MB的图片被缓存,岂不是要爆炸? - 此外
BufferedImage
存储的内容是不经过压缩的,你本地磁盘上读取了一个图片文件,转成BufferedImage
对象后,大小可能是文件大小的数倍。这是因为BufferedImage
的对象大小是要按照位图那一套算法计算的,即像素数 * 单个像素存储大小
。一般项目中都是用彩图,即24位。- 一个例子:一张1200 * 900的彩图A和黑白图B,大小分别为800KB和100KB,均为JPG格式,但是读到内存里后,大小变为了3MB多,这是因为它们都用彩图存储(
ImageIO.read()
就是这么处理的),且size = 1200 * 900 * 24 / 8 = 3240000 bytes
。
- 一个例子:一张1200 * 900的彩图A和黑白图B,大小分别为800KB和100KB,均为JPG格式,但是读到内存里后,大小变为了3MB多,这是因为它们都用彩图存储(
所以当要缓存图片或者大对象(包含大量字节串)的时候,可以考虑:
- 对对象进行压缩(显然
BufferedImage
压缩比较困难,可以用第三方库?) - 另外我认为,对于图像的缓存(或者是大的
byte[]
对象),应该利用磁盘缓存或者用类似Redis那样的缓存,而不是保存在本机内存里