动手探究Java内存泄露问题

译文
开发 后端
在本系列教程中,将带大家动手探究Java内存泄露之谜,并教授给读者相关的分析方法。以下是一个案例。

在本系列教程中,将带大家动手探究Java内存泄露之谜,并教授给读者相关的分析方法。以下是一个案例。

最近有一个服务器,经常运行的时候就出现过载宕机的现象。重启脚本和系统后,该个问题还是会出现。尽管有大量的数据丢失,但因不是关键业务,问题并不严重。不过还是决定作进一步的调查,来看下问题到底出现在哪。首先注意到的是,服务器通过了所有的单元测试和完整的集成环境的测试。在测试环境下使用测试数据时运行正常,那么为什么在生产环境中运行会出现问题呢?很容易会想到,也许是因为实际运行时的负载大于测试时的负载,甚至超过了设计的负荷,从而耗尽了资源。但是到底是什么资源,在哪里耗尽了呢?下面我们就研究这个问题

为了演示这个问题,首先要做的是编写一些内存泄露的代码,将使用生产-消费者模式去实现,以便更好说明问题。

例子中,假定有这样一个场景:假设你为一个证劵经纪公司工作,这个公司将股票的销售额和股份记录在数据库中。通过一个简单进程获取命令并将其存放在一个队列中。另一个进程从该队列中读取命令并将其写入数据库。命令的POJO对象十分简单,如下代码所示:
 

  1. public class Order { 
  2.   
  3.   private final int id; 
  4.   
  5.   private final String code; 
  6.   
  7.   private final int amount; 
  8.   
  9.   private final double price; 
  10.   
  11.   private final long time; 
  12.   
  13.   private final long[] padding; 
  14.   
  15.   /** 
  16.    * @param id 
  17.    *            The order id 
  18.    * @param code 
  19.    *            The stock code 
  20.    * @param amount 
  21.    *            the number of shares 
  22.    * @param price 
  23.    *            the price of the share 
  24.    * @param time 
  25.    *            the transaction time 
  26.    */ 
  27.   public Order(int id, String code, int amount, double price, long time) { 
  28.     super(); 
  29.     this.id = id; 
  30.     this.code = code; 
  31.     this.amount = amount; 
  32.     this.price = price; 
  33.     this.time = time; 
  34.     
  35.     //这里故意设置Order对象足够大,以方便例子稍后在运行的时候耗尽内存 
  36.     this.padding = new long[3000]; 
  37.     Arrays.fill(padding, 0, padding.length - 1, -2); 
  38.   } 
  39.   
  40.   public int getId() { 
  41.     return id; 
  42.   } 
  43.   
  44.   public String getCode() { 
  45.     return code; 
  46.   } 
  47.   
  48.   public int getAmount() { 
  49.     return amount; 
  50.   } 
  51.   
  52.   public double getPrice() { 
  53.     return price; 
  54.   } 
  55.   
  56.   public long getTime() { 
  57.     return time; 
  58.   } 
  59.   

这个POJO对象是Spring应用的一部分,该应用有三个主要的抽象类,当Spring调用它们的start()方法的时候将分别创建一个新的线程。

第一个抽象类是OrderFeed。run()方法将生成一系列随机的Order对象,并将其放置在队列中,然后它会睡眠一会儿,又再接着生成一个新的Order对象,代码如下:

  1. public class OrderFeed implements Runnable { 
  2.  
  3.  private static Random rand = new Random(); 
  4.  
  5.  private static int id = 0
  6.  
  7.  private final BlockingQueue<Order> orderQueue; 
  8.  
  9.  public OrderFeed(BlockingQueue<Order> orderQueue) { 
  10.    this.orderQueue = orderQueue; 
  11.  } 
  12.  
  13.  /** 
  14.   *在加载Context上下文后由Spring调用,开始生产order对象 
  15.   */ 
  16.  public void start() { 
  17.  
  18.    Thread thread = new Thread(this"Order producer"); 
  19.    thread.start(); 
  20.  } 
  21.  
  22.   @Override 
  23.  public void run() { 
  24.  
  25.    while (true) { 
  26.      Order order = createOrder(); 
  27.      orderQueue.add(order); 
  28.      sleep(); 
  29.    } 
  30.  } 
  31.  
  32.  private Order createOrder() { 
  33.  
  34.    final String[] stocks = { "BLND.L""DGE.L""MKS.L""PSON.L""RIO.L""PRU.L"
  35.        "LSE.L""WMH.L" }; 
  36.    int next = rand.nextInt(stocks.length); 
  37.    long now = System.currentTimeMillis(); 
  38.  
  39.    Order order = new Order(++id, stocks[next], next * 100, next * 10, now); 
  40.    return order; 
  41.  } 
  42.  
  43.  private void sleep() { 
  44.    try { 
  45.      TimeUnit.MILLISECONDS.sleep(100); 
  46.    } catch (InterruptedException e) { 
  47.      e.printStackTrace(); 
  48.    } 
  49.  } 

#p#

第二个类是OrderRecord,这个类负责从队列中提取Order对象,并将它们写入数据库。问题是,将Order对象写入数据库的耗时比产生Order对象的耗时要长得多。为了演示,将在recordOrder()方法中让其睡眠1秒。

  1. public class OrderRecord implements Runnable { 
  2.  
  3.   private final BlockingQueue<Order> orderQueue; 
  4.  
  5.   public OrderRecord(BlockingQueue<Order> orderQueue) { 
  6.     this.orderQueue = orderQueue; 
  7.   } 
  8.  
  9.   public void start() { 
  10.  
  11.     Thread thread = new Thread(this"Order Recorder"); 
  12.     thread.start(); 
  13.   } 
  14.  
  15.   @Override 
  16.   public void run() { 
  17.  
  18.     while (true) { 
  19.  
  20.       try { 
  21.         Order order = orderQueue.take(); 
  22.         recordOrder(order); 
  23.       } catch (InterruptedException e) { 
  24.         e.printStackTrace(); 
  25.       } 
  26.     } 
  27.  
  28.   } 
  29.  
  30.   /** 
  31.    * 模拟记录到数据库的方法,这里只是简单让其睡眠一秒  
  32.    */ 
  33.   public void recordOrder(Order order) throws InterruptedException { 
  34.     TimeUnit.SECONDS.sleep(1); 
  35.   } 
  36.  

为了证明这个效果,特意增加了一个监视类 OrderQueueMonitor ,这个类每隔几秒就打印出队列的大小,代码如下:

  1. public class OrderQueueMonitor implements Runnable { 
  2.  
  3.   private final BlockingQueue<Order> orderQueue; 
  4.  
  5.   public OrderQueueMonitor(BlockingQueue<Order> orderQueue) { 
  6.     this.orderQueue = orderQueue; 
  7.   } 
  8.  
  9.   public void start() { 
  10.  
  11.     Thread thread = new Thread(this"Order Queue Monitor"); 
  12.     thread.start(); 
  13.   } 
  14.  
  15.   @Override 
  16.   public void run() { 
  17.  
  18.     while (true) { 
  19.  
  20.       try { 
  21.         TimeUnit.SECONDS.sleep(2); 
  22.         int size = orderQueue.size(); 
  23.         System.out.println("Queue size is:" + size); 
  24.       } catch (InterruptedException e) { 
  25.         e.printStackTrace(); 
  26.       } 
  27.     } 
  28.   } 
  29.  

接下来配置Spring框架的相关配置文件如下:

  1. <?xml version="1.0" encoding="UTF-8"?> 
  2. <beans xmlns="http://www.springframework.org/schema/beans" 
  3. xmlns:p="http://www.springframework.org/schema/p" 
  4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  5. xmlns:context="http://www.springframework.org/schema/context" 
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
  7. http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd" 
  8. default-init-method="start" 
  9. default-destroy-method="destroy"
  10.   
  11. <bean id="theQueue" class="java.util.concurrent.LinkedBlockingQueue"/>  
  12. <bean id="orderProducer"
  13. <constructor-arg ref="theQueue"/> 
  14. </bean> 
  15.   
  16. <bean id="OrderRecorder"
  17. <constructor-arg ref="theQueue"/> 
  18. </bean> 
  19.   
  20. <bean id="QueueMonitor"
  21. <constructor-arg ref="theQueue"/> 
  22. </bean> 
  23.   
  24. </beans> 

接下来运行这个Spring应用,并且可以通过jConsole去监控应用的内存情况,这需要作一些配置,配置如下:

  1. -Dcom.sun.management.jmxremote  
  2. -Dcom.sun.management.jmxremote.port=9010  
  3. -Dcom.sun.management.jmxremote.local.only=false  
  4. -Dcom.sun.management.jmxremote.authenticate=false  
  5. -Dcom.sun.management.jmxremote.ssl=false 

如果你看看堆的使用量,你会发现随着队列的增大,堆的使用量逐渐增大,如下图所示,你可能不会发现1KB的内存泄露,但当达到1GB的内存溢出就很明显了。所以,接下来要做的事情就是等待其溢出,然后进行分析。

#p#

接下来我们来看下如何发现并解决这类问题。在Java中,可以借助不少自带的或第三方的工具帮助我们进行相关的分析。

下面介绍分析程序内存泄露问题的三个步骤:

  1. 提取发生内存泄露的服务器的转储文件。
  2. 用这个转储文件生成报告。
  3. 分析生成的报告。

有几个工具能帮你生成堆转储文件,分别是:

  • jconsole
  •  visualvm
  • Eclipse Memory Analyser Tool(MAT)

用jconsole提取堆转储文件

使用jconsole连接到你的应用:单击MBeans选项卡打开com.sun.management包,点击HotSpotDiagnostic,点击Operations,然后选择dumpHeap。这时你将会看到dumpHeap操作:它接受两个参数p0和p1。在p0的编辑框内输入一个堆转储的文件名,然后按下DumpHeap按钮就可以了。如下图:

用jvisualvm提取堆转储文件

首先使用jvisual vm连接示例代码,然后右键点击应用,在左侧的“application”窗格中选择“Heap Dump”。

注意:如果需要分析的发生内存泄露的是在远程服务器上,那么jvisualvm将会把转存出来的文件保存在远程机器(假设这是一台unix机器)上的/tmp目录下。

用MAT来提取堆转储文件

jconsole和jvisualvm本身就是JDK的一部分,而MAT或被称作“内存分析工具”,是一个基于eclipse的插件,可以从eclipse.org下载。

最新版本的MAT需要你在电脑上安装JDk1.6。如果你用的是Java1.7版本也不用担心,因为它会自动为你安装1.6版本,并且不会和安装好的1.7版本产生冲突。

使用MAT的时候,只需要点击“Aquire Heap Dump”,然后按步骤操作就可以了,如下图:

要注意的是,使用上面的三种方法,都需要配置远程JMX连接如下:

  1. -Dcom.sun.management.jmxremote 
  2. -Dcom.sun.management.jmxremote.port=9010 
  3. -Dcom.sun.management.jmxremote.local.only=false 
  4. -Dcom.sun.management.jmxremote.authenticate=false 
  5. -Dcom.sun.management.jmxremote.ssl=false 

何时提取堆转存文件

那么在什么时候才应该提取堆转存文件呢?这需要耗费点心思和碰下运气。如果过早提取了堆转储文件,那么将可能不能发现问题症结所在,因为它们被合法,非泄露类的实例屏蔽了。不过也不能等太久,因为提取堆转储文件也需要占用内存,进行提取的时候可能会导致应用崩溃。

最好的办法是将jconsole连接到应用程序并监控堆的占用情况,知道它何时在崩溃的边缘。因为没有发生内存泄露时,三个堆部分指标都是绿色的,这样很容易就能监控到,如下图:

分析转储文件

现在轮到MAT派上用场了,因为它本身就是设计用来分析堆转储文件的。要打开和分析一个堆转储文件,可以选择File菜单的Heap Dump选项。选择了要打开的文件后,将会看到如下三个选项:

#p#

选择Leak Suspect Report选项。在MAT运行几秒后,会生成如下图的页面:

如饼状图显示:疑似有一处发生了内存泄露。也许你会想,这样的做法只有在代码受到控制的情况下才可取。毕竟这只是个例子,这又能说明什么呢?好吧,在这个例子里,所有的问题都是浅然易见的;线程a占用了98.7MB内存,其他线程用了1.5MB。在实际情况中,得到的图表可能是上图那样。让我们继续探究,会得到如下图:

如上图所示,报告的下一部分告诉我们,有一个LinkedBlockQueue占用了98.46%的内存。想要进一步的探究,点击Details>>就可以了,如下图:

可以看到,问题确实是出在我们的orderQueue上。这个队列里存储了所有生成的随机生成的Order对象,并且可以被我们上篇博文里提到的三个线程OrderFeed、OrderRecord、OrderMonitor访问。

那么一切都清楚了,MAT告诉我们:示例代码中有一个LinkedBlockQueue,这个队列用尽了所有的内存,从而导致了严重的问题。不过我们不知道这个问题为什么会产生,也不能指望MAT告诉我们。

本文代码可以在:https://github.com/roghughe/captaindebug/tree/master/producer-consumer中下载。

原文链接:http://www.javacodegeeks.com/2013/12/investigating-memory-leaks-part-1-writing-leaky-code.html

责任编辑:陈四芳 来源: 51CTO
相关推荐

2010-05-31 16:53:21

Java

2024-10-31 09:24:42

2020-06-23 09:48:09

Python开发内存

2010-09-25 11:32:24

Java内存泄漏

2023-06-30 23:25:46

HTTP模块内存

2013-04-09 14:49:18

Linux内存统计内存泄露

2009-06-16 11:11:07

Java内存管理Java内存泄漏

2010-09-17 16:18:43

Java内存溢出

2015-05-14 15:38:40

javajava内存泄露

2011-11-17 13:59:41

Java内存管理内存泄露

2015-05-20 16:04:22

Chrome

2015-12-07 09:39:27

Java内存泄露

2012-04-11 13:46:33

ibmdw

2017-12-11 11:00:27

内存泄露判断

2010-09-25 11:23:15

Java内存泄露

2011-06-22 13:47:16

Java多线程

2011-06-22 13:57:54

Java多线程

2019-04-04 13:11:37

React内存泄露memory leak

2021-04-14 10:14:34

JVM生产问题定位内存泄露

2017-05-04 16:07:11

Tomcat内存泄露
点赞
收藏

51CTO技术栈公众号