【jvm】堆外内存

本文深入探讨了JVM中堆外内存的使用与管理机制,解释了DirectByteBuffer如何处理堆外内存,以及GC对其回收的影响。此外,还分析了堆外内存的优势与潜在问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  JVM可以使用的内存分外2种:堆内存和堆外内存.

    堆内存完全由JVM负责分配和释放,如果程序没有缺陷代码导致内存泄露,那么就不会遇到java.lang.OutOfMemoryError这个错误。

    使用堆外内存,就是为了能直接分配和释放内存,提高效率。JDK5.0之后,代码中能直接操作本地内存的方式有2种:使用未公开的Unsafe和NIO包下ByteBuffer。

    关于Unsafe对象的简介和获取方式,可以参考:http://blog.youkuaiyun.com/aitangyong/article/details/38276681

    使用ByteBuffer分配本地内存则非常简单,直接ByteBuffer.allocateDirect(10 * 1024 * 1024)即可。

    C语言的内存分配和释放函数malloc/free,必须要一一对应,否则就会出现内存泄露或者是野指针的非法访问。java中我们需要手动释放获取的堆外内存吗?

    我们一起来看看NIO中提供的ByteBuffer

      我们将最大堆外内存设置成40M,运行这段代码会发现:程序可以一直运行下去,不会报OutOfMemoryError。如果使用了-verbose:gc -XX:+PrintGCDetails,会发现程序频繁的进行垃圾回收活动。那么DirectByteBuffer究竟是如何释放堆外内存的?

    我们修改下JVM的启动参数,重新运行之前的代码:

      与之前的JVM启动参数相比,增加了-XX:+DisableExplicitGC,这个参数作用是禁止代码中显示调用GC。代码如何显示调用GC呢,通过System.gc()函数调用。如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果,相当于是没有这行代码一样。

      显然堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说NIO直接内存的回收,需要依赖于System.gc()。如果我们的应用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险

     我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,可能会进行垃圾回收,也可能不会。什么时候才是合适的时机呢?一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),还有就是内存不足,不得不进行垃圾回收。我们例子中的根本矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象,才能释放堆外内存,但是我们又不能强制JVM释放堆内存。

Direct Memory的回收机制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。

Direct ByteBuffer分配出去的内存其实也是由GC负责回收的,而不像Unsafe是完全自行管理的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有则同时也会回收其占用的堆外内存。

参考:http://blog.youkuaiyun.com/aitangyong/article/details/39403031

          http://blog.youkuaiyun.com/aitangyong/article/category/2159887

          http://blog.youkuaiyun.com/aitangyong/article/details/39323125

          http://hellojava.info/?p=56

          http://hellojava.info/?tag=maxdirectmemorysize

          https://yq.aliyun.com/articles/2948

       使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。

堆外内存的好处是:

(1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;

(2)理论上能减少GC暂停时间;

(3)可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

(4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

站在系统设计的角度来看,使用堆外内存可以为你的设计提供更多可能。最重要的提升并不在于性能,而是决定性的

不好的地方:

  1 堆外内存难以控制,如果内存泄漏,那么很难排查 
  2 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。

堆外内存说明请参见:http://ju.outofmemory.cn/entry/200574

                                    http://blog.youkuaiyun.com/aitangyong/article/category/2159887

### 关于JVM堆外内存的使用和管理 #### 堆外内存概述 堆外内存指的是不位于Java虚拟机(JVM)堆中的内存区域。这部分内存由操作系统直接分配给应用程序,而不是通过JVM的标准垃圾回收机制来管理和释放。因此,在某些情况下,利用堆外内存能够提高性能并减少停顿时间。 #### 主要组成部分 JVM非堆内存主要包括以下几个方面[^3]: - **GC**: 自身元数据存储空间。 - **JIT编译器**: 编译后的本地代缓存区。 - **线程栈(Thread Stacks)**: 每个线程所需的私有内存块用于保存局部变量等信息。 - **类加载(Class Metadata/ClassLoaders)**: 存储已加载类型的结构描述符以及方法表等相关资料;对于旧版本JDK而言还包括永久代(PermGen),其中可能引发`java.lang.OutOfMemoryError: PermGen space`错误[^4]。 - **NIO Direct Buffers**: 提供了一种方式使得文件I/O操作可以直接访问物理地址而无需经过缓冲池转换过程,从而提高了效率。 #### 使用场景与优势 当处理大量临时性的大对象或者频繁创建销毁的对象时,如果这些对象被放置到常规的Java堆上,则会增加GC负担甚至导致应用响应延迟等问题。此时采用off-heap技术就显得尤为重要了——它可以有效地降低GC频率及其影响范围,并且允许开发者更精细地控制资源生命周期。 此外,像Apache Spark这样的分布式计算框架也会充分利用这一特性来进行任务执行期间所需的数据交换工作。例如设置参数`spark.executor.memory`时,默认预留一部分作为内部开销之外可用作实际运算的空间大小[^2]. ```scala // 设置Spark Executor 的总内存容量 conf.set("spark.executor.memory", "8g") ``` #### 管理挑战 尽管如此,合理规划和监控仍然是必不可少的工作环节之一。因为一旦超出系统所能承受的最大限度就会触发OOM(out-of-memory)异常终止进程运行。同时也要注意避免因过度依赖而导致其他潜在风险比如安全性漏洞或是难以调试的应用逻辑缺陷等等。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值