我正試圖在我正在編寫的程序中爲某些任務並行性實現多線程。該計劃使用Spring框架並在Pivotal Cloud Foundry上運行。它偶爾會崩潰,所以我進去查看了日誌和性能指標;這是當我發現它有內存泄漏。在進行一些測試後,我將線人的實施範圍縮小到了罪魁禍首。我對JVM中的GC的理解是,它不會處理未死的線程,也不會處理任何仍在被另一個對象或後面的可執行代碼行引用的對象。然而,我並沒有對線程進行任何引用,如果我這樣做,它聲稱一旦它完成運行就將自己置於死亡狀態,所以我不知道是什麼導致了泄漏。Java線程內存泄漏
我寫了一個乾淨的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");
}
}
}
任何人都可以解釋爲什麼這個程序泄漏內存?
編輯:我已經得到了關於字符串賦值VS串建設和字符串池中的幾個答覆,所以我改變了我的代碼以下
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過去的原始部署內存)的內存增長。
如果我改變它來除去的StringBuilder:
String temp = "";
for(int j = 0; j< 100; j++)
temp += characters[rng.nextInt(36)];
new Thread(new MyRunnable(temp)).start();
它泄漏無限期;我假設所有36^100字符串一旦產生就會停止。
結合這些發現我猜我的真正問題可能是字符串池的問題,以及春天如何分配內存的問題。我仍然不明白的是,在我的真實應用程序中,如果我在主線程上創建一個runnable並調用run(),內存似乎不會突然增加,但如果我創建一個新線程並給它一個runnable,那麼內存跳轉。繼承人什麼我可以運行看起來像當前在應用程序我建立:
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。我查看了我的團隊,在此期間沒有人使用該應用程序。不知道該怎麼做,要麼
看看這篇文章http://stackoverflow.com/questions/65668/why-to-use-stringbuffer-in-java-instead-of-the-string-concatenation-operator以及http:// stackoverflow.com/questions/18406703/when-will-a-string-be-garbage-collected-in-java – JavaHopper
很明顯,字符串vs強大的生成器問題與是否發生內存泄漏無關。你怎麼知道你在開始泄漏?如果在之前的迭代完成之前該方法被調用太頻繁,則會導致內存不足。另一方面,如果您仍有空閒內存,即使某些對象是可收集的,也沒有理由開始GC收集。這看起來不像任何地方都有內存泄漏。 – Voo
@Voo如果我運行應用程序PCF報告使用約400mb內存。如果我告訴它啓動幾千個線程,內存使用量將增加到450MB。如果我在幾個小時後檢查它,它仍然在450mb –