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