作者:BB15107669916 | 来源:互联网 | 2023-09-09 13:40
本文由 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 | public class OrderFeed implements Runnable { private static Random rand = new Random(); private static int id = 0 ; private final BlockingQueue orderQueue; public OrderFeed(BlockingQueue 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秒。
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 | public class OrderRecord implements Runnable { private final BlockingQueue orderQueue; public OrderRecord(BlockingQueue 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。这个类每隔几秒就会打印出队列的大小,这样就能看到运行时产生的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class OrderQueueMonitor implements Runnable { private final BlockingQueue orderQueue; public OrderQueueMonitor(BlockingQueue 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框架,我加入了应用上下文,示例代码如下:
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 |
xml version &#61; "1.0" encoding &#61; "UTF-8" ?> < beans xmlns &#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" > < bean id &#61; "theQueue" /> < bean id &#61; "orderProducer" > < constructor-arg ref &#61; "theQueue" />
bean > < bean id &#61; "OrderRecorder" > < constructor-arg ref &#61; "theQueue" />
bean > < bean id &#61; "QueueMonitor" > < constructor-arg ref &#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