堆外内存(off-heap memory)

Introduction

堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。

我们经常用 java.nio.DirectByteBuffer 对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。

1
ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);

当内存不够时候,会抛出 java.lang.OutOfMemoryError: Direct buffer memory 异常。

堆外内存的优点:

  • 减少了垃圾回收。因为垃圾回收会暂停其他的工作。
  • 加快了复制的速度。堆内存在flush到远程时,会先复制到直接内存(非堆内存),然后在发送。而堆外内存直接发送,不需要复制。

堆外内存的缺点:

  • 内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

Notices

java.nio.DirectByteBuffer 对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到 java.nio.DirectByteBuffer 对象里,这样就可以直接操作这些内存。

这些内存只有在 DirectByteBuffer 对象回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发 CMS GC 或者 Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次 Full GC 来回收掉没有被使用的堆外内存。

前提是没有启用DisableExplicitGC

在写NIO程序经常使用ByteBuffer来读取或者写入数据,那么使用 ByteBuffer.allocate(capability) 还是使用 ByteBuffer.allocteDirect(capability) 来分配缓存了?

第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢;第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。


回收方法

  1. Full GC,一般发生在年老代垃圾回收以及调用System.gc的时候。
  2. 调用ByteBuffer的cleaner的clean(),内部还是调用System.gc(),所以一定不要-XX:+DisableExplicitGC。

ByteBuffer的堆外内存回收

Allocate And Free By GC

JVM Options: -Xms128m -Xmx128m -XX:MaxDirectMemorySize=40M -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps

1
2
3
4
5
fun main(args: Array<String>) {
while (true) {
val buffer = ByteBuffer.allocateDirect(8 * 1024 * 1024)
}
}
1
2
3
4
5
2019-01-06T00:14:39.446-0800: [GC (System.gc()) [PSYoungGen: 3370K->688K(38400K)] 3370K->696K(125952K), 0.0007600 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-06T00:14:39.447-0800: [Full GC (System.gc()) [PSYoungGen: 688K->0K(38400K)] [ParOldGen: 8K->475K(87552K)] 696K->475K(125952K), [Metaspace: 3335K->3335K(1056768K)], 0.0036042 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

2019-01-06T00:14:39.485-0800: [GC (System.gc()) [PSYoungGen: 681K->128K(38400K)] 1157K->611K(125952K), 0.0006844 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2019-01-06T00:14:39.486-0800: [Full GC (System.gc()) [PSYoungGen: 128K->0K(38400K)] [ParOldGen: 483K->444K(87552K)] 611K->444K(125952K), [Metaspace: 3339K->3339K(1056768K)], 0.0039458 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

Allocate Failure

JVM Options: 同上

1
2
3
4
5
6
fun main(args: Array<String>) {
while (true) {
// allocate 50M
val buffer = ByteBuffer.allocateDirect(50 * 1024 * 1024)
}
}

抛出异常

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.demo.bytebuffer.AllocateByteBufferKt.main(AllocateByteBuffer.kt:14)


-XX:+DisableExplicitGC

JVM Options: JVM Options: -Xms128m -Xmx128m -XX:MaxDirectMemorySize=40M -XX:+DisableExplicitGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps

1
2
3
4
5
6
fun main(args: Array<String>) {
while (true) {
// allocate 50M
val buffer = ByteBuffer.allocateDirect(8 * 1024 * 1024)
}
}

抛出异常

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.demo.bytebuffer.AllocateByteBufferKt.main(AllocateByteBuffer.kt:14)

结果说明,NIO的DirectMemory回收需要依赖于System.gc()

从DirectByteBuffer的源码可知,ByteBuffer.allocateDirect()会调用Bits.reservedMemory()方法,在该方法中显示调用了System.gc()用户内存回收,如果-XX:+DisableExplicitGC打开,则让System.gc()无效,内存无法有效回收而导致OOM。

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


Free ByteBuffer By Manual Control

JVM Options: 同上

1
2
3
4
5
val buffer = ByteBuffer.allocateDirect(40 * 1024 * 1024)
TimeUnit.SECONDS.sleep(3)
(buffer as DirectBuffer).cleaner().clean()
TimeUnit.SECONDS.sleep(3)
println("free is ok")

开源堆外缓存框架

  • Ehcache 3.0
  • Chronical Map:OpenHFT包括很多类库,使用这些类库很少产生垃圾,并且应用程序使用这些类库后也很少发生Minor GC。类库主要包括:Chronicle Map,Chronicle Queue等等。
  • OHC

Summary of understanding “-XX:MaxDirectMemorySize” setting

https://developer.ibm.com/answers/questions/398383/summary-of-understanding-xxmaxdirectmemorysize-set/


Reference

https://www.cnblogs.com/duanxz/p/6089485.html
https://www.jianshu.com/p/50be08b54bee