当前位置: 首页 > news >正文

Netty中的直接内存是怎么回事?

直接内存

在 Java 中对象都是在堆内分配的,通常我们说的JVM 内存也就指的堆内内存,堆内内存完全被JVM 虚拟机所管理,JVM 有自己的垃圾回收算法,对于使用者来说不必关心对象的内存如何回收。堆外内存与堆内内存相对应,对于整个机器内存而言,除堆内内存以外部分即为堆外内存。堆外内存不受 JVM 虚拟机管理,直接由操作系统管理。直接内存申请空间调用 allocateDirect() 方法就可以了。
Java 中堆外内存的分配方式有两种:ByteBuffer#allocateDirectUnsafe#allocateMemory
Java NIO 包中的 ByteBuffer 类的分配方式如下:

// 分配 10M 堆外内存ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024); 

下图展示了 Buffer 分配在直接内存的功能和作用:
image-20250317112327750

直接内存的好处是:Java 程序可以直接在内存上为 ByteBuffer 申请空间,而不是在 JVM 的堆空间上申请。如果我们在 JVM 申请空间,想保存到磁盘中,数据的拷贝路径是这样的:JVM 空间–>操作系统控制的内存–>磁盘。但如果我们在直接内存给 ByteBuffer 分配空间,那么数据的拷贝路径是:操作系统控制的直接内存–>磁盘。这样就少了一次数据拷贝次数,提高了效率。

当然,直接内存也是有劣势的,比如申请和释放直接内存的开销比 JVM 内存要大。

堆外内存怎么回收?

image-20250317191825781

堆内存放的 DirectByteBuffer 对象并不大,仅仅包含堆外内存的地址、大小等属性,同时还会创建对应的 Cleaner 对象,通过 ByteBuffer 分配的堆外内存不需要手动回收,它可以被 JVM 自动回收。当堆内的 DirectByteBuffer 对象被 GC 回收时,Cleaner 就会用于回收对应的堆外内存

试想这么一种场景,因为 DirectByteBuffer 对象有可能长时间存在于堆内内存,所以它很可能晋升到 JVM 的老年代,所以这时候 DirectByteBuffer 对象的回收需要依赖 Old GC 或者 Full GC 才能触发清理。如果长时间没有 Old GC 或者 Full GC 执行,那么堆外内存即使不再使用,也会一直在占用内存不释放,很容易将机器的物理内存耗尽,这是相当危险的。

那么在使用 DirectByteBuffer 时我们如何避免物理内存被耗尽呢?因为 JVM 并不知道堆外内存是不是已经不足了,所以我们最好通过 JVM 参数 -XX:MaxDirectMemorySize 指定堆外内存的上限大小,当堆外内存的大小超过该阈值时,就会触发一次 Full GC 进行清理回收,如果在 Full GC 之后还是无法满足堆外内存的分配,那么程序将会抛出 OOM 异常。

此外在 ByteBuffer.allocateDirect 分配的过程中,如果没有足够的空间分配堆外内存,在 Bits.reserveMemory 方法中也会主动调用 System.gc() 强制执行 Full GC,但是在生产环境一般都是设置了 -XX:+DisableExplicitGC,System.gc() 是不起作用的,所以依赖 System.gc() 并不是一个好办法。

通过前面堆外内存分配方式的介绍,可以知道 DirectByteBuffer 在初始化时会创建一个 Cleaner 对象,它会负责堆外内存的回收工作。Cleaner 就属于虚引用PhantomReference 的子类,虚引用唯一能做的是在对象被GC时,收到通知,并执行一些后续工作。如以下源码所示,PhantomReference 不能被单独使用,需要与引用队列 ReferenceQueue 联合使用。

public class Cleaner extends java.lang.ref.PhantomReference<java.lang.Object> {private static final java.lang.ref.ReferenceQueue<java.lang.Object> dummyQueue;private static sun.misc.Cleaner first;private sun.misc.Cleaner next;private sun.misc.Cleaner prev;private final java.lang.Runnable thunk;public void clean() {}}

当初始化堆外内存时,内存中的对象引用情况如下图所示,first 是 Cleaner 类中的静态变量,Cleaner 对象在初始化时会加入 Cleaner 链表中。DirectByteBuffer 对象包含堆外内存的地址、大小以及 Cleaner 对象的引用,ReferenceQueue 用于保存需要回收的 Cleaner 对象。

image-20250317192638529

当发生 GC 时,DirectByteBuffer 对象被回收,内存中的对象引用情况发生了如下变化:

image-20250317192655460

此时 Cleaner 对象不再有任何引用关系,在下一次 GC 时,该 Cleaner 对象将被添加到 ReferenceQueue 中,有一个后台任务会从这个队列中拿出对象并执行 clean() 方法。clean() 方法主要做两件事情:

  1. 将 Cleaner 对象从 Cleaner 链表中移除;
  2. 调用 unsafe.freeMemory 方法清理堆外内存。

四大引用

在Java层面,一共有四种引用:强引用、软引用、弱引用、虚引用,从名字也可以发现,这几种引用的生命周期由强到弱。

强引用

强引用(Strong Reference)是使用最普遍的引用,99%的代码可能都是强引用,很多人平时接触的也都是强引用相关的代码,比如下面这种:

Object o=new Object()

这种情况是普遍存在的,在写中间件框架代码时,可能才需要其它引用。

