Java IO

传统流式IO

传统的Java IO是流式的IO,从诸如类名InputStreamOutputStream中就可以看出。流式IO是单向的,分为输入和输出流。在使用输入流或者输出流读写文件时,每次读写操作是以字节为单位,我们需要指定读出或者写入的大小,中间没有任何用户空间的缓存。例如从文件中读取4字节长度的数据,Java会创建一个4字节长度的byte数组,然后通过JNI层经由系统调用read读文件,每次读入一个字节的数据,将数据写入对应的byte数组的正确位置。一共需要进行4次系统调用,因为每次我们只能读入一个字节。随着文件读入的进行,我们没有办法重新访问我们已经读入的数据,因为流是单向的,我们不能seek某个位置,除非我们自己将这些已经读入的数据进行了缓存,才能在以后需要时进行访问。当我们写数据的时候也是如此,我们每次只能写入一个字节的数据,写4个字节的数据就需要4次系统调用。系统调用需要从用户态切换到内核态,然后再切换回来,可想而知,流式的IO的读写性能开销是很大的。如下是Java中流式IO读写的实现,从代码中我们就可以印证上面的事实。

//java.io.InputStream#read(byte[], int, int)
public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read(); //每次只读一个字节
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }
//java.io.OutputStream#write(byte[], int, int)
public void write(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if ((off < 0) || (off > b.length) || (len < 0) ||
                   ((off + len) > b.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]); //每次只写一个字节
        }
    }

NIO

JDK 1.4之后,java引入了NIO。NIO以块为单位进行读写,而不是以单字节为单位。Channel在NIO中代表一个通道,我们可以操作通道进行读写,换句话说,通道是双向的。通过NIO读写文件,我们并不是直接操作通道,而是通过Buffer来中转。Buffer代表一块缓冲区,其实就是一个字节数组。当我们要写文件时,首先将数据写入对应的buffer中,然后通过channel将buffer中的数据写入文件。而当我们需要读入数据时,也是首先将数据读入一个Buffer中,然后从buffer中访问。因为每次操作是以块为单位的,因此我们能大大减少系统调用的次数,极大的提高IO性能。同时Buffer作为一个缓冲区也允许我们在之后的某段时间内重新访问之前的数据,Buffer内部会自己维护数据的位置信息,如positionlimitcapacity等。

DirectByteBuffer vs HeapByteBuffer

ByteBuffer代表一个字节数组的缓冲区。Java提供了direct和non-direct buffer。java.nio.ByteBuffer#allocate会创建一个HeapByteBuffer,即分配在jvm heap上的一个字节数组。而通过java.nio.ByteBuffer#allocateDirect方法返回一个DirectByteBuffer对象,它也是封装了一个字节数组,但是这个字节数组并不是直接分配在通用的jvm heap上的,而是另外一块单独的内存区域中(人们喜欢将之称为堆外内存),在不同的虚拟机版本可能有不同的实现。例如ART运行时,会有一个heap之外的区域,我理解为大对象区域,这个区域主要用来分配一些大对象,如Bitmap,DirectByteeBuffer等。我们都知道大对象对jvm的GC会造成一些影响,所以单独开辟这些区域用来存储一些生命周期长的大对象是有道理,可以减少正常GC的次数,提高内存效率。
在进行NIO时,我们可以通过DirectByteBuffer提高IO性能。官方的原话是:

A byte buffer is either <i>direct</i> or <i>non-direct</i>. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.

到底是怎么个性能提高法呢?还是看代码更清晰。

//IOUtil.java 
static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if(var1 instanceof DirectBuffer) {
            return writeFromNativeBuffer(var0, var1, var2, var4); //如果是directbytebuffer,直接写
        } else {
            int var5 = var1.position();
            int var6 = var1.limit();

            assert var5 <= var6;

            int var7 = var5 <= var6?var6 - var5:0;
            ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); //获取一个临时的directbytebuffer

            int var10;
            try {
                var8.put(var1); //复制数据到directbytebuffer之后再写
                var8.flip();
                var1.position(var5);
                int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
                if(var9 > 0) {
                    var1.position(var5 + var9);
                }

                var10 = var9;
            } finally {
                Util.offerFirstTemporaryDirectBuffer(var8);
            }

            return var10;
        }
    }

    static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if(var1.isReadOnly()) {
            throw new IllegalArgumentException("Read-only buffer");
        } else if(var1 instanceof DirectBuffer) { //是directbytebuffer 直接读入
            return readIntoNativeBuffer(var0, var1, var2, var4);
        } else {
            ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining()); //获取一个临时的directbytebuffer

            int var7;
            try {
                int var6 = readIntoNativeBuffer(var0, var5, var2, var4); //读入数据到directbytebuffer中
                var5.flip();
                if(var6 > 0) {
                    var1.put(var5); //拷贝数据到目标buffer中
                }

                var7 = var6;
            } finally {
                Util.offerFirstTemporaryDirectBuffer(var5);
            }

            return var7;
        }
    }

在使用NIO进行读写的时候,最终会调用IOUtil中的相关read、write方法。可以看到如果是DirectByteBuffer,在IO时直接在该buffer上进行读写。如果不是,则需要获取一个临时的DirectByteBuffer(jvm从directbytebuffer cache中获取),将数据拷贝到directbytebuffer中再写入或者读入directbuffer中在拷贝到目标Buffer中。可以看到,如果是DirectByteBuffer,那么可以省去了很多拷贝的开销。那么jvm为什么需要一个中间的DirectByteBuffer缓冲区呢?我的猜想是普通的buffer是分配在heap上的,可能是内存空间不连续的字节数组,而且随着程序的运行 GC可能会移动对应的字节数组,这就给IO带来了挑战。反观DirectByteBuffer,它是连续的字节数组,不是分配在堆上的,受GC影响小,而且一般而言DirectByteBuffer分配内存都是指定non-movale的。但是DirectByteBuffer也不是没有任何缺点,因为它不是在堆上的,所以可能造成访问速度慢,并且DirectByteBuffer的分配和释放开销比HeapByteBuffer要大。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容