【发布时间】:2019-10-17 13:23:21
【问题描述】:
我有一个 5GB 大小的文件,我想按块读取它,比如 2MB。使用java.io.InputStream 工作正常。所以我对这个东西进行了如下测量:
static final byte[] buffer = new byte[2 * 1024 * 1024];
public static void main(String args[]) throws IOException {
while(true){
InputStream is = new FileInputStream("/tmp/log_test.log");
long bytesRead = 0;
int readCurrent;
long start = System.nanoTime();
while((readCurrent = is.read(buffer)) > 0){
bytesRead += readCurrent;
}
long end = System.nanoTime();
System.out.println(
"Bytes read = " + bytesRead + ". Time elapsed = " + (end - start)
);
}
}
结果 = 2121714428
可以看出平均需要2121714428纳秒。之所以如此,是因为实现将数据的(*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf); 读入malloced 或堆栈分配的缓冲区,如here 所示。所以memcpy 占用了大量的 CPU 时间:
由于 JNI 规范定义了
在临界区内,本地代码不得调用其他 JNI 函数,或任何可能导致当前线程 阻塞并等待另一个 Java 线程。 (例如,当前 线程不得在另一个 Java 写入的流上调用 read 线程。)
我认为从关键部分中的常规文件读取没有任何问题。从常规文件中读取只会被短暂阻塞,并且不依赖于任何 java 线程。像这样的:
static final byte[] buffer = new byte[2 * 1024 * 1024];
public static void main(String args[]) throws IOException {
while (true) {
int fd = open("/tmp/log_test.log");
long bytesRead = 0;
int readCurrent;
long start = System.nanoTime();
while ((readCurrent = read(fd, buffer)) > 0) {
bytesRead += readCurrent;
}
long end = System.nanoTime();
System.out.println("Bytes read = " + bytesRead + ". Time elapsed = " + (end - start));
}
}
private static native int open(String path);
private static native int read(int fd, byte[] buf);
JNI 函数:
JNIEXPORT jint JNICALL Java_com_test_Main_open
(JNIEnv *env, jclass jc, jstring path){
const char *native_path = (*env)->GetStringUTFChars(env, path, NULL);
int fd = open(native_path, O_RDONLY);
(*env)->ReleaseStringUTFChars(env, path, native_path);
return fd;
}
JNIEXPORT jint JNICALL Java_com_test_Main_read
(JNIEnv *env, jclass jc, jint fd, jbyteArray arr){
size_t java_array_size = (size_t) (*env)->GetArrayLength(env, arr);
void *buf = (*env)->GetPrimitiveArrayCritical(env, arr, NULL);
ssize_t bytes_read = read(fd, buf, java_array_size);
(*env)->ReleasePrimitiveArrayCritical(env, arr, buf, 0);
return (jint) bytes_read;
}
结果 = 1179852225
在循环中运行它平均需要 1179852225 纳秒,这几乎是效率的两倍。
问题:从临界区中的常规文件读取的实际问题是什么?
【问题讨论】:
-
InputStream没有缓冲,存在上下文切换并且读取文件系统是(潜在的)阻塞操作。而不是 JNI,您可能应该使用 nio 来读取您的文件。 -
任何不会导致当前线程阻塞并等待另一个Java线程;主要是因为你可以通过这种方式使 JVM 崩溃(其他线程不知道通知阻塞的本机线程)。
-
我不会在生产代码中使用“可能不危险”的东西。你需要确定。但是......嘿......我们只能提供建议。责任在你。
-
但这里是一个“例如”。考虑如果您正在阅读的文件位于网络共享或 NFS 服务器上会发生什么。在这种情况下,操作系统可能需要通过网络获取数据。可能需要很长时间。事实上,它甚至可能需要无限长的时间......如果服务器出现故障等。一直以来,您的代码都处于“关键区域”,并且可能会阻塞 GC 等等。
-
即使“直接读入数组”也可能是“从底层 fs 缓冲区复制”操作,所以当您确信源是本地文件时,您可能更喜欢使用内存映射,结合复制到
HeapByteBuffer。这仍然承担着不可避免的复制操作,但避免了中间DirectByteBuffer引入的额外复制操作。
标签: java performance io jvm inputstream