Java线程内存泄漏

标签 java multithreading memory-leaks spring-boot cloud-foundry

我正在尝试在我正在编写的程序中实现多线程以实现某些任务并行性。该程序利用Spring框架并在Pivotal Cloud Foundry上运行。它偶尔会崩溃,所以我进去查看日志和性能指标;这是我发现它有内存泄漏的时候。经过一些测试后,我将罪魁祸首的范围缩小到了我的线程实现。我对JVM中GC的理解是,它不会处置未死的线程,也不会处置任何仍在被另一个对象或后面的可执行代码行引用的对象。然而,我根本没有保留对该线程的任何引用,如果我这样做,它声称一旦完成运行就会将自己置于死亡状态,所以我不知道是什么导致了泄漏。

我已经编写了一个干净的 PoC 来演示泄漏。它使用一个休息 Controller ,这样我就可以控制线程的数量,一个可运行的类,因为我的真实程序需要参数,以及一个字符串来占用内存中的任意空间,该空间将由真实程序中的其他字段保存(使泄漏更多)明显)。

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)部署内存)。

如果我更改它以删除 stringbuilder:

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

它看起来是渐近的,但对我来说最有趣的是,当我出去参加 session 和吃午餐时,它决定自行增长 40mb。我与我的团队核实过,在那段时间没有人使用该应用程序。也不知道该怎么做

最佳答案

这是因为你不断地添加字符串。 Java不会自动GC字符串池

Java String Pool

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

使用StringBuilder相反

关于Java线程内存泄漏,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38753203/

相关文章:

c++ - 内存仍可访问的错误已修复,但为什么呢?

java - java中的扩展形式

java - 生成全对测试的测试

c++ - 如何避免与 `asio::ip::tcp::iostream` 的数据竞争?

c - 并发标志集是否需要#pragma omp atomic?

java - 共享队列 VS Actor 模型

JavaScript 关闭内存泄漏

Linux 和内存泄漏

java - 创建带参数的构造函数

java - Redis 键被自动删除