1.什么是堆外内存?
堆外内存和堆内内存是两个相对的概念,其中堆内内存(on-heap memory)是我们平常工作中接触比较多的。Java中分配的非空对象都是由Java虚拟机的垃圾收集器管理的,都是放在堆内内存。我们可以通过jvm参数-Xms,-Xmx等设置堆的大小和最大值。
jvm会采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,Full GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。
和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接直接受操作系统管理(而不是java虚拟机)。
2.堆外内存有什么优势?
减少垃圾回收:因为垃圾回收会对其他的应用产生影响
加快了复制的速度:堆内在flush到远程时,会先复制到直接内存(非堆内存),然后再发送;而存储在堆外内存相当于省略掉了这个工作。
堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,需要自己管理,当发生内存溢出时排查起来非常困难。
3.堆外内存的使用
堆外内存不是 JVM 运行时数据区 Runtime Data Area 的一部分,这部分内存区域直接被操作系统管理。
通常我们使用 java.nio.DirectByteBuffer 对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。
3.1堆外内存的设置
堆外内存的限额默认与堆内内存(由-Xmx 设定)相仿,可用 -XX:MaxDirectMemorySize 进行设定。
当使用达到了阈值的时候将调用 System.gc 来做一次 Full GC,以此来回收掉没有被使用的堆外内存。
3.2堆外内存的创建
在使用DirectByteBuffer 时,首先会向 Bits 类申请存储额度。Bits 类内部维护着当前已经使用的堆外内存值,Bits 类有一个全局的 totalCapacity 变量,记录着全部 DirectByteBuffer 的总大小,每次申请,都先看看是否超过最大值:
如果超过最大值,会主动执行 Sytem.gc(),期待能主动回收一点堆外内存。然后休眠100ms,看看 totalCapacity 降下来没有,如果内存还是不足,就会抛出内存溢出。
如果额度被批准,就调用 sun.misc.Unsafe 去分配内存,返回内存基地址,Unsafe 的是C++实现,标准的 malloc。然后再调一次 Unsafe 把这段内存给清零。
使用DirectByteBuffer的注意事项
java.nio.DirectByteBuffer 对象在创建过程中会先通过Unsafe接口通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer 对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer 回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者 Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,一定要通过-XX:MaxDirectMemorySize 来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次 full gc,以此来回收掉没有被使用的堆外内存。
3.3堆外内存的回收
3.3.1自动回收
Java 是不用用户去管理内存的,所以 Java 对堆外内存的管理也是自动回收的。它是由 GC 模块负责的,在 GC 时会扫描 DirectByteBuffer 对象是否有有效引用指向该对象,如没有,在回收 DirectByteBuffer 对象的同时且会回收其占用的堆外内存。但是 JVM 如何释放其占用的堆外内存呢?
这得从 Cleaner 继承了 PhantomReference(虚引用) 说起。说到 Reference,还有 SoftReference、WeakReference、FinalReference 他们作用各不相同,这里就不展开说了。
简单介绍 PhantomReference,首先虚引用是不会影响 JVM 去回收其指向的对象,当 GC 某个对象时,如果有此对象上还有虚引用对其引用,会将 PhantomReference 对象插入 ReferenceQueue 队列。
PhantomReference插入到哪个队列呢?看 PhantomReference 类代码,其继承自 Reference,Reference 对象有个 ReferenceQueue 成员,这个也就是 PhantomReference 对象插入的 ReferenceQueue 队列,此成员如果不由外部传入就是 ReferenceQueue.NULL。如果需要通过 queue 拿到 PhantomReference 对象,这个 ReferenceQueue 对象还是必须由外部传入。
这里可以看到一种尴尬的情况,因为 DirectByteBuffer 本身的个头很小,只要熬过了 Young GC,即使已经失效了也能在老生代1里舒服的呆着,不容易把老生代撑爆触发 Full GC,如果没有别的大块头进入老生代触发Full GC,就一直在那耗着,占着一大片堆外内存不释放。
3.3.2手动回收
手动回收,就是由开发手动调用 DirectByteBuffer 的 cleaner 的 clean 方法来释放空间。由于 cleaner 是 private 访问权限,所以自然想到使用反射来实现。
还有另一种方法,DirectByteBuffer 实现了 DirectBuffer 接口,这个接口有 cleaner 方法可以获取 cleaner 对象。
对于 Sun 的 JDK 这其实很简单,只要从 DirectByteBuffer 里取出那个 sun.misc.Cleaner,然后调用它的 clean() 就行。