【发布时间】:2016-12-09 17:49:03
【问题描述】:
我正在尝试在我正在编写的程序中为某些任务并行性实现多线程。该程序利用 Spring 框架并在 Pivotal Cloud Foundry 上运行。它偶尔会崩溃,所以我进去查看了日志和性能指标;那是我发现它有内存泄漏的时候。经过一些测试,我缩小了我的线程实现的罪魁祸首。我对 JVM 中的 GC 的理解是,它不会处理未死的线程,也不会处理仍然被另一个对象或后面的可执行代码行引用的任何对象。但是,我根本没有对线程的任何引用,如果我这样做了,它声称一旦它完成运行就会将自己置于死状态,所以我不知道是什么导致了泄漏。
我编写了一个干净的 PoC 来演示泄漏。它使用了一个休息控制器,所以我可以控制线程的数量,一个可运行的类,因为我的真实程序需要参数,以及一个占用内存中任意空间的字符串,该空间将由真实程序中的其他字段持有(使泄漏更多明显)。
package com.example;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LeakController {
@RequestMapping("/Run")
public String DoWork(@RequestParam("Amount") int amount, @RequestParam("Args") String args)
{
for(int i = 0; i < amount; i++)
new Thread(new MyRunnable(args)).start();
return "Workin' on it";
}
public class MyRunnable implements Runnable{
String args;
public MyRunnable(String args){ this.args = args; }
public void run()
{
int timeToSleep = Integer.valueOf(args);
String spaceWaster = "";
for (int i = 0; i < 10000; i ++)
spaceWaster += "W";
System.out.println(spaceWaster);
try {Thread.sleep(timeToSleep);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("Done");
}
}
}
谁能解释一下为什么这个程序会泄漏内存?
编辑:我收到了一些关于字符串分配与字符串构建和字符串池的回复,所以我将代码更改为以下内容
int[] spaceWaster = new int[10000];
for (int i = 0; i < 10000; i ++)
spaceWaster[i] = 512;
System.out.println(spaceWaster[1]);
它仍然会泄漏。
编辑:在获取一些实数来回应 Voo 时,我注意到了一些有趣的事情。调用新线程开始消耗内存,但只是到了一定程度。在永久增长约 60mb 后,新的基于整数的程序将停止进一步增长,无论它如何推动。这是否与spring框架分配内存的方式有关?
我还认为回到 String 示例是有好处的,因为它与我的实际用例更密切相关;这是对传入的 JSON 进行正则表达式操作,每秒数百个这样的 JSON。考虑到这一点,我将代码更改为:
@RestController
public class LeakController {
public static String characters[] = {
"1","2","3","4","5","6","7","8","9","0",
"A","B","C","D","E","F","G","H","I","J","K","L","M",
"N","O","P","Q","R","S","T","U","V","W","X","Y","Z"};
public Random rng = new Random();
@RequestMapping("/Run")
public String GenerateAndSend(@RequestParam("Amount") int amount)
{
for(int i = 0; i < amount; i++)
{
StringBuilder sb = new StringBuilder(100);
for(int j = 0; j< 100; j++)
sb.append(characters[rng.nextInt(36)]);
new Thread(new MyRunnable(sb.toString())).start();
System.out.println("Thread " + i + " created");
}
System.out.println("Done making threads");
return "Workin' on it";
}
public class MyRunnable implements Runnable{
String args;
public MyRunnable(String args){ this.args = args; }
public void run()
{
System.out.println(args);
args = args.replaceAll("\\d+", "\\[Number was here\\]");
System.out.println(args);
}
}
}
这个新应用程序表现出与整数示例类似的行为,因为它永久增长了大约 50mb(在 2000 个线程之后),并且从那里逐渐减小,直到我无法注意到每批新的 1000 个线程有任何内存增长(大约 85mb 超过原来的部署内存)。
如果我将其更改为删除字符串生成器:
String temp = "";
for(int j = 0; j< 100; j++)
temp += characters[rng.nextInt(36)];
new Thread(new MyRunnable(temp)).start();
它无限期地泄漏;我假设一旦生成了所有 36^100 个字符串,它就会停止。
结合这些发现,我想我的真正问题可能是字符串池的问题和 spring 如何分配内存的问题。我仍然无法理解的是,在我的实际应用程序中,如果我在主线程上创建一个可运行并调用 run(),内存似乎不会激增,但是如果我创建一个新线程并给它可运行,那么内存会跳转.这是我正在构建的应用程序中我的可运行文件当前的样子:
public class MyRunnable implements Runnable{
String json;
public MyRunnable(String json){
this.json = new String(json);
}
public void run()
{
DocumentClient documentClient = new DocumentClient (END_POINT,
MASTER_KEY, ConnectionPolicy.GetDefault(),
ConsistencyLevel.Session);
System.out.println("JSON : " + json);
Document myDocument = new Document(json);
System.out.println(new DateTime().toString(DateTimeFormat.forPattern("MM-dd-yyyy>HH:mm:ss.SSS"))+">"+"Created JSON Document Locally");
// Create a new document
try {
//collectioncache is a variable in the parent restcontroller class that this class is declared inside of
System.out.println("CollectionExists:" + collectionCache != null);
System.out.println("CollectionLink:" + collectionCache.getSelfLink());
System.out.println(new DateTime().toString(DateTimeFormat.forPattern("MM-dd-yyyy>HH:mm:ss.SSS"))+">"+"Creating Document on DocDB");
documentClient.createDocument(collectionCache.getSelfLink(), myDocument, null, false);
System.out.println(new DateTime().toString(DateTimeFormat.forPattern("MM-dd-yyyy>HH:mm:ss.SSS"))+">"+"Document Creation Successful");
System.out.flush();
currentThreads.decrementAndGet();
} catch (DocumentClientException e) {
System.out.println("Failed to Upload Document");
e.printStackTrace();
}
}
}
任何想法我真正的泄漏在哪里?有什么地方我需要一个字符串生成器吗?字符串只是让记忆变得有趣吗,我需要给它一个更高的上限来伸展它会好吗?
编辑:我做了一些基准测试,因此我可以实际绘制行为图,以便更好地了解 GC 正在做什么
00000 Threads - 457 MB
01000 Threads - 535 MB
02000 Threads - 545 MB
03000 Threads - 549 MB
04000 Threads - 551 MB
05000 Threads - 555 MB
2 hours later - 595 MB
06000 Threads - 598 MB
07000 Threads - 600 MB
08000 Threads - 602 MB
这似乎是渐近的,但对我来说最有趣的是,当我出去参加会议和吃午饭时,它决定自己增长 40mb。我与我的团队核实,在那段时间没有人使用该应用程序。也不知道该怎么做
【问题讨论】:
-
很明显,字符串与强构建器的问题与是否发生内存泄漏无关。你怎么知道你一开始就有泄漏?如果在之前的迭代完成之前调用该方法过于频繁,您将耗尽内存。另一方面,如果您仍有可用内存,则即使某些对象是可收集的,也没有理由启动 GC 收集。这似乎没有任何地方存在内存泄漏。
-
@Voo 如果我运行应用程序 PCF 报告大约使用了 400mb 内存。如果我告诉它启动几千个线程,那么内存使用量会上升到 450mb。如果我几个小时后检查它,它仍然是 450mb
-
这不一定是内存泄漏,只是GC决定在没有内存压力的情况下不收集任何内存。
-
@Voo 很有趣,所以 GC 在 java 中是惰性的?我来自 .NET 领域,我从未见过这种行为。您是否有任何详细说明 Java GC 行为的链接,您认为可以很好地解释它,还是我应该深入研究 oracle 的东西?
标签: java multithreading memory-leaks spring-boot cloud-foundry