1

我正試圖在我正在編寫的程序中爲某些任務並行性實現多線程。該計劃使用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。我查看了我的團隊,在此期間沒有人使用該應用程序。不知道該怎麼做,要麼

+0

看看這篇文章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

+0

很明顯,字符串vs強大的生成器問題與是否發生內存泄漏無關。你怎麼知道你在開始泄漏?如果在之前的迭代完成之前該方法被調用太頻繁,則會導致內存不足。另一方面,如果您仍有空閒內存,即使某些對象是可收集的,也沒有理由開始GC收集。這看起來不像任何地方都有內存泄漏。 – Voo

+0

@Voo如果我運行應用程序PCF報告使用約400mb內存。如果我告訴它啓動幾千個線程,內存使用量將增加到450MB。如果我在幾個小時後檢查它,它仍然在450mb –

回答

0

這是因爲你不斷添加字符串。 Java那樣自動

Java String Pool

String spaceWaster = ""; 
      for (int i = 0; i < 10000; i ++) 
       spaceWaster += "W"; 

使用StringBuilder沒有GC字符串池,而不是

+0

一旦循環結束,雖然它應該完成該方法,使用spaceWaster完成,並且處理該字段,而不處理該字段。StringBuilder不會有所作爲,我可以用「int [] spacewaster2 = new int [1000000]」替換spaceWaster,並且泄漏仍然存在 –

+0

在Java中實現的唯一字符串是文字(本例中爲「W」和空白字符串)或你顯式調用intern的字符串。其他一切都不是因爲明顯的原因。 – Voo

-1

使用stringbuilder是正確的

不認爲你需要2000多個線程。

對於任務(字符串/文檔)和thread pool來說,更好的設計可以是A Queue來處理字符串/文檔。

+0

我同意threadpooling會比使用AtomicInteger跟蹤運行的線程數更好,但是我沒有使用Java中的池的經驗,並且我目前正在執行比生產代碼更多的PoC。該程序實際上是從隊列中讀取並旋轉線程來處理隊列。我一直在線程化的全部原因是因爲Azure文檔數據庫(一種NoSQL產品)花費了無法接受的時間來添加新記錄,但同時可以很好地擴展到多個調用。 –

+0

好的,我明白了。取決於你的字符串,它可能直接到'Permanent Generation',而不是'eden space'。您需要調整jvm參數 – user3644708

+0

沒有動態分配的字符串不會進入永久生成,因此完全沒有理由調整任何參數。地獄甚至沒有實習期間的字符串結束了一段時間。 – Voo