Eclipse Memory Analyzer(MAT)是一个图形化的Java堆分析工具,速度快且特性丰富,可以用于取代JDK自带的堆Dump浏览器(jhat)。使用MAT,你可以快速分析包含上亿对象的生产环境Dump文件,快速计算某种对象导致的内存占用量,进而评估内存泄漏风险。
你可以将MAT安装为独立运行的RCP应用,也可以作为插件安装到现有的Eclipse中。
jhat不是本文的主题,这里做一个简单的介绍。
这是JVM自带的堆Dump分析工具,执行如下命令即可使用:
jhat a.map # a.map为Dump文件路径
jhat默认在7000端口暴露HTTP服务,你可以通过浏览器进行以下查询:
| 查询 | 说明 |
| All classes including platform | 显示堆中所有类型(Java类)的列表,这些类被加载到JVM中 |
| All Classes excluding platform | 类似上面,但是不包括平台(即JDK内部)的类 |
| Show all members of the rootset |
显示所有GC Root的对外引用,按照以下几类GC Root,分段展示:
|
| Show instance counts for all classes (including platform) | 显示所有类型的实例计数(包含平台类) |
| Show instance counts for all classes (excluding platform) | 显示所有类型的实例计数(不包含平台类) |
| Show heap histogram |
显示一个表格,统计各类型的实例数量、内存占用,按内存占用降序排列 内存占用显示为Shallow size,即对象自身的内存占用。除了数组、字符串等之外的对象Shallow size通常很小,通过引用(指针)关联的其它对象不计入Shallow size,仅仅计入固定尺寸的指针大小 |
下面是一段急剧消耗内存的示例代码:
package com.dangdang.digital.test;
import org.apache.commons.lang.math.RandomUtils;
import java.util.concurrent.TimeUnit;
public class BusyApplication {
public static void main( String[] args ) {
new MemoryPredator( Integer.parseInt( args[0] ), Integer.parseInt( args[1] ) ).start();
}
}
class MemoryPredator extends Thread {
private final int consumeInterval;
private int retainTime;
public MemoryPredator( int consumeInterval, int retainTime ) {
this.consumeInterval = consumeInterval;
this.retainTime = retainTime;
}
@Override
public void run() {
for ( ; ; ) {
consumeMemory( retainTime );
try {
TimeUnit.MILLISECONDS.sleep( consumeInterval );
} catch ( InterruptedException e ) {
}
}
}
private void consumeMemory( final int retainTime ) {
new MemoryPreyThread( retainTime ).start();
}
}
class MemoryPreyThread extends Thread {
private final int retainTime;
public MemoryPreyThread( int retainTime ) {
this.retainTime = retainTime;
}
@Override
public void run() {
int n = ( RandomUtils.nextInt( 100 ) + 1 );
Object bigObj = createBigObject( n );
System.out.println( String.format( "%d MB consumed", n ) );
try {
TimeUnit.MILLISECONDS.sleep( retainTime );
} catch ( InterruptedException e ) {
}
}
private Object createBigObject( int n ) {
Object big = new byte[n * 1024 * 1024];
return big;
}
}
让上述程序执行一段时间(应用程序参数使用500 7000),然后使用下面的命令获取堆Dump:
# 只Dump可达对象,使用二进制格式,输出为bzapp.hprof jmap -dump:live,format=b,file=bzapp.hprof `jps | grep BusyApplication | cut -d' ' -f1`
通过jhat开启服务后,打开浏览器,首先看到的是堆中包含的类的列表(不包含平台类):
All Classes (excluding platform) Package com.dangdang.digital.test class com.dangdang.digital.test.BusyApplication [0xc0005048] class com.dangdang.digital.test.MemoryPredator [0xc0004ff0] class com.dangdang.digital.test.MemoryPreyThread [0xc0004f98] Package com.jarego.jayatana class com.jarego.jayatana.Agent [0xc0005200] Package org.apache.commons.lang.math class org.apache.commons.lang.math.JVMRandom [0xc0004e60] class org.apache.commons.lang.math.RandomUtils [0xc0004f00]
点击界面下方的Show heap histogram连接,可以看到Shallow size最大的类型:
Class Instance Count Total Size class [B 1063 788662183 class [I 512 6789172 class [C 2552 349224 class java.lang.Class 566 54336 class [Ljava.lang.Object; 467 48496 class [S 770 44518 class java.lang.String 2494 39904 class java.net.URL 346 30448 class [Ljava.util.WeakHashMap$Entry; 156 22464 class java.util.HashMap$Entry 771 21588
可以看到byte[]类型,即[B,占用绝大部分内存,接近800MB。
点击class [B上的链接,进入byte[]类型的主页面“Class 0xc009a858”,后面的是类对象的内存地址,和Dump分析关系不大。留意“References to this object”这一段内容:
[B@0xbb0b6e18 (240 bytes) : ?? [B@0xc00bad20 (30 bytes) : ?? [B@0xbae503c0 (29 bytes) : ?? [B@0xbb091708 (115 bytes) : ?? [B@0xbb057350 (29 bytes) : ?? ... [B@0xc87c6368 (15728656 bytes) : ?? # 大对象
任何类型的示例,都会持有该类型的Class对象的引用,byte[]也不例外。所有存活的byte[]对象因此列于“References to this object”中。我们需要关心的通常(取决于应用场景)是大对象,简单的滚动页面,留意那些更长的行,就可以定位到大的byte[]了。
可以看到对象[B@0xc87c6368占据了15M的内存空间,点击其上的链接,进入[B@0xc87c6368对象的主页面。在页面的下方,可以看到Other Queries - Reference Chains from Rootset - Exclude weak refs,点击后,可以查看从GC Root到此对象的引用链页面:
References to [B@0xc87c6368 (15728656 bytes) Java Local References Java Local Reference (from com.dangdang.digital.test.MemoryPreyThread@0xc4ec6208) : # 这是GCRoot --> [B@0xc87c6368 (15728656 bytes)
至此,我们可以大概判定,引发内存问题的代码,位于类MemoryPreyThread中。
点击菜单项 File ⇨ Open Heap Dump ...,看到如下界面:
上半部分显示当前正在分析的对象的基本信息,以上面的截图为例:
| 值 | 意义 |
| 0xcb4c69f8 |
对象的地址 |
| MemoryPreyThread | 对象的类型 |
| com.dangdang.digital.test | 对象的包名 |
| class com.dangdang.digital.test.MemoryPreyThread @ 0xc0004f98 |
对象的类型,以及类对象的地址 |
| java.lang.Thread | 对象的父类型 |
| sun.misc.Launcher$AppClassLoader @ 0xc0000bd8 | 加载对象的类的ClassLoader |
| 112(shadlow size) | 此对象本身的大小 |
| 104,857,784(retained size) | 此对象本身的大小 + 此对象引用(包括间接)对象的大小 |
| GC root: Native Stack,Thread | 提示此对象是否GC Root |
下半部分包括4个选项卡:
此面板使用饼图显示Retained Size最大的对象,并列出其导致的内存驻留的尺寸。这些对象往往就是问题所在。
饼图上方的按钮区、和饼图下方的Actions/Reports/Step By Step,功能是对应的。
其中Actions包括:
其中Reports包括:
使用jhat,我们就可以获取堆Dump中的全部信息了。 但是其UI非常简陋,缺乏排序、统计类功能,这些功能正是MAT能提供的。下面我们使用MAT来分析上一章的示例应用。
打开Dump后,在Overview面板的饼图上,可以立即看到所有大内存驻留都是由com.dangdang.digital.test.MemoryPreyThread类型的对象所引发。
单击任何一个饼图切片,点击菜单List Objects ⇨ With Outgoing References,可以看到对象引用的哪些对象的Shallow Size较大。这些对象就是内存驻留的根源:
很明显, MemoryPreyThread所引用的、byte[]类型的局部变量,其Shallow Size占据了全部内存份额。注意:如果上级对象的Outgoing引用特别多,则问题所在的子级对象可能看不到,这种情况下可以点击Retained Heap列排序或者手工展开。
对于像示例应用这样简单的代码,分析到这里就直接能定位到问题代码所在行号了。生产环境中的情况要复杂的多,单纯基于堆内存Dump进行静态分析不一定能奏效,需要结合线程栈Dump,甚至是剖析工具(JMC、JProfiler等)录制的内存分配历史。
概要情况如下图:

某个线程引用了接近6G的数据。从线程名字上来看,它是一个普通的工作线程,持有的6G内存应该是某种临时性对象。
分析此线程的Outgoing引用,排序Retained Heap列,将大内存条目显示出来:
可以看到,内存由一个局部变量(<Java Local>)导致。需要注意,局部变量的GC Root都是当前执行线程。出现问题的变量引用一个ArrayList对象,进一步展开可以猜测到此对象是数据库查询的结果集:
可以看到,此ArrayList的元素多达1000万。很可能是错误的SQL语句把大量行甚至整个表都读取过来,一下子把JVM内存撑爆了。
展开一个ByteArrayRow,查看行中的元素,进一步定位到所操作的表。配合JMC查看Hot Methods,定位到了存在问题的方法。
线上环境某服务,因为内存溢出、连续FGC被监控进程杀死,我们获得的堆Dump,内存占用总计1.7GB,Netty 3.x中的类NioClientSocketPipelineSink导致了绝大部分的内存驻留。
点击饼图中最大的切片NioClientSocketPipelineSink进行分析,打开菜单项List Objects ⇨ With Incoming References,展开:
可以看到NioClientSocketPipelineSink是Netty处理管线(Pipeline)的一部分。在很多框架中,管线常常和SRC、SINK等术语一起出现,SRC常常表示管线的输入、SINK则表示输出。
一个全局性的ChannelFactory、大量DefaultChannelPipeline通过sink属性引用了NioClientSocketPipelineSink。这些DefaultChannelPipeline应该是某种一次性对象,不熟悉代码的话只能猜测到这个程度,可以考虑通过JMC来录制对象分配的调用栈,来证实这种猜测。
那么,谁创建了这些DefaultChannelPipeline呢?从上面的截图中可以看到Dubbo的NettyClient间接引用了NioClientSocketPipelineSink,再随机抽取几个DefaultChannelPipeline展开:
可以看到来自Dubbo的NettyClient通过channel ⇨ pipeline ⇨ sink 引用了DefaultChannelPipeline。com.alibaba.dubbo上面引用链上唯一的非Netty包,我们有理由猜测Dubbo和内存溢出有关。
我们可以看一下相关对象的计数。对于希望统计数量的对象,在调用链上点选它,左下面板切换到Class Hierarchy,在类名上右击,选择List Object ⇨ With Incoming References:
由于类的每个对象都会引用类对象,因此展开右侧窗口的根节点,即可知晓对象的大概数量。在本例中,DefaultChannelPipeline、NioClientSocketChannel的数量大概是130万,NettyClient的数量是33。
MAT的支配树功能,可以帮助你追踪实际(Shallow)消耗内存的对象:
可以看到,Netty内部的某种链表结构(LinkedTransferQueue)的每个元素的RegisterTask对象占用了1KB的内存,但是链表长度非常大,因此这个链表消耗了大量的内存。
从名字LinkedTransferQueue可以猜测,这是Netty用来存储待处理的网络数据的。
从MAT分析的结果来看,NioClientSocketPipelineSink的内部类Boss的实例变量registerTaskQueue,占据了高达1.64GB的内存。我们来看一下这个类是做什么的:
// 代码中不相关的部分做了删减
class NioClientSocketPipelineSink extends AbstractChannelSink {
final Executor bossExecutor;
private final Boss[] bosses;
private final NioWorker[] workers;
private final AtomicInteger bossIndex = new AtomicInteger();
private final AtomicInteger workerIndex = new AtomicInteger();
// 这是外部能影响队列registerTaskQueue的唯一入口
public void eventSunk( ChannelPipeline pipeline, ChannelEvent e) throws Exception {
if (e instanceof ChannelStateEvent) {
ChannelState state = event.getState();
Object value = event.getValue();
switch (state) {
case CONNECTED:
if (value != null) {
// 连接到客户端通道
connect(channel, future, (SocketAddress) value);
} else {
channel.worker.close(channel, future);
}
break;
} else if (e instanceof MessageEvent) {}
}
private void connect( final NioClientSocketChannel channel, final ChannelFuture cf, SocketAddress remoteAddress) {
try {
if (channel.socket.connect(remoteAddress)) {
channel.worker.register(channel, cf);
} else {
// 通道是非阻塞模式,因此会走到这个分支
nextBoss().register(channel);
}
} catch (Throwable t) {}
}
Boss nextBoss() {
return bosses[Math.abs( bossIndex.getAndIncrement() % bosses.length)];
}
private final class Boss implements Runnable {
private final AtomicBoolean wakenUp = new AtomicBoolean();
private final Object startStopLock = new Object();
private final Queue<Runnable> registerTaskQueue = new LinkedTransferQueue<Runnable>();
// 这个方法直接导致registerTaskQueue增大
void register(NioClientSocketChannel channel) {
Runnable registerTask = new RegisterTask(this, channel);
synchronized (startStopLock) {
// 如果尚未启动,则使用Executor启动Client Boss线程
// 每当有新的连接请求到达,都会尝试启动Client Boss线程
if (!started) {
DeadLockProofWorker.start(
bossExecutor,
new ThreadRenamingRunnable(
this, "New I/O client boss #" + id + '-' + subId));
}
// 添加一个RegisterTask
boolean offered = registerTaskQueue.offer(registerTask);
}
if (wakenUp.compareAndSet(false, true)) {
// 唤醒选择器
selector.wakeup();
}
}
public void run() {
Selector selector = this.selector;
long lastConnectTimeoutCheckTimeNanos = System.nanoTime();
for (;;) {
wakenUp.set(false);
try {
// 最多阻塞500ms
int selectedKeyCount = selector.select(500);
if (wakenUp.get()) {
selector.wakeup();
}
// 消费registerTaskQueue队列
processRegisterTaskQueue();
if (selector.keys().isEmpty()) {
if (shutdown ||
bossExecutor instanceof ExecutorService && ((ExecutorService) bossExecutor).isShutdown()) {
synchronized (startStopLock) {
if (registerTaskQueue.isEmpty() && selector.keys().isEmpty()) {
// 标记Client Boss线程为停止状态,下次再有连接请求到达,会再次启动
started = false;
try {
selector.close();
} catch (IOException e) {
logger.warn(
"Failed to close a selector.", e);
} finally {
this.selector = null;
}
// 退出条件
break;
} else {
shutdown = false;
}
}
} else {
shutdown = true;
}
} else {
shutdown = false;
}
} catch (Throwable t) {}
}
}
private void processRegisterTaskQueue() {
for (;;) {
final Runnable task = registerTaskQueue.poll();
if (task == null) {
break;
}
task.run();
}
}
private static final class RegisterTask implements Runnable {
RegisterTask(Boss boss, NioClientSocketChannel channel) {
this.boss = boss;
this.channel = channel;
}
public void run() {
try {
// 在指定的选择器上注册通道的事件
channel.socket.register( boss.selector, SelectionKey.OP_CONNECT, channel);
} catch (ClosedChannelException e) {
channel.worker.close(channel, succeededFuture(channel));
}
int connectTimeout = channel.getConfig().getConnectTimeoutMillis();
if (connectTimeout > 0) {
channel.connectDeadlineNanos = System.nanoTime() + connectTimeout * 1000000L;
}
}
}
}
可以看到:
也就是说,正常情况下,registerTaskQueue队列不应该出现堆积的。由于发现问题时没有开启Dubbo或Netty的日志,准备等待下次问题重现后继续深入分析。
Leave a Reply