JVM

JVM-13丨堆外内存

Posted by jiefang on December 5, 2019

堆外内存

简介

堆外内存也称为非堆内存(Non-Heap Memory)或者直接内存(Direct Memory),位于Java虚拟机管控之外,受操作系统管理。Netty和NIO中广泛的使用了堆外内存,这部分内存如果得不到释放,容易造成堆外内存溢出,导致系统故障。

申请和释放

堆外内存申请

ByteBuffer提供了静态方法allocateDirect(int capacity)来分配堆外内存,其底层使用Unsafe.allocateMemory(long var1)

ByteBuffer.allocateDirect(int capacity)方法源码:

1
2
3
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

DirectByteBuffer构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        //增加直接内存计数
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = UNSAFE.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            //释放直接内存计数
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        UNSAFE.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

Bits.reserveMemory()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Bits {  
	static void reserveMemory(long size, int cap) {

        if (!MEMORY_LIMIT_SET && VM.initLevel() >= 1) {
            //获取JVM设置的最大直接内存
            MAX_MEMORY = VM.maxDirectMemory();
            MEMORY_LIMIT_SET = true;
        }

        // optimist!,乐观主义,尝试直接分配
        if (tryReserveMemory(size, cap)) {
            return;
        }

        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
        boolean interrupted = false;
        try {

            // Retry allocation until success or there are no more
            // references (including Cleaners that might free direct
            // buffer memory) to process and allocation still fails.
            //重试分配直到成功或者没有更多引用需要处理,分配仍然失败
            boolean refprocActive;
            do {
                try {
                    refprocActive = jlra.waitForReferenceProcessing();
                } catch (InterruptedException e) {
                    // Defer interrupts and keep trying.
                    interrupted = true;
                    refprocActive = true;
                }
                if (tryReserveMemory(size, cap)) {
                    return;
                }
            } while (refprocActive);

            // trigger VM's Reference processing
            //显示调用使JVM进行full GC
            System.gc();

            // A retry loop with exponential back-off delays.
            // Sometimes it would suffice to give up once reference
            // processing is complete.  But if there are many threads
            // competing for memory, this gives more opportunities for
            // any given thread to make progress.  In particular, this
            // seems to be enough for a stress test like
            // DirectBufferAllocTest to (usually) succeed, while
            // without it that test likely fails.  Since failure here
            // ends in OOME, there's no need to hurry.
            long sleepTime = 1;
            int sleeps = 0;
            //重试9次然后还是不能分配直接内存抛出直接内存溢出
            while (true) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                try {
                    if (!jlra.waitForReferenceProcessing()) {
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    }
                } catch (InterruptedException e) {
                    interrupted = true;
                }
            }

            // no luck
            throw new OutOfMemoryError("Direct buffer memory");
		//中断线程
        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }
}

Bits.tryReserveMemory()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    private static boolean tryReserveMemory(long size, int cap) {
        // -XX:MaxDirectMemorySize limits the total capacity rather than the
        // actual memory usage, which will differ when buffers are page
        // aligned.
        long totalCap;
        //如果直接内存容量还要剩余通过CAS方式分配并返回
        while (cap <= MAX_MEMORY - (totalCap = TOTAL_CAPACITY.get())) {
            if (TOTAL_CAPACITY.compareAndSet(totalCap, totalCap + cap)) {
                RESERVED_MEMORY.addAndGet(size);
                COUNT.incrementAndGet();
                return true;
            }
        }
        return false;
    }

Unsafe.allocateMemory(long var1)方法源码:

1
    public native long allocateMemory(long var1);

JVM的unsafe.cpp#Unsafe_AllocateMemory方法里,通过os::malloc来实现堆外内存申请。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
  UnsafeWrapper("Unsafe_AllocateMemory");
  size_t sz = (size_t)size;
  if (sz != (julong)size || size < 0) {
    THROW_0(vmSymbols::java_lang_IllegalArgumentException());
  }
  if (sz == 0) {
    return 0;
  }
  sz = round_to(sz, HeapWordSize);
  void* x = os::malloc(sz, mtInternal);
  if (x == NULL) {
    THROW_0(vmSymbols::java_lang_OutOfMemoryError());
  }
  //Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
  return addr_to_java(x);
