JVM直接内存释放问题

最近发现个问题, 有点困惑,请指点:
Java程序使用 ByteBuffer.allocateDirect(1024 * 1024)分配500M直接内存(循环500次),在top -p或者在proc的VmRSS指标中 显示内存被占用(约500M的上涨);
反复继续分配-直到达到直接内存上限,抛出直接内存的OOM异常;此时VmRSS内存指标已达到一个峰值;
手动触发Full-GC来释放掉直接内存,此时VmRSS没有明显的下降(波动10M以内),而VisualVM通过Buffer Pool插件跟踪的直接内存下降了500M;
再次使用ByteBuffer.allocateDirect(500* 1024 * 1024) 分配成功,VmRSS和VisualVM的Buffer Pool同时上涨500M;不断进行(clear -> allocate),VmRSS会不断往上提升,远远超过直接内存上限

逻辑代码如下所示:

 private static final List<ByteBuffer> BUFFER_LIST = new ArrayList<>();

  private void allocate(int n) {
        for (int i = 0; i < n; i++) {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024);
            BUFFER_LIST.add(byteBuffer);
        }
    }

  private void clear() {
        BUFFER_LIST.clear();
        System.gc();
    }

这是因为直接内存的释放与 JVM GC 不完全相同。ByteBuffer.allocateDirect() 分配的内存空间是由操作系统管理的,而不是由 JVM 的 GC 管理的。当你持有 Direct Buffers 时,JVM 的垃圾收集器并不会回收它们,因此在 JVM 进程的 VMRSS 中,反复申请时会出现 VMRSS 一直上涨的情况。

另外一点需要注意的是,手动触发 System.gc() 并不能保证立即释放所有 Direct Buffers 占用的空间,因为 JVM 并不能准确知道 Direct Buffers 到底是否被使用中,因此可能需要等待更长的时间才能真正回收这些空间。

如果需要释放已分配的直接内存空间,在 Java NIO 库中提供了 ByteBuffer 类的 clear() 方法,该方法清空 Direct Buffer,并将其标记为空闲状态,让 Direct Buffers 归还给系统进行回收。因此,建议在适当的时候调用 ByteBuffer.clear() 方法释放 Direct Buffers 内存空间,而不是依赖于手动触发垃圾回收。

首先,ByteBuffer.allocateDirect这个方法是使用了堆内存,并不是由java jvm来直接管理的。其次,手动触发Full-GC来释放掉直接内存这个操作,并不代表jvm中的内存会马上回收,只是告诉了系统要回收,具体什么时候回收不一定的。如果你要快速的回收,你可以调用BUFFER_LIST的clear等方法情况数据后,再赋值为NUll。

ByteBuffer.allocateDirect()方法分配的是直接内存,而直接内存是由操作系统管理的,而不是由JVM管理的,所以会导致你这个内存问题。因此使用ByteBuffer.allocateDirect()来分配直接内存的操作要谨慎,尽量别用这种方式来操作

该回答通过自己思路及引用到GPTᴼᴾᴱᴺᴬᴵ搜索,得到内容具体如下:

在Java中,使用ByteBuffer.allocateDirect()方法分配的内存是直接内存,即不在堆上分配的内存。在Java中,直接内存的分配和释放是由JVM来管理的,而不是由垃圾回收器来管理的。因此,当我们使用ByteBuffer.allocateDirect()方法分配直接内存时,JVM会在本地内存中分配一块空间,并将其映射到Java的直接内存中,这个映射关系是一一对应的。

在你的代码中,你使用了ByteBuffer.allocateDirect()方法分配了500M的直接内存,这会导致本地内存中分配一块大小为500M的空间,并将其映射到Java的直接内存中。当你反复继续分配直到达到直接内存上限时,JVM会抛出直接内存的OOM异常,此时本地内存中的空间并没有被释放,而Java的直接内存已经达到了一个峰值。

当你手动触发Full-GC来释放直接内存时,JVM会将Java的直接内存中的数据清空,并将其映射到本地内存中的空闲空间中。此时,本地内存中的空间大小并没有改变,而Java的直接内存已经被释放了。

再次使用ByteBuffer.allocateDirect()方法分配直接内存时,JVM会在本地内存中分配一块新的空间,并将其映射到Java的直接内存中,此时本地内存中的空间大小会增加500M,而Java的直接内存也会增加500M。当你不断进行clear -> allocate操作时,本地内存中的空间会不断增加,而Java的直接内存也会不断增加,因此,VmRSS会不断往上提升,远远超过直接内存上限。

总之,JVM会在本地内存中分配并管理直接内存,当我们手动触发Full-GC时,只会释放Java的直接内存中的数据,而不会释放本地内存中的空间。因此,如果我们反复使用ByteBuffer.allocateDirect()方法分配和释放直接内存,会导致本地内存中的空间不断增加,最终可能会导致本地内存溢出。因此,我们在使用直接内存时,需要注意避免过度分配,以及需要及时释放直接内存。


如果以上回答对您有所帮助,点击一下采纳该答案~谢谢

