热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

本文由ImportNew-黄索远翻译自captaindebug。如需转载本文,请先参见文章末尾处的转载要求。ImportNew注:如果你也对Java技术
本文由 ImportNew - 黄索远 翻译自 captaindebug。如需转载本文,请先参见文章末尾处的转载要求。

ImportNew注:如果你也对Java技术翻译分享感兴趣,欢迎加入我们的 Java开发 小组。参与方式请查看小组简介。

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

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

和往常一样,为了说明内存泄露代码,我需要人为建立一个场景。在这个场景中,假定你为一个证劵经纪公司工作,这个公司将股票的销售额和股份记录在一个数据库中。通过一个简单进程获取命令并将其存放在一个队列中。另一个进程从该队列中读取命令并将其写入数据库。命令的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()方法会生成一个虚拟的命令并将其放置在队列中。生成命令后它会睡眠一会儿,然后生成一个新的命令。


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
39
40
41
42
43
44
45
46
47
48
49
50
51
publicclassOrderFeedimplementsRunnable {
privatestaticRandom rand = newRandom();
privatestaticintid = 0;
privatefinalBlockingQueue orderQueue;
publicOrderFeed(BlockingQueue orderQueue) {
this.orderQueue = orderQueue;
}
/**
* Called by Spring after loading the context. Start producing orders
*/
publicvoidstart() {
Thread thread = newThread(this,"Order producer");
thread.start();
}
/** The main run loop */
@Override
publicvoidrun() {
while(true) {
Order order = createOrder();
orderQueue.add(order);
sleep();
}
}
privateOrder createOrder() {
finalString[] stocks = { "BLND.L","DGE.L","MKS.L","PSON.L","RIO.L","PRU.L",
"LSE.L","WMH.L"};
intnext = rand.nextInt(stocks.length);
longnow = System.currentTimeMillis();
Order order = newOrder(++id, stocks[next], next * 100, next * 10, now);
returnorder;
}
privatevoidsleep() {
try{
TimeUnit.MILLISECONDS.sleep(100);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}

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


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
39
40
41
publicclassOrderRecordimplementsRunnable {
privatefinalBlockingQueue orderQueue;
publicOrderRecord(BlockingQueue orderQueue) {
this.orderQueue = orderQueue;
}
publicvoidstart() {
Thread thread = newThread(this,"Order Recorder");
thread.start();
}
@Override
publicvoidrun() {
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
*/
publicvoidrecordOrder(Order order) throwsInterruptedException {
TimeUnit.SECONDS.sleep(1);
}

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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
publicclassOrderQueueMonitorimplementsRunnable {
    privatefinalBlockingQueue orderQueue;
    publicOrderQueueMonitor(BlockingQueue orderQueue) {
        this.orderQueue = orderQueue;
    }
    publicvoidstart() {
       Thread thread = newThread(this,"Order Queue Monitor");
       thread.start();
    }
    @Overridepublicvoidrun() {
       while(true) {
          try{
            TimeUnit.SECONDS.sleep(2);
            intsize = orderQueue.size();
            System.out.println("Queue size is:" + size);
          }catch(InterruptedException e) {
            e.printStackTrace();
         }
    }
}

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


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
xmlversion="1.0"encoding="UTF-8"?>
<beansxmlns&#61;"http://www.springframework.org/schema/beans"
xmlns:p&#61;"http://www.springframework.org/schema/p"
xmlns:xsi&#61;"http://www.w3.org/2001/XMLSchema-instance"
xmlns:context&#61;"http://www.springframework.org/schema/context"
xsi:schemaLocation&#61;"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&#61;"start"
default-destroy-method&#61;"destroy">
<beanid&#61;"theQueue"/>
<beanid&#61;"orderProducer">
<constructor-argref&#61;"theQueue"/>
bean>
<beanid&#61;"OrderRecorder">
<constructor-argref&#61;"theQueue"/>
bean>
<beanid&#61;"QueueMonitor">
<constructor-argref&#61;"theQueue"/>
bean>
beans>

下一步就是把这个内存泄露的代码跑起来&#xff0c;你需要改变下面的目录&#xff1a;


1
/git/captaindebug/producer-consumer/target/classes

然后输入下面的命令&#xff1a;


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


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


1
2
3
4
5
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port&#61;9010
-Dcom.sun.management.jmxremote.local.only&#61;false
-Dcom.sun.management.jmxremote.authenticate&#61;false
-Dcom.sun.management.jmxremote.ssl&#61;false

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


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

原文链接&#xff1a; captaindebug 翻译&#xff1a; ImportNew.com 黄索远
译文链接&#xff1a; http://www.importnew.com/7807.html


推荐阅读
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • Java SE从入门到放弃(三)的逻辑运算符详解
    本文详细介绍了Java SE中的逻辑运算符,包括逻辑运算符的操作和运算结果,以及与运算符的不同之处。通过代码演示,展示了逻辑运算符的使用方法和注意事项。文章以Java SE从入门到放弃(三)为背景,对逻辑运算符进行了深入的解析。 ... [详细]
  • 开发笔记:spring boot项目打成war包部署到服务器的步骤与注意事项
    本文介绍了将spring boot项目打成war包并部署到服务器的步骤与注意事项。通过本文的学习,读者可以了解到如何将spring boot项目打包成war包,并成功地部署到服务器上。 ... [详细]
author-avatar
BB15107669916
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有