You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

glibc malloc非主内存池内存归还至操作系统的问题问询

问题背景

我在Java中模拟了一个触发glibc严重内存碎片的场景,具体如下:

  • 模拟600线程的多线程环境;
  • 每秒启动2个新线程,通过java.nio.channels.FileChannel读取大文件;
  • 线程执行10秒后自动终止,交由JVM GC清理资源。
观察到的现象

程序运行过程中持续占用**非主内存池(non-main arena)**的内存,直至达到内存池上限后用量趋于稳定。但当所有工作线程停止、仅保留主线程休眠时,进程的RSS(驻留集大小)始终处于高位,并未下降:

  • JVM Native Memory Tracking(NMT)报告显示JVM本身并未占用这么多内存;
  • Linux pmap和smaps工具显示,非主内存池的每个64MB块中约有8MB物理内存被占用,且内存内容均为文件数据。

特别值得注意的是glibc参数的影响:设置MALLOC_TRIM_THRESHOLD_环境变量后,无论取值如何,RSS都会开始回收。由于读取文件使用的DirectBuffer为8MB,我分别设置了52428800(50MB)和8388608(8MB),每次进程RSS均回收约16MB。通过gdb查看ptmalloc2分配器的trim_threshold参数,默认值约为48MB,但设置该环境变量后,内存回收行为完全改变,尤其是非主内存池的内存。

补充验证:每次文件读取时,进程调用malloc申请堆外内存,线程销毁后由GC线程触发free操作;JVM dump确认所有对象(包括堆外DirectBuffer)均已被正确销毁。

核心问题
  1. 为何MALLOC_TRIM_THRESHOLD_参数会影响内存用量的下降?
  2. 当设置为50MB时,理论上未达到50MB可回收阈值,glibc仍会将非主内存池的内存归还操作系统,是否设置该参数后glibc的free行为发生了改变?
环境与配置
  • JDK版本:21-openjdk
  • glibc版本:2.39
  • JVM运行参数:
    -Xms6g -Xmx6g -XX:MaxDirectMemorySize=5g -XX:+AlwaysPreTouch -XX:+UseG1GC -Xlog:gc:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100M
    
测试代码
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

public class FileRead1 {

    private static volatile boolean stop = false;

    public static void main(String[] args) throws InterruptedException {

        liveThreadLoopWithoutMemory();
        

        int count = 0;
        while (true) {
            if (count > (Integer.MAX_VALUE -1) ) {
                break;
            }
            count++;
            try {
                for (int i = 0; i < 2; i++) {
                    newThread();
                }

                for (int i = 0; i < 5; i++) {
                    Thread.sleep(1000L);
                }
            } catch (InterruptedException e) {
            }
        }

        stop = true;

        while (true) {
            for (int i = 0; i < 5; i++) {
                Thread.sleep(1000L);
            }
        }
    }

    private static void liveThreadLoopWithoutMemory() {
        for (int i = 0; i < 600; i++) {
            new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    if (stop) {
                        break;
                    }
                }
            }).start();
        }
    }

    private static void newThread() {
        Thread thread = new Thread(new Runnable() {

            private final List<String> strings = new ArrayList<>();

            @Override
            public void run() {

                test(strings, "test.txt", "UTF-8", false);

                strings.clear();
            }
        });
        thread.setDaemon(true);
        thread.start();
    }

    private static void test(List<String> strings, String src, String encoding, boolean hasTitle) {
        test1(strings, src, encoding, hasTitle);
    }

    private static void test1(List<String> strings, String src, String encoding, boolean hasTitle) {
        test2(strings, src, encoding, hasTitle);

    }

    private static void test2(List<String> strings, String src, String encoding, boolean hasTitle) {
        test3(strings, src, encoding, hasTitle);
    }

    private static void test3(List<String> strings, String src, String encoding, boolean hasTitle) {
        test4(strings, src, encoding, hasTitle);

    }

    private static void test4(List<String> strings, String src, String encoding, boolean hasTitle) {
        test5(strings, src, encoding, hasTitle);
    }

    private static void test5(List<String> strings, String src, String encoding, boolean hasTitle) {
        test6(strings, src, encoding, hasTitle);

    }


    private static void test6(List<String> strings, String src, String encoding, boolean hasTitle) {
        test7(strings, src, encoding, hasTitle);
    }

    private static void test7(List<String> strings, String src, String encoding, boolean hasTitle) {
        test8(strings, src, encoding, hasTitle);
    }

    private static void test8(List<String> strings, String src, String encoding, boolean hasTitle) {
        test9(strings, src, encoding, hasTitle);
    }

    private static void test9(List<String> strings, String src, String encoding, boolean hasTitle) {
        test10(strings, src, encoding, hasTitle);
    }

    private static void test10(List<String> strings, String src, String encoding, boolean hasTitle) {
        new TaskReadFile() {
            @Override
            public void process(String str) {
                super.process(str + "swk");
            }
        }.readFile(strings, src, encoding, hasTitle);
    }


    static class TaskReadFile implements ReadFileInterface {


        private final List<String> stringList = new ArrayList<>();

        @Override
        public void process(String str) {
            stringList.add(str + "123");
        }

        @Override
        public void clear() {
            stringList.clear();
        }
    }

    interface ReadFileInterface {

        void process(String str);

        void clear();

        default void readFile(List<String> strings, String src, String encoding, boolean hasTitle) {
            int readLimit = 8 * 1024 * 1024;
            int startIndex = 0;

            try (RandomAccessFile raf = new RandomAccessFile(src, "r");) {
                FileChannel channel = raf.getChannel();
                ByteBuffer rbuf = ByteBuffer.allocate(readLimit);

                synchronized (rbuf) {
                    channel.position(startIndex);
                    
                    byte[] temp = new byte[0];
                    int LF = 10;
                    long lineCount = 0;

                    while (channel.read(rbuf) != -1) {
                        int position = rbuf.position();
                        byte[] rbyte = new byte[position];
                        rbuf.flip();
                        rbuf.get(rbyte);
                        int startnum = 0;

                        for (int i = 0; i < rbyte.length; i++) {
                            if (rbyte[i] == LF) {
                                if (channel.position() == startIndex) {
                                    startnum = i + 1;
                                } else {
                                    if (hasTitle && 0 == lineCount) {
                                        startnum = i + 1;
                                        lineCount++;
                                        continue;
                                    }
                                    int lineLen = i - startnum + 1;
                                    byte[] line = new byte[temp.length + lineLen];
                                    System.arraycopy(temp, 0, line, 0, temp.length);
                                    System.arraycopy(rbyte, startnum, line, temp.length, lineLen);
                                    startnum = i + 1;
                                    temp = new byte[0];
                                    String str = trimEndingCRLF(line, encoding);
                                    strings.add(str);
                                    process(str);
                                }
                            }
                        }


                        if (startnum < rbyte.length) {
                            byte[] temp2 = new byte[temp.length + rbyte.length - startnum];
                            System.arraycopy(temp, 0, temp2, 0, temp.length);
                            System.arraycopy(rbyte, startnum, temp2, temp.length, rbyte.length - startnum);
                            temp = temp2;
                        }
                        rbuf.clear();
                    }
                }

                rbuf.clear();

                try {
                    Thread.sleep(10000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                clear();

            } catch (Exception e) {
                throw new RuntimeException();
            }
        }

        default String trimEndingCRLF(byte[] line, String encoding) throws UnsupportedEncodingException {
            if (line.length != 0 && line[0] != 10) {
                int lastIdx = line.length - 1;
                if (line[lastIdx] != 10) {
                    ++lastIdx;
                } else if (line[lastIdx - 1] == 13) {
                    --lastIdx;
                }

                return new String(line, 0, lastIdx, encoding);
            } else {
                return "";
            }
        }
    }
}
问题解答

1. MALLOC_TRIM_THRESHOLD_影响内存回收的原因

glibc的ptmalloc2分配器中,trim_threshold控制着内存池(arena)何时向操作系统归还空闲内存。默认情况下,非主arena的trim逻辑和主arena存在差异:

  • 主arena在free操作后会检查空闲内存是否超过阈值,若超过则调用sbrk或mmap归还内存;
  • 非主arena(由多线程环境触发创建)默认的trim行为更保守,甚至可能不会主动触发trim——因为ptmalloc2默认认为多线程场景下内存会被频繁复用,避免频繁向OS申请/归还内存带来的开销。

当你显式设置MALLOC_TRIM_THRESHOLD_环境变量时,相当于强制为所有arena(包括非主arena)开启了主动trim的逻辑:每次free操作后,分配器都会检查当前arena的空闲内存总量是否超过阈值,若满足条件则会将顶部连续的空闲内存块归还给操作系统,从而降低RSS。

你的场景中,大量线程创建的非主arena在free后积累了空闲内存,但默认未触发trim;设置该参数后,分配器主动触发trim,将空闲内存归还,因此RSS下降。

2. 设置50MB阈值仍触发回收的原因

你观察到的现象并非阈值失效,而是ptmalloc2对非主arena的trim逻辑存在特殊处理:

  • 显式设置MALLOC_TRIM_THRESHOLD_后,非主arena的trim行为不再遵循默认的保守策略,即使空闲内存未达到阈值,某些情况下也会触发trim——比如当arena对应的线程退出后,分配器可能会对该arena进行更积极的内存回收(因为该arena后续被复用的概率降低);
  • 另外,ptmalloc2的trim操作并非严格只回收超过阈值的内存,而是会尝试归还顶部所有连续的空闲块。你的场景中每个非主arena有8MB被占用,剩余56MB空闲,当触发trim时,这56MB空闲块会被归还给OS,而你看到的RSS回收16MB是多个arena累计的结果;
  • 还有一种可能:glibc 2.39中对MALLOC_TRIM_THRESHOLD_的处理逻辑有调整,显式设置该参数会覆盖非主arena的默认trim策略,使其行为与主arena一致,即使空闲内存未达到阈值,也会在特定时机(如线程退出)触发trim。

总结来说,设置MALLOC_TRIM_THRESHOLD_不仅改变了阈值大小,还改变了非主arena的trim触发逻辑,使其从保守变为主动,因此即使设置为50MB,也会触发内存回收。

内容的提问来源于stack exchange,提问作者Forever

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 01:52:01