

JVM源码分析之不可控的堆外内存 | PerfMa应用性能技术社区
source link: https://club.perfma.com/article/142660?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

之前写过篇文章,关于堆外内存的,JVM源码分析之堆外内存完全解读,里面重点讲了DirectByteBuffer的原理,但是今天碰到一个比较奇怪的问题,在设置了-XX:MaxDirectMemorySize=1G的前提下,然后统计所有DirectByteBuffer对象后面占用的内存达到了7G,远远超出阈值,这个问题很诡异,于是好好查了下原因,虽然最终发现是我们统计的问题,但是期间发现的其他一些问题还是值得分享一下的。
不得不提的DirectByteBuffer构造函数
打开DirectByteBuffer这个类,我们会发现有5个构造函数
DirectByteBuffer(int cap);
DirectByteBuffer(long addr, int cap, Object ob);
private DirectByteBuffer(long addr, int cap);
protected DirectByteBuffer(int cap, long addr,FileDescriptor fd,Runnable unmapper);
DirectByteBuffer(DirectBuffer db, int mark, int pos, int lim, int cap,int off)
我们从java层面创建DirectByteBuffer对象,一般都是通过ByteBuffer的allocateDirect方法
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
也就是会使用上面提到的第一个构造函数,即
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(size, cap)方法会做堆外内存的阈值check
static void reserveMemory(long size, int cap) {
synchronized (Bits.class) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
if (cap <= maxMemory - totalCapacity) {
reservedMemory += size;
totalCapacity += cap;
count++;
return;
}
}
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
if (totalCapacity + cap > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
totalCapacity += cap;
count++;
}
}
因此当我们已经分配的内存超过阈值的时候会触发一次gc动作,并重新做一次分配,如果还是超过阈值,那将会抛出OOM,因此分配动作会失败。
所以从这一切看来,只要设置了-XX:MaxDirectMemorySize=1G
是不会出现超过这个阈值的情况的,会看到不断的做GC。
构造函数再探
那其他的构造函数主要是用在什么情况下的呢?
我们知道DirectByteBuffer回收靠的是里面有个cleaner的属性,但是我们发现有几个构造函数里cleaner这个属性却是null,那这种情况下他们怎么被回收呢?
那下面请大家先看下DirectByteBuffer里的这两个函数:
public ByteBuffer slice() {
int pos = this.position();
int lim = this.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
int off = (pos << 0);
assert (off >= 0);
return new DirectByteBuffer(this, -1, 0, rem, rem, off);
}
public ByteBuffer duplicate() {
return new DirectByteBuffer(this,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
0);
}
从名字和实现上基本都能猜出是干什么的了,slice其实是从一块已知的内存里取出剩下的一部分,用一个新的DirectByteBuffer对象指向它,而duplicate就是创建一个现有DirectByteBuffer的全新副本,各种指针都一样。
因此从这个实现来看,后面关联的堆外内存其实是同一块,所以如果我们做统计的时候如果仅仅将所有DirectByteBuffer对象的capacity加起来,那可能会导致算出来的结果偏大不少,这其实也是我查的那个问题,本来设置了阈值1G,但是发现达到了7G的效果。所以这种情况下使用的构造函数,可以让cleaner为null,回收靠原来的那个DirectByteBuffer对象被回收。
被遗忘的检查
但是还有种情况,也是本文要讲的重点,在jvm里可以通过jni方法回调上面的DirectByteBuffer构造函数,这个构造函数是
private DirectByteBuffer(long addr, int cap) {
super(-1, 0, cap, cap);
address = addr;
cleaner = null;
att = null;
}
而调用这个构造函数的jni方法是 jni_NewDirectByteBuffer
extern "C" jobject JNICALL jni_NewDirectByteBuffer(JNIEnv *env, void* address, jlong capacity)
{
// thread_from_jni_environment() will block if VM is gone.
JavaThread* thread = JavaThread::thread_from_jni_environment(env);
JNIWrapper("jni_NewDirectByteBuffer");
#ifndef USDT2
DTRACE_PROBE3(hotspot_jni, NewDirectByteBuffer__entry, env, address, capacity);
#else /* USDT2 */
HOTSPOT_JNI_NEWDIRECTBYTEBUFFER_ENTRY(
env, address, capacity);
#endif /* USDT2 */
if (!directBufferSupportInitializeEnded) {
if (!initializeDirectBufferSupport(env, thread)) {
#ifndef USDT2
DTRACE_PROBE1(hotspot_jni, NewDirectByteBuffer__return, NULL);
#else /* USDT2 */
HOTSPOT_JNI_NEWDIRECTBYTEBUFFER_RETURN(
NULL);
#endif /* USDT2 */
return NULL;
}
}
// Being paranoid about accidental sign extension on address
jlong addr = (jlong) ((uintptr_t) address);
// NOTE that package-private DirectByteBuffer constructor currently
// takes int capacity
jint cap = (jint) capacity;
jobject ret = env->NewObject(directByteBufferClass, directByteBufferConstructor, addr, cap);
#ifndef USDT2
DTRACE_PROBE1(hotspot_jni, NewDirectByteBuffer__return, ret);
#else /* USDT2 */
HOTSPOT_JNI_NEWDIRECTBYTEBUFFER_RETURN(
ret);
#endif /* USDT2 */
return ret;
}
想象这么种情况,我们写了一个native方法,里面分配了一块内存,同时通过上面这个方法和一个DirectByteBuffer对象关联起来,那从java层面来看这个DirectByteBuffer确实是一个有效的占有不少native内存的对象,但是这个对象后面关联的内存完全绕过了MaxDirectMemorySize的check,所以也可能给你造成这种现象,明明设置了MaxDirectMemorySize,但是发现DirectByteBuffer关联的堆外内存其实是大于它的。
Recommend
-
69
-
60
-
36
-
62
背景 组内一个项目最近一直报swap区域使用过高异常,笔者被叫去帮忙查看原因。发现配置的4G堆内内存,但是实际使用的物理内存高达7G,确实有点不正常,JVM参数配置是“-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+A...
-
53
导读 Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程。 Netty 底层基于 JDK 的 NIO,我们为什么不直接基于 J...
-
36
-
85
-
59
-
49
记得那时一个风和日丽的周末,太阳红彤彤,花儿五颜六色,96 年的普哥微信找到我,描述了一个诡异的线上问题:线上程序使用了 NIO FileChannel 的 堆内内存作为缓冲区,读写文件,逻辑可以说相当简单,但根据监控却发现堆外内存飙升,导致...
-
38
涤生的博客 转载请注明原创出处,谢谢 如果读完觉得有收获的话,欢迎点赞加关注 堆外内存简介 DirectByteBuffer 这个类是 JDK 提供使用堆外内存的一种途径,当然常见的业务开发一般不会接触到,即...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK