探究内存泄露—Part1—编写泄露代码

几天前我发现了一个小问题:有一个服务器在跑了一段时间后挂掉了。重启脚本和系统后,这个问题还是会出现。因为问题代码不是关键业务,所以尽管有大量的数据丢失,但是问题并不严重。不过我还是决定作进一步的调查,来探寻一下问题到底出现在哪。首先注意到的是,服务器通过了所有的单元测试和集成环境的完整测试。在测试环境下使用测试数据时运行得非常正常。那么为什么在工作环境中一跑起来就会出现问题呢?很容易就能想到,也许是因为在实际运行时的负载大于测试,甚至超过了设计时所能承载的负重,从而耗尽了资源。但是到底是什么资源,又是在哪里耗尽的呢?这就是本文需要探究的难题。

为了演示如何调查这个问题,第一件事情就是写一些内存泄露的代码。我将会采用生产者—消费者模型,以便更好的说明这个问题。

和往常一样,为了说明内存泄露代码,我需要人为建立一个场景。在这个场景中,假定你为一个证劵经纪公司工作,这个公司将股票的销售额和股份记录在一个数据库中。通过一个简单进程获取命令并将其存放在一个队列中。另一个进程从该队列中读取命令并将其写入数据库。命令的POJO(简单Java对象)非常的直观:

public class Order {

  private final int id;

  private final String code;

  private final int amount;

  private final double price;

  private final long time;

  private final long[] padding;

  /**
   * @param id
   *            The order id
   * @param code
   *            The stock code
   * @param amount
   *            the number of shares
   * @param price
   *            the price of the share
   * @param time
   *            the transaction time
   */
  public Order(int id, String code, int amount, double price, long time) {
    super();
    this.id = id;
    this.code = code;
    this.amount = amount;
    this.price = price;
    this.time = time;
    // This just makes the Order object bigger so that
    // the example runs out of heap more quickly.
    this.padding = new long[3000];
    Arrays.fill(padding, 0, padding.length - 1, -2);
  }

  public int getId() {
    return id;
  }

  public String getCode() {
    return code;
  }

  public int getAmount() {
    return amount;
  }

  public double getPrice() {
    return price;
  }

  public long getTime() {
    return time;
  }

}

这个命令POJO是Spring应用的一部分。这个应用有三个主要的抽象类,当应用调用他们的start()方法时分别创建一个新进程。

第一个抽象类是OrderFeed。run()方法会生成一个虚拟的命令并将其放置在队列中。生成命令后它会睡眠一会儿,然后生成一个新的命令。

