Java DIRECT IO

java 10 支持了DIRECT IO,可以绕过page cache直接写文件。

1
2
Path path = Paths.get("test.txt");
FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE, ExtendedOpenOption.DIRECT);

DIRECT IO需要对齐,但是他有一些非常微妙的地方。

那些地方需要对齐:

1
2
3
Util.checkChannelPositionAligned(position(), alignment);
Util.checkBufferPositionAligned(bb, pos, alignment);
Util.checkRemainingBufferSizeAligned(rem, alignment);

分别是:

  1. 文件写入位置
  2. directBuffer起始地址
  3. directBuffer中剩余数据的长度

他们都必须是alignment的整数倍。

按多少字节对齐?

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
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_setDirect0(JNIEnv *env, jclass clazz,
jobject fdo)
{
jint fd = fdval(env, fdo);
jint result;
#ifdef MACOSX
struct statvfs file_stat;
#else
struct statvfs64 file_stat;
#endif

#ifdef MACOSX
result = fstatvfs(fd, &file_stat);
#else
result = fstatvfs64(fd, &file_stat);
#endif
if(result == -1) {
JNU_ThrowIOExceptionWithLastError(env, "DirectIO setup failed");
return result;
} else {
result = (int)file_stat.f_frsize;
}
#else
result == -1;
#endif
return result;
}

statvfs.frsize是文件系统的逻辑块大小,可能包含多个物理块。

unsigned long f_frsize; /* Fragment size */

如何获得起始起始位置是对齐的directBuffer

系统分配的内存虚拟地址并不确定,需要分配大一些的空间(pagesize + capcity),只使用其中的一部分。

1
long size = Math.max(1L, (long)cap + (pa ? ps : 0));

假如设置了-Dsun.nio.PageAlignDirectMemory=true参数会自动对齐。

注:这场jdk里的注释是错的,-XX:MaxDirectMemorySize=没有任何作用

这里DirectByteBuffer会把整段分配的内存写0,而不仅仅是用到的部分。导致没用到的虚拟内存也会分配物理内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

假设分配一页内存,虚拟地址横跨两页,最终会导致分配两倍的物理内存。如果分配小量内存或者开启大页情况可能会更严重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Hello {
static final int _4K = 4096;
static Object[] save = new Object[256 * 1024];
public static void main(String[] args) {
for (int i = 0; i < 256 * 1024; i++) {
ByteBuffer buffer = ByteBuffer.allocateDirect(_4K);
save[i] = buffer;
}
try {
new CountDownLatch(1).await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

这份代码不带对齐参数占用的物理内存约为1G,而加上-Dsun.nio.PageAlignDirectMemory=true将占用2G。

1
2
3
4
5
6
7
$ java -XX:NativeMemoryTracking=summary -XX:MaxDirectMemorySize=1g -Dsun.nio.PageAlignDirectMemory=true Hello
$ jcmd 29084 VM.native_memory
29084:
Native Memory Tracking:
Total: reserved=7840547KB, committed=2470423KB
- Other (reserved=2097152KB, committed=2097152KB)
(malloc=2097152KB #262144)

注意到-XX:MaxDirectMemorySize=1g,但是DirectMemory远远超出了1g。

MaxDirectMemorySize的逻辑

Bits.tryReserveMemory是按分配的容量算的,而不是实际分配的内存大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
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;
}

而在开启对齐的时候size = cap + pageSize,显然size要比cap大得多。