UNSAFE_END

堆外内存释放

Unsafe.freeMemory(long var1)方法源码:

1
    public native void freeMemory(long var1);

JVM的unsafe.cpp#Unsafe_FreeMemory方法里,通过os::free来实现堆外内存释放。

1
2
3
4
5
6
7
8
UNSAFE_ENTRY(void, Unsafe_FreeMemory(JNIEnv *env, jobject unsafe, jlong addr))
  UnsafeWrapper("Unsafe_FreeMemory");
  void* p = addr_from_java(addr);
  if (p == NULL) {
    return;
  }
  os::free(p);
UNSAFE_END

Bits.unreserveMemory()

1
2
3
4
5
6
static void unreserveMemory(long size, int cap) {
    long cnt = COUNT.decrementAndGet();
    long reservedMem = RESERVED_MEMORY.addAndGet(-size);
    long totalCap = TOTAL_CAPACITY.addAndGet(-cap);
    assert cnt >= 0 && reservedMem >= 0 && totalCap >= 0;
}

回收机制

JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对象的Cleaner对象,这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。

image

其中firstCleaner类的静态变量,Cleaner对象在初始化时会被添加到Clener链表中,和first形成引用关系,ReferenceQueue是用来保存需要回收的Cleaner对象。

image

如果该DirectByteBuffer对象在一次GC中被回收了。只有Cleaner对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次GC时,把该Cleaner对象放入到ReferenceQueue中,并触发clean方法。clean方法把自身从链表删除,等候GC回收;释放堆外内存。

DirectByteBuffer

内部类

DeallocatorDirectByteBuffer的内部类,实现了Runnable,复写run方法,调用Unsafe.freeMemory实现释放堆外内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    private static class Deallocator
        implements Runnable{
        private static Unsafe unsafe = Unsafe.getUnsafe();

        private long address;
        private long size;
        private int capacity;

        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            //释放堆外内存
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
    }

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            //调用Unsafe.allocateMemory方法分配堆外内存
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        //Cleaner.clean()执行时会调用Deallocator.run()方法
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

Cleaner

Cleaner是PhantomReference(虚引用)的子类。

当GC时发现虚引用包装的DirectByteBuffer已经被清除掉,就会把Cleaner放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 "Reference Handler"的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Cleaner extends PhantomReference<Object> {
    //用于保存虚引用的队列
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    private static Cleaner first = null;
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;
    //链表新增节点
    private static synchronized Cleaner add(Cleaner var0) {
        if (first != null) {
            var0.next = first;
            first.prev = var0;
        }
        first = var0;
        return var0;
    }
    //链表删除节点
    private static synchronized boolean remove(Cleaner var0) {
        if (var0.next == var0) {
            return false;
        } else {
            if (first == var0) {
                if (var0.next != null) {
                    first = var0.next;
                } else {
                    first = var0.prev;
                }
            }

            if (var0.next != null) {
                var0.next.prev = var0.prev;
            }

            if (var0.prev != null) {
                var0.prev.next = var0.next;
            }

            var0.next = var0;
            var0.prev = var0;
            return true;
        }
    }
    //构造方法
    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }
    //加入链表
    public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }
    //清除方法
    public void clean() {
        //将自身从链表清除
        if (remove(this)) {
            try {
                //调用Deallocator.run方法,释放堆外内存
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }
                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }
}

Deallocator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static class Deallocator implements Runnable{

    private long address;
    private long size;
    private int capacity;

    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }

    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        UNSAFE.freeMemory(address);
        address = 0;
        //回收直接内存计数
        Bits.unreserveMemory(size, capacity);
    }
}

堆外内存GC

  • -XX:MaxDirectMemorySize=40M:设置堆外内存大小
  • 在使用Netty等会产生堆外内存的框架时需要禁止使用-XX:+DisableExplicitGC参数,它导致了System.gc()等于一个空函数,根本不会触发FGC。
  • 使用CMS垃圾回收器时,可以设置-XX:ExplicitGCInvokesConcurrent参数,使单线程FGC,变为CMS GC。