public class OrderFeed implements Runnable {

private static Random rand = new Random();

private static int id = 0;

private final BlockingQueue<Order> orderQueue;

public OrderFeed(BlockingQueue<Order> orderQueue) {
this.orderQueue = orderQueue;
}

/**
* Called by Spring after loading the context. Start producing orders
*/
public void start() {

Thread thread = new Thread(this, "Order producer");
thread.start();
}

/** The main run loop */
@Override
public void run() {

while (true) {
Order order = createOrder();
orderQueue.add(order);
sleep();
}
}

private Order createOrder() {

final String[] stocks = { "BLND.L", "DGE.L", "MKS.L", "PSON.L", "RIO.L", "PRU.L",
"LSE.L", "WMH.L" };
int next = rand.nextInt(stocks.length);
long now = System.currentTimeMillis();

Order order = new Order(++id, stocks[next], next * 100, next * 10, now);
return order;
}

private void sleep() {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

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

public class OrderRecord implements Runnable {

private final BlockingQueue<Order> orderQueue;

public OrderRecord(BlockingQueue<Order> orderQueue) {
this.orderQueue = orderQueue;
}

public void start() {

Thread thread = new Thread(this, "Order Recorder");
thread.start();
}

@Override
public void run() {

while (true) {

try {
Order order = orderQueue.take();
recordOrder(order);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

/**
* Record the order in the database
*
* This is a dummy method
*
* @param order
*            The order
* @throws InterruptedException
*/
public void recordOrder(Order order) throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
}

结果将是显而易见的,OrderRecord线程跟不上命令产生的速度,导致这个队列越来越长,直到JAVA虚拟机用尽堆内存从而崩溃。这就是生产者—消费者模式的存在一个大问题:消费者的速度必须跟上生产者的速度。

为了证明这一点,我加入了第三个类OrderMonitor。这个类每隔几秒就会打印出队列的大小,这样就能看到运行时产生的问题。

public class OrderQueueMonitor implements Runnable {
    private final BlockingQueue<Order> orderQueue;
    public OrderQueueMonitor(BlockingQueue<Order> orderQueue) { 
        this.orderQueue = orderQueue; 
    }
    public void start() {
       Thread thread = new Thread(this, "Order Queue Monitor");
       thread.start();
    }
    @Override public void run() {
       while (true) {
          try {
            TimeUnit.SECONDS.sleep(2);
            int size = orderQueue.size();
            System.out.println("Queue size is:" + size);
          } catch (InterruptedException e) {
            e.printStackTrace();
         }
    } 
}

为了完成Spring框架,我加入了应用上下文,示例代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd"
default-init-method="start"
default-destroy-method="destroy">

<bean id="theQueue"/>

<bean id="orderProducer">
<constructor-arg ref="theQueue"/>
</bean>

<bean id="OrderRecorder">
<constructor-arg ref="theQueue"/>
</bean>

<bean id="QueueMonitor">
<constructor-arg ref="theQueue"/>
</bean>

</beans>

下一步就是把这个内存泄露的代码跑起来,你需要改变下面的目录:

<your-path>/git/captaindebug/producer-consumer/target/classes

然后输入下面的命令:

java -cp /path-to/spring-beans-3.2.3.RELEASE.jar:/path-to/spring-context-3.2.3.RELEASE.jar:/path-to/spring-core-3.2.3.RELEASE.jar:/path-to/slf4j-api-1.6.1-javadoc.jar:/path-to/commons-logging-1.1.1.jar:/path-to/spring-expression-3.2.3.RELEASE.jar:. com.captaindebug.producerconsumer.problem.Main

“path-to”对应着你的jar文件目录。

Java比较讨厌的一点是,从命令行来运行程序非常的困难——你必须要搞清楚类的目录、选项、需要设定的属性、main所在的类在哪里。当然,有方法能让你只需要输入Java的项目名称,然后Java虚拟机帮你把一切都搞定,特别是使用默认设置:这有多难呢?

你也可以通过附加一个简单的JConsole来监控应用程序的内存泄漏。如果你最近运行过,则需要在上面的命令行中添加如下的选项(选择自己的端口号):

-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=9010 
-Dcom.sun.management.jmxremote.local.only=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false

如果你看看堆的使用量,你会发现随着队列的增大堆逐渐变大。

你可能不会发现1KB的内存泄露,但1GB的内存泄露就很明显了。所以,接下来要做的事情就是等待内存的泄露直到进入下一个阶段的研究。下回见……

原文链接: captaindebug 翻译: ImportNew.com - 黄索远
译文链接: http://www.importnew.com/7807.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: 黄索远

(新浪微博:@黄索远

查看黄索远的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

6 条评论

  1. dennis.lee 说道:

    管这种交内存泄露?太肤浅了

    Well-loved. Like or Dislike: Thumb up 4 Thumb down 0

  2. dennis.lee 说道:

    补充一下,LZ 这贴的情况属于内存溢出。内存溢出与内存泄露最大的区别的就是内存中的对象是否代码逻辑可达。这种消费者速度赶不上生产者速度的情况,在提高消费速度的时候所有对象都是代码逻辑可达的,属于内存溢出。内存泄漏比这个复杂多了

    Thumb up 1 Thumb down 0

  3. OneCoder 说道:

    这只是内存溢出,不是泄漏。

    Thumb up 1 Thumb down 0

  4. mikan 说道:

    把Order翻译成命令,有点不妥吧?

    Thumb up 0 Thumb down 0

  5. Jerry Ler 说道:

    可以把demo代码放到github上,方便clone下来可自己运行看效果:)

    Thumb up 0 Thumb down 0

  6. flying5 说道:

    用固定大小的阻塞队列就啥事都没了

    队列满了,生产者阻塞就可以了

    Thumb up 0 Thumb down 0

跳到底部
返回顶部