Java在释放线程时会从堆中泄漏内存。

6

我有一个小应用程序,它分配了大约25000个线程,然后释放它们。当线程被释放时,应用程序的内存消耗会增加,并且即使所有线程退出后,内存消耗仍然很高。

top 的显示如下:

 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
9133 root      20   0 22.601g 8.612g  12080 S   0.0  9.1   1:18.61 java

当执行 jmap -heap 命令时,输出结果如下:

Attaching to process ID 9133, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.66-b17

using thread-local object allocation.
Parallel GC with 18 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 104857600 (100.0MB)
   MaxNewSize               = 104857600 (100.0MB)
   OldSize                  = 968884224 (924.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 1073741824 (1024.0MB)
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 78643200 (75.0MB)
   used     = 1572864 (1.5MB)
   free     = 77070336 (73.5MB)
   2.0% used
From Space:
   capacity = 13107200 (12.5MB)
   used     = 0 (0.0MB)
   free     = 13107200 (12.5MB)
   0.0% used
To Space:
   capacity = 13107200 (12.5MB)
   used     = 0 (0.0MB)
   free     = 13107200 (12.5MB)
   0.0% used
PS Old Generation
   capacity = 968884224 (924.0MB)
   used     = 1264416 (1.205841064453125MB)
   free     = 967619808 (922.7941589355469MB)
   0.13050227970271916% used

808 interned Strings occupying 54648 bytes.

据我所见,jmap报告中没有任何内容可以解释top报告的8.612g。
Java版本为Oracle 1.8.0_66。
应用程序运行在Red Hat Enterprise Linux Server release 7.1 (Maipo)上。
以下是应用程序代码:
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

public class WaitTest {
    static AtomicInteger ai = new AtomicInteger(0);

    public static void main(String[] args) {
        List<WaitingThread> threads = new LinkedList<>();
        while (true) {
            System.out.println("number of threads: " + ai.get());
            String s = System.console().readLine();
            if (s == null)
                System.exit(0);
            s = s.trim();
            if (s.isEmpty())
                continue;
            char command = s.charAt(0);
            if (command != '+' && command != '-') {
                System.out.println("+ or - please");
                continue;
            }
            String num = s.substring(1);
            int iNum;
            try {
                iNum = Integer.parseInt(num.trim());
            } catch (Exception ex) {
                System.out.println("valid number please");
                continue;
            }
            if (command == '+') {
                for (int i = 0; i < iNum; i++) {
                    WaitingThread t = new WaitingThread();
                    t.start();
                    threads.add(t);
                }
            }
            if (command == '-') {
                Set<WaitingThread> threadsToJoin = new HashSet<>();
                for (Iterator<WaitingThread> it = threads.iterator(); it.hasNext(); ) {
                    if (iNum > 0) {
                        WaitingThread t = it.next();
                        threadsToJoin.add(t);

                        synchronized (t.lock) {
                            t.lock.notify();
                        }

                        it.remove();
                        iNum--;
                    } else
                        break;

                }
                for (WaitingThread t : threadsToJoin)
                    try {
                        t.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            }
            System.gc();
        }
    }

    static class WaitingThread extends Thread {
        public final Object lock = new Object();

        @Override
        public void run() {
            ai.incrementAndGet();
            try {
                deepStack(200);
            } catch (InterruptedException ex) {
            } catch (Exception ex) {
                System.out.println("exception in thread " + Thread.currentThread().getName() + ", " + ex.getMessage());
            } finally {
                ai.decrementAndGet();
            }
        }

        private void deepStack(int depth) throws InterruptedException {
            if (depth == 0) {
                synchronized (lock) {
                    lock.wait();
                }

            } else
                deepStack(depth - 1);
        }
    }

}

以下是您需要翻译的内容:

这里是pmap输出的链接:

这里是jmap -dump(hprof)的链接


1
你必须获取你的进程的堆转储。 - Fran Montero
1
请发布pmap PID的输出。请参见http://man7.org/linux/man-pages/man1/pmap.1.html。 - Andrew Henle
你正在将所有线程的引用保存在threadsToJoin中,即使线程完成后它们仍然会存在于内存中... - nafas
nafas,threadsToJoin是局部变量,并且在以if(command ==' - ')开头的块开始退出后从内存中删除。 - Amir
JVM不会将内存释放回操作系统。 - user207421
3个回答

0
应用程序内存只与堆大小松散耦合。当垃圾回收运行并清理时,它清理的是堆而不是系统内存。实际上,虚拟机甚至可能不会执行系统调用以减少系统内存,即使它经常释放堆空间。

0

每个线程都需要一个堆栈(分配的内存),它不是Java堆的一部分。

堆栈的默认大小取决于JVM版本、操作系统等等。

您可以使用-Xss JVM参数进行调整。

25000个线程可以轻松解释您的8.6g。

根据Amir的评论更新:

不确定,但查看您的pmap日志,您可能在glibc的竞技场分配器中遇到问题。

请查看所有64Mb的分配情况。

检查您的glibc版本是否大于2.10。

尝试使用环境变量MALLOC_ARENA_MAX=2运行程序。您可以调整实际值。


谢谢,benbenw, 但问题是为什么应用程序的内存消耗在所有线程退出后仍然保持高水平。 - Amir
我通过执行/lib/libc.so.6检查了我的glibc版本。 它是2.17。我使用MALLOC_ARENA_MAX=1运行了我的程序, 然后使用MALLOC_ARENA_MAX=2, 然后使用MALLOC_ARENA_MAX=10, 然后使用MALLOC_ARENA_MAX=1000。结果相同。 - Amir
你是怎么设置的?用 -D MALLOC_ARENA_MAX=xx 还是用操作系统环境变量 export MALLOC_ARENA_MAX=xx? - benbenw
导出 MALLOC_ARENA_MAX=xx - Amir
嗨,Amir!我在RHEL7.4上使用glibc版本-2.17时遇到了巨大的Java内存问题。我尝试了不同的MALLOC_ARENA_MAX值(4和2),但结果都一样。你找到解决这个问题的方法了吗? - Sergey

0

Kelly,列表变量“threads”在“-”命令期间被清除。此外,您可以在附加的hprof文件中看到,“threads”列表完全为空。 - Amir
你能否切换到使用ArrayList,看看是否会影响内存使用? - Kelly S. French
当然。 已切换到ArrayList。 对内存使用没有显著影响。 - Amir

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接