这是因为直接内存的释放与 JVM GC 不完全相同。ByteBuffer.allocateDirect() 分配的内存空间是由操作系统管理的,而不是由 JVM 的 GC 管理的。当你持有 Direct Buffers 时,JVM 的垃圾收集器并不会回收它们,因此在 JVM 进程的 VMRSS 中,反复申请时会出现 VMRSS 一直上涨的情况。

另外一点需要注意的是,手动触发 System.gc() 并不能保证立即释放所有 Direct Buffers 占用的空间,因为 JVM 并不能准确知道 Direct Buffers 到底是否被使用中,因此可能需要等待更长的时间才能真正回收这些空间。

如果需要释放已分配的直接内存空间,在 Java NIO 库中提供了 ByteBuffer 类的 clear() 方法,该方法清空 Direct Buffer,并将其标记为空闲状态,让 Direct Buffers 归还给系统进行回收。因此,建议在适当的时候调用 ByteBuffer.clear() 方法释放 Direct Buffers 内存空间,而不是依赖于手动触发垃圾回收。

参考chatgpt PLUS版本回答

Java的直接内存和垃圾回收机制有关。
首先,ByteBuffer.allocateDirect()方法用于分配直接内存,这是一种不受Java堆限制的内存分配方式。直接内存通常用于需要快速读写的操作,如网络操作和文件IO。

在您的逻辑代码中,您通过循环调用allocate()方法来分配直接内存,并将它们存储在BUFFER_LIST中。每次分配1MB的直接内存,总共分配了500次,总量达到500MB。这导致了VmRSS内存指标的上涨。

当您继续分配直到达到直接内存上限时,可能会抛出直接内存的OOM异常。这是因为直接内存的大小受系统的限制,当超过这个限制时,会抛出OutOfMemoryError异常。

在手动触发Full-GC后,您观察到VisualVM的Buffer Pool插件显示直接内存下降了500MB。这是因为Full-GC会回收不再被引用的对象,包括直接内存。但是,VmRSS内存指标可能没有明显的下降,这是因为垃圾回收并不总是立即回收所有的内存,而是根据垃圾回收算法和策略来决定回收的时机。

在再次调用allocateDirect()方法分配500MB直接内存后,您可能会观察到VmRSS和VisualVM的Buffer Pool同时上涨了500MB。这是因为您重新分配了500MB的直接内存,并将它们存储在BUFFER_LIST中。这会导致VmRSS内存指标超过直接内存上限,因为VmRSS包括了Java堆以外的内存,如线程栈和其他资源的消耗。

要注意的是,手动调用System.gc()并不能保证立即回收所有的内存,垃圾回收的具体行为是由JVM控制的。垃圾回收的时机和方式受到多种因素的影响,包括系统负载、内存压力和垃圾回收策略等。

建议您在使用直接内存时,仔细考虑内存的使用和释放方式,避免超过系统限制导致的OOM异常。确保正确地释放不再使用的直接内存,并合理管理内存的分配和回收。另外,监控和分析内存使用情况可以帮助您更好地了解和优化应用程序的性能。

这个问题的原因可能是Java程序中使用了直接内存(Direct Memory),而直接内存的分配和释放机制与堆内存不同,因此触发了一些特殊情况。

首先,通过ByteBuffer.allocateDirect(1024 * 1024)分配的直接内存是由操作系统管理的,不受Java堆内存限制的影响。因此,在使用allocateDirect方法分配500M直接内存的循环中,每次分配都会占用一部分直接内存,并在操作系统的内存指标中显示出来。

当直接内存达到上限时,继续分配直接内存会抛出OOM异常。此时,手动触发Full-GC会释放直接内存,但是由于直接内存不受Java堆内存管理,所以VmRSS内存指标可能没有明显下降,因为它只反映了Java堆内存的使用情况。

使用ByteBuffer.allocateDirect(500 * 1024 * 1024)再次分配直接内存时,会重新申请新的直接内存,并且VmRSS和VisualVM的Buffer Pool指标都会上涨500M。这是因为直接内存的分配和释放是与操作系统紧密相关的,而不仅仅受限于Java堆内存。

在不断进行clear -> allocate操作时,VmRSS会不断上升,可能超过直接内存上限,这是因为操作系统可能对直接内存的管理机制与Java堆内存不同,可能会有一些缓冲机制或者其他因素导致内存占用超出限制。

总结来说,这个问题的现象是因为使用直接内存分配和释放,而直接内存的管理机制与Java堆内存不同,导致内存指标的表现和预期有所不同。

jvm内存的释放

package direct;
import sun.misc.Unsafe;
import java.io.IOException;
import java.lang.reflect.Field;
public class GC_memory_bottom { 
    static int _1Gb = 1024 * 1024 * 1024;
    public static void main(String[] args) throws IOException { 
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();
        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }
    public static Unsafe getUnsafe() { 
        try { 
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) { 
            throw new RuntimeException(e);
        }
    }
}

https://blog.csdn.net/t000818/article/details/79023134
这篇文章对ByteBuffer介绍得比较详细,可以抽空阅读一下,有助于你理解这个问题。