写点什么

面试官:JVM 是如何分配和回收堆外内存的?

  • 2023-03-03
    湖南
  • 本文字数:2061 字

    阅读完需:约 7 分钟

JVM 内存模型

在 JVM 中内存被分成两大块,分别是堆内存和堆外内存,堆内存就是 JVM 使用的内存,而堆外内存就是非 JVM 使用的内存,一般是分配给机器使用的内存。


那么整个内存模型如下:

因此在 JVM 中正常只能分配之际独有的内存即堆内存,而我们知道 JVM 并不建议开发者直接操作堆外内存的,因此容易造成内存泄漏,并且难以排查,但是在 JVM 中是可以操作堆外内存的并且也可以回收堆外内存,但是是一种不建议的方式。

如何分配堆外内存

那么在堆内存中如何分配堆外内存呢?


在 Java 中存在两种方式分配堆外内存,分别是 ByteBuffer#allocateDirect 和 Unsafe#allocateMemory。

可能第一个会经常使用到,这是 Java NIO 提供的一个分配内存的类,在做网络开发时会经常使用该方式进行分配内存,而第二种方式是 Unsafe 的方式,我们知道 Unsafe 是一种不安全的类,该类是提供给开发者操作最底层数据的类,类似 C 或者 C++直接操作内存的方式,因此该类并不建议使用,如果使用该类分配内存但是没有及时回收容易造成内存泄漏。

第一种方式:ByteBuffer#allocateDirect

该类分配内存的实现方式如下:

//分配10M的内存ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
复制代码

通过该方式分配堆外内存其实最底层还是使用的是 Unsafe#allocateMemory 进行分配内存,ByteBuffer 只是对 Unsafe 做了一层封装。

第二种方式:Unsafe#allocateMemory

public class Test {    private static Unsafe unsafe = null;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { //分配10M的内存 Field getUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); getUnsafe.setAccessible(true); unsafe = (Unsafe)getUnsafe.get(null); //分配完内存返回内存的地址 long address = unsafe.allocateMemory(10 * 1024 * 1024); }}
复制代码

该方式中 Unsafe 类并不能直接被使用,但是可以通过反射的方式使用该类,该类分配内存后需要手动回收,不然被分配的内存不会被释放。

如何回收堆外内存

说完了如何分配内存,那么继续了解如何回收堆外内存。

第一种方式:Unsafe#freeMemory

分配堆外内存的两种方式中,第二种 Unsafe 的方式其实提供了一个释放堆外内存的实现,实现如下:

public class Test {    private static Unsafe unsafe = null;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { //分配10M的内存 Field getUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); getUnsafe.setAccessible(true); unsafe = (Unsafe)getUnsafe.get(null); //分配完内存返回内存的地址 long address = unsafe.allocateMemory(10 * 1024 * 1024); //回收分配的堆外内存 unsafe.freeMemory(address); }}
复制代码

在 Unsafe 中提供了 freeMemory 的实现进行回收堆外内存,但是前提是需要知道被分配的堆外内存地址才可以实现对应的内存回收。


这种回收堆外内存的方式其实是开发者自己手动回收,并不是由 JVM 引起的内存回收,那么 JVM 如何回收堆外内存呢?

第二种方式:JVM 回收堆外内存

通过 ByteBuffer#allocateDirect 分配的堆外内存在 JVM 中其实也是存在一定的内存占用的,具体关联关系如下:



当通过 ByteBuffer#allocateDirect 分配堆外内存后,会将堆外内存的地址、大小等信息通过 DirectByteBuffer 进行关联,那么堆内存中就可以关联到堆外内存。


那么 Cleaner 又是什么东西呢?


了解 Cleaner 需要知道 JVM 中四种引用方式:强引用、弱引用、软引用、虚引用,Cleaner 就是虚引用的实现,上图中的 ReferenceQueue 就是一个引用队列,将需要回收的 Cleaner 放入到该队列中,实现逻辑如下:

  1. JVM 执行 Full GC 时会将 DirectByteBuffer 进行回收,回收之后 Clearner 就不存在引用关系

  2. 再下一次发生 GC 时会将 Cleaner 对象放入 ReferenceQueue 中,同时将 Cleaner 从链表中移除

  3. 最后调用 unsafe#freeMemory 清除堆外内存


那么可能会存在疑问,为什么 DirectByteBuffer 会被回收呢?


首先 DirectByteBuffer 是存在堆内存中的对象,那么既然存在堆内存中就会发生 GC 晋级,即晋升到老年代中,在老年代中就会发生 Full GC 或者 Old GC。

注意点

注意点 1:

在实际使用 DirectByteBuffer 时要避免把内存使用完,但是在实际操作中我们可能不知道堆外内存还剩余多少,因此我们可以在 JVM 中通过参数控制,通过 JVM 参数 -XX:MaxDirectMemorySize 指定堆外内存的上限大小,当超过指定的内存上限大小时,会主动触发一次 Full GC 进行回收内存。

注意点 2:

通过 DirectByteBuffer 分配内存时,可能会出现分配内存不够的情况,因此 JVM 如果发现堆外内存分配不足时,也会主动发起一次 GC,只不过这次 GC 是通过 System.gc() 实现的强制 GC,但是在实际生产环境中我们都是通过 JVM 参数 -XX:+DisableExplicitGC,禁止使用 System.gc()的,因此在实际使用过程中一定要注意分配内存的情况,避免出现内存泄漏。


作者:陈汤姆

链接:https://juejin.cn/post/7115787649910046750

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
面试官:JVM是如何分配和回收堆外内存的?_Java_做梦都在改BUG_InfoQ写作社区