如果一个对象,和GC Root有强引用的关系,当内存不足发生GC时,宁可抛出OOM异常,终止程序,也不会回收这些对象。相反,当一个对象,和GC Root没有强引用关系时,可能会被回收(因为可能还有其它引用),如果没有任何引用关系,GC之后,该对象就被回收了。

软引用

如果一个对象只具有软引用(Soft Reference),则内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。这种特别适合用来实现缓存。

Object reference = new MyObject();
System.out.println(reference);
Reference root = new SoftReference(reference);
reference = null; // MyObject对象只有软引用
System.gc();
System.out.println(root.get());输出:
cn.itcast.nio.c10.MyObject@511d50c0
cn.itcast.nio.c10.MyObject@511d50c0

弱引用

弱引用(Weak Reference),相对于软引用,它的生命周期更短,当发生GC时,如果扫描到一个对象只有弱引用,不管当前内存是否足够,都会对它进行回收。

Object reference = new MyObject();
System.out.println(reference);
Reference root = new WeakReference(reference);
reference = null; // MyObject对象只有弱引用
System.gc(); 
System.out.println(root.get());
输出:
null

ThreadLocal

scala 代码解读复制代码static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//......}

ThreadLocal.ThreadLocalMap.Entry 继承了弱引用,key为当前线程实例,和WeakHashMap基本相同。

虚引用

虚引用(Phantom Reference),和之前两种引用的最大不同是:它的get方法一直返回null。

很奇怪,一个返回null的引用有什么用?

虚引用的使用场景很窄,在JDK中,目前只知道在申请堆外内存时有它的身影。 申请堆外内存时,在JVM堆中会创建一个对应的Cleaner对象,这个Cleaner类继承了PhantomReference,当DirectByteBuffer对象被回收时,可以执行对应的Cleaner对象的clean方法,做一些后续工作,这里是释放之前申请的堆外内存。

由于虚引用的get方法无法拿到真实对象,所以当你不想让真实对象被访问时,可以选择使用虚引用,它唯一能做的是在对象被GC时,收到通知,并执行一些后续工作。

实现原理

上述引用中,除了强引用,其它几种都有对应的实现类,都继承了Reference。

Reference有几个重要的参数,有些和GC密切相关:

  1. referent: 就是所引用的对象,会被GC特别对待。
  2. queue:RererenceQueue,看名字也知道它是一个Reference队列,用来保存Reference对象,当新建一个Reference时,可以选择性的传入第二个参数。
  3. discovered:该对象被JVM使用,表示下一个要被处理的Reference对象(1.8的实现)
  4. next:当Reference对象被放入RererenceQueue时,使用next变量形成链表结构。
  5. pending:该对象会被JVM使用,当前被处理的Reference对象。

Reference中有一个重要的线程 Reference Handler,运行优先级极高,启动之后负责轮询pending变量是否有数据,如果pending被JVM设置了一个值,就把它拿出来放到queue中,这里有个例外,就是之前说的堆外内存申请时的Cleaner对象,只会执行它的clean方法,并不会放到queue中。

当Reference对象被放进queue之后,就可以使用一个线程,依次拿出来进行处理。

  @Testpublic void test2() throws IOException {final ReferenceQueue queue = new ReferenceQueue();new Thread(new Runnable() {@Overridepublic void run() {while (true) {try {Reference reference = queue.remove();System.out.println(reference + "回收了");} catch (InterruptedException e) {}}}}).start();Object o = new Object();Reference root = new WeakReference(o, queue);System.out.println(root);o = null;System.gc();System.in.read();}java.lang.ref.WeakReference@4c98385c
java.lang.ref.WeakReference@4c98385c回收了  

上述代码中,先初始化了一个ReferenceQueue,随后又初始化了一个线程,循环的从queue中捞数据,因为当一个软引用、弱引用或虚引用的对象被GC回收时,这个引用会被放到对应的ReferenceQueue中,这里会被拿出来进行打印,更多的是做一些清理工作。


http://www.mrgr.cn/news/95267.html

相关文章:

  • 强推 Maven多镜像源快速切换工具,GUI操作超便捷
  • 软件工程之软件验证计划Software Verification Plan
  • 模型空间、图纸空间、布局(Layout)之间联系——CAD c#二次开发
  • Java+Html实现前后端客服聊天
  • K8S学习之基础四十:K8S配置altermanager发送告警到钉钉群
  • [AI速读]如何构建高效的AMBA协议检查器(Checker IP)
  • 基于carla的模仿学习(附数据集CORL2017)更新中........
  • PWM控制电机转速的原理及相关寄存器值计算
  • 失败的面试经历(ʘ̥∧ʘ̥)
  • 英伟达黄仁勋2025GTC演讲深度解析:液冷GPU、AI工厂、机器人AI…...
  • electron桌面应用多种快速创建方法
  • Windows 系统安装 Python3.7 、3.8、3.9、3.10、 3.11 、3.12、3.13最新版,附带相应程序。
  • 尝试在软考65天前开始成为软件设计师-计算机网络
  • VulnHub-Web-Machine-N7通关攻略
  • mysql之DATE_FORMAT迁移到gbase8s
  • 拓展 Coco AI 功能 - 智能检索 Hexo 博客
  • 论文分享:PL-ALF框架实现无人机低纹理环境自主飞行
  • T-CSVT投稿记录
  • Android自动化测试终极指南:从单元到性能全覆盖!
  • MCU-芯片时钟与总线和定时器关系,举例QSPI