浅论java文件处理
时间记录:2019-8-22
我们在一开始学习Java的时候相信大部分在使用java的文件处理的时候是十分的抱怨的,总觉得很慢。我们来比较下java处理文件的几种方式,来进行对比下。
java文件处理方式
在最开始的时候出现的文件处理方式为流的方式处理,然后是带缓冲的流处理方式,最后是文件映射的方式来进行处理的。普通的流成为文件流,带缓冲的成为缓冲流,然后就是文件映射,这里还有一种文件的处理方式叫做随机流,随机是针对于文件的数据的位置来说的,我们知道在一个很大的文件的读取的时候,如果我们想要某一个位置的信息的时候,那么需要随机流来将文件读取文件,也就是读取任意位置的数据。
以下处理的文件为CentOS-7-x86_64-Minimal-1810.iso,918M。
文件流
package com.huo.mapfile;
import java.io.File;
import java.io.FileInputStream;
public class Test
{
public static void main(String[] args) throws Exception
{
long time1 = System.currentTimeMillis();
String path = "D:/package/CentOS-7-x86_64-Minimal-1810.iso";
FileInputStream fileInputStream = new FileInputStream(new File(path));
byte[] buffer = new byte[1024];
int num=0;
int sum=0;
while((num = fileInputStream.read(buffer)) !=-1 )
{
sum += num;
}
long time2 = System.currentTimeMillis();
System.out.println("花费的时间为:"+(time2-time1)+" "+(sum/1024/1024));
}
}
结果为
花费的时间为:1296 918
缓冲流
package com.huo.mapfile;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
public class Test
{
public static void main(String[] args) throws Exception
{
long time1 = System.currentTimeMillis();
String path = "D:/package/CentOS-7-x86_64-Minimal-1810.iso";
FileInputStream fileInputStream = new FileInputStream(new File(path));
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[1024];
int num=0;
int sum=0;
while((num = bufferedInputStream.read(buffer)) !=-1 )
{
sum += num;
}
long time2 = System.currentTimeMillis();
System.out.println("花费的时间为:"+(time2-time1)+" "+(sum/1024/1024));
}
}
结果为
花费的时间为:486 918
注意:由于java本身存在预热机制(不记得名字了),以上时间是在运行多次后的结果
我们发现这里使用缓冲和不使用缓冲在时间上的差异十分大,那这是为什么呢?还有我们的缓冲在这里起到了什么作用。
文件映射
package com.huo.mapfile;
import java.io.File;
import java.io.FileInputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class Test
{
public static void main(String[] args) throws Exception
{
long time1 = System.currentTimeMillis();
String path = "D:/package/CentOS-7-x86_64-Minimal-1810.iso";
FileInputStream fileInputStream = new FileInputStream(new File(path));
FileChannel channel = fileInputStream.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
long time2 = System.currentTimeMillis();
System.out.println("花费的时间为:"+(time2-time1)+" "+( buffer.capacity()/1024/1024));
}
}
结果为
花费的时间为:6 918
我们发现这个时间简直是不可想象,相对于其他的两种方式时间几乎是指数的降低。那么这里的内存映射究竟干了什么事呢。
我们发现在将这个文件替换成小的文件的时候会发现这里的时间基本上没有变化,那么这里引起了我们的思考,将文件映射到内存,实际上只是改变了获取文件的数据的方式将I/O操作给替换了,在面对大的文件的操作的时候可使用文件内存映射的方式来处理文件,避免了IO带来的损耗问题,当然在对小文件进行的处理操作还是选择普通的方式就好了。
文件流处理分析
FileInputStream主要的处理可以发现,携带的数据主要是path和fd[FileDescriptor,描述当前文件的一个文件句柄],然后执行一个open函数[native],我们来看下这个方法具体干了什么。
JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
fileOpen(env, this, path, fis_fd, O_RDONLY);
}
path为当前的文件路径 this为当前的对象,也就是FileInputStream fis_fd指的是文件的句柄 O_RDONLY 指的是权限,
这里是只读
继续看fuleopen函数对应的是什么
缓冲流分析
我们知道文件在读取的时候,会会有读写的过程,在读取一定的数量的时候就会进行写,然后在不同的状态中来回进行切换就增加了io的消耗,那么在这中间增加一个缓冲的效果,当读到的内容先放到一个缓冲中,然后在将缓冲中的内容写出,就达到了减少io操作的次数,就提高了速度,但是其本身还是依赖于普通的流读写,只是改变了其中的操作方式而已。
文件映射内存分析
在内存映射文件的时候,会将一个文件映射到虚拟内存中,那么是如何做到的呢?
先进行文件的map操作
public MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException
{
ensureOpen();
if (mode == null)
throw new NullPointerException("Mode is null");
if (position < 0L)
throw new IllegalArgumentException("Negative position");
if (size < 0L)
throw new IllegalArgumentException("Negative size");
if (position + size < 0)
throw new IllegalArgumentException("Position + size overflow");
if (size > Integer.MAX_VALUE)
throw new IllegalArgumentException("Size exceeds Integer.MAX_VALUE");
int imode = -1;
if (mode == MapMode.READ_ONLY)
imode = MAP_RO;
else if (mode == MapMode.READ_WRITE)
imode = MAP_RW;
else if (mode == MapMode.PRIVATE)
imode = MAP_PV;
assert (imode >= 0);
if ((mode != MapMode.READ_ONLY) && !writable)
throw new NonWritableChannelException();
if (!readable)
throw new NonReadableChannelException();
long addr = -1;
int ti = -1;
try {
begin();
ti = threads.add();
if (!isOpen())
return null;
long mapSize;
int pagePosition;
synchronized (positionLock) {
long filesize;
do {
filesize = nd.size(fd);
} while ((filesize == IOStatus.INTERRUPTED) && isOpen());
if (!isOpen())
return null;
if (filesize < position + size) { // Extend file size
if (!writable) {
throw new IOException("Channel not open for writing " +
"- cannot extend file to required size");
}
int rv;
do {
rv = nd.truncate(fd, position + size);
} while ((rv == IOStatus.INTERRUPTED) && isOpen());
if (!isOpen())
return null;
}
if (size == 0) {
addr = 0;
// a valid file descriptor is not required
FileDescriptor dummy = new FileDescriptor();
if ((!writable) || (imode == MAP_RO))
return Util.newMappedByteBufferR(0, 0, dummy, null);
else
return Util.newMappedByteBuffer(0, 0, dummy, null);
}
pagePosition = (int)(position % allocationGranularity);
long mapPosition = position - pagePosition;
mapSize = size + pagePosition;
try {
// If map0 did not throw an exception, the address is valid
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError x) {
// An OutOfMemoryError may indicate that we've exhausted
// memory so force gc and re-attempt map
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException y) {
Thread.currentThread().interrupt();
}
try {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError y) {
// After a second OOME, fail
throw new IOException("Map failed", y);
}
}
} // synchronized
// On Windows, and potentially other platforms, we need an open
// file descriptor for some mapping operations.
FileDescriptor mfd;
try {
mfd = nd.duplicateForMapping(fd);
} catch (IOException ioe) {
unmap0(addr, mapSize);
throw ioe;
}
assert (IOStatus.checkAll(addr));
assert (addr % allocationGranularity == 0);
int isize = (int)size;
Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
if ((!writable) || (imode == MAP_RO)) {
return Util.newMappedByteBufferR(isize,
addr + pagePosition,
mfd,
um);
} else {
return Util.newMappedByteBuffer(isize,
addr + pagePosition,
mfd,
um);
}
} finally {
threads.remove(ti);
end(IOStatus.checkAll(addr));
}
}
然后就是map0[native,这个还是在不同的平台上是不一样的,所以说java的跨平台还不是真的跨平台,只是不同的平台所提供的基础内容不一样,在上层表现的是跨平台而已]的函数
----linux下的
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len)
{
void *mapAddress = 0;
jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
jint fd = fdval(env, fdo);
int protections = 0;
int flags = 0;
if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
protections = PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_PRIVATE;
}
mapAddress = mmap64( /* 这里实际上是mmap,由于上面定义宏,至于为什么要这样修改还是不明白*/
0, /* Let OS decide location */
len, /* Number of bytes to map */
protections, /* File permissions */
flags, /* Changes are shared */
fd, /* File descriptor of mapped file */
off); /* Offset into file */
if (mapAddress == MAP_FAILED) {
if (errno == ENOMEM) {
JNU_ThrowOutOfMemoryError(env, "Map failed");
return IOS_THROWN;
}
return handle(env, -1, "Map failed");
}
return ((jlong) (unsigned long) mapAddress);
}
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len)
{
void *mapAddress = 0;
jint lowOffset = (jint)off;
jint highOffset = (jint)(off >> 32);
jlong maxSize = off + len;
jint lowLen = (jint)(maxSize);
jint highLen = (jint)(maxSize >> 32);
jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
HANDLE fileHandle = (HANDLE)(handleval(env, fdo));
HANDLE mapping;
DWORD mapAccess = FILE_MAP_READ;
DWORD fileProtect = PAGE_READONLY;
DWORD mapError;
BOOL result;
if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
fileProtect = PAGE_READONLY;
mapAccess = FILE_MAP_READ;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
fileProtect = PAGE_READWRITE;
mapAccess = FILE_MAP_WRITE;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
fileProtect = PAGE_WRITECOPY;
mapAccess = FILE_MAP_COPY;
}
mapping = CreateFileMapping(
fileHandle, /* Handle of file */
NULL, /* Not inheritable */
fileProtect, /* Read and write */
highLen, /* High word of max size */
lowLen, /* Low word of max size */
NULL); /* No name for object */
if (mapping == NULL) {
JNU_ThrowIOExceptionWithLastError(env, "Map failed");
return IOS_THROWN;
}
mapAddress = MapViewOfFile(
mapping, /* Handle of file mapping object */
mapAccess, /* Read and write access */
highOffset, /* High word of offset */
lowOffset, /* Low word of offset */
(DWORD)len); /* Number of bytes to map */
mapError = GetLastError();
result = CloseHandle(mapping);
if (result == 0) {
JNU_ThrowIOExceptionWithLastError(env, "Map failed");
return IOS_THROWN;
}
if (mapAddress == NULL) {
if (mapError == ERROR_NOT_ENOUGH_MEMORY)
JNU_ThrowOutOfMemoryError(env, "Map failed");
else
JNU_ThrowIOExceptionWithLastError(env, "Map failed");
return IOS_THROWN;
}
return ptr_to_jlong(mapAddress);
}
总结: 我们发现所有的文件的操作都是通过底层的操作系统的支持方式来做的,而且在所有的操作中,都是通过直接内存来进行操作的,从文件的读写就读来说也是直接通过直接内存来进行的,有直接内存是属于堆外的内存在操作效率上远远高于堆内的内存,至于为什么说java文件操作效率低下,个人觉的是在JNI的数据传输上面导致的{之前有同事进行过测试,在底层进行一个数据的操作,然后返回给java,和直接进行操作方式上时间差很大},而且在使用流读取数据的时候使用的大部分都是普通的数组或者是buffer,然后在操作时使用的还是直接内存,但是在那这种的频繁的文件操作时,会直接malloc一段内存,这个是耗时的,然后将这个内存再put到非直接内存上面,这个耗时也是非常的严重的(之前有进行过测试在40倍,在12M左右的数据,其余的数据有兴趣可以自行测试)。由于jvm的内存结构的设计,在很大程度上限制了自由。
由于笔者对c的部分不是很了解,可能很多内容不正确,仅供参考。
提醒:上面的代码有的部分是sun包下的,需要自己去下openjdk,native部分的代码也在openjdk下。
时间记录:2019-8-22