关于java:Java内存泄露问题分析

41次阅读

共计 6324 个字符,预计需要花费 16 分钟才能阅读完成。

最近有一个服务器,常常运行的时候就呈现过载宕机的景象。重启脚本和零碎后,该个问题还是会呈现。只管有大量的数据失落, 但因不是要害业务,问题并 不重大。不过还是决定作进一步的考察,来看下问题到底呈现在哪。首先留神到的是,服务器通过了所有的单元测试和残缺的集成环境的测试。在测试环境下应用测 试数据时运行失常, 那么为什么在生产环境中运行会呈现问题呢?很容易会想到,兴许是因为理论运行时的负载大于测试时的负载,甚至超过了设计的负荷,从而耗 尽了资源。然而到底是什么资源, 在哪里耗尽了呢? 上面咱们就钻研这个问题

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

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

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; 

// 这里成心设置 Order 对象足够大,以不便例子稍后在运行的时候耗尽内存 
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 利用的一部分,该利用有三个次要的抽象类,当 Spring 调用它们的 start() 办法的时候将别离创立一个新的线程。

http://www.developcls.com
http://www.developcls.com/qa/ef2c7b5c050b4cfaa54a0d11080077c5…

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

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;
}

/**
* 在加载 Context 上下文后由 Spring 调用,开始生产 order 对象
*/
public void start() {

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

@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,这个类负责从队列中提取 Order 对象,并将它们写入数据库。问题是,将 Order 对象写入数据库的耗时比产生 Order 对象的耗时要长得多。为了演示,将在 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(); 
  } 
} 

}

/**

  • 模仿记录到数据库的办法,这里只是简略让其睡眠一秒
    */

public void recordOrder(Order order) throws InterruptedException {

TimeUnit.SECONDS.sleep(1); 

}

}

为了证实这个成果,特意减少了一个监督类 OrderQueueMonitor,这个类每隔几秒就打印出队列的大小,代码如下:

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” class=”java.util.concurrent.LinkedBlockingQueue”/>
<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>

接下来运行这个 Spring 利用,并且能够通过 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 的内存溢出就很显著了。所以,接下来要做的事件就是期待其溢出,而后进行剖析。

接下来咱们来看下如何发现并解决这类问题。在 Java 中,能够借助不少自带的或第三方的工具帮忙咱们进行相干的剖析。

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

 提取产生内存泄露的服务器的转储文件。用这个转储文件生成报告。剖析生成的报告。

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

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 连贯如下:

-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

何时提取堆转存文件

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

最好的方法是将 jconsole 连贯到应用程序并监控堆的占用状况,晓得它何时在解体的边缘。因为没有产生内存泄露时,三个堆局部指标都是绿色的,这样很容易就能监控到,如下图:

剖析转储文件

当初轮到 MAT 派上用场了,因为它自身就是设计用来剖析堆转储文件的。要关上和剖析一个堆转储文件,能够抉择 File 菜单的 Heap Dump 选项。抉择了要关上的文件后,将会看到如下三个选项:

抉择 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 中下载。

正文完
 0