Java知识梳理四

一、Java中的文件复制

1.Java IO实现文件复制

       利用java.io类库,直接为源文件构建一个FileInputStream读取,然后再为目标文件构建一个FileOutputStream,完成写入工作。示例代码如下:

 public static void copyFileByStream(File source, File dest) throws IOException {
     try (InputStream is = new FileInputStream(source);
         OutputStream os = new FileOutputStream(dest);){
         byte[] buffer = new byte[1024];
         int length;
         while ((length = is.read(buffer)) > 0) {
                os.write(buffer, 0, length);
         }
     }
 }
2.Java NIO实现文件复制

       利用java.nio类库提供的transferTo或transferFrom方法实现。示例代码如下:

    public static void copyFileByChannel(File source, File dest) throws IOException {
        try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
             FileChannel targetChannel = new FileOutputStream(dest).getChannel();){
                for (long count = sourceChannel.size() ;count>0 ;) {
                long transferred = sourceChannel.transferTo(
                    sourceChannel.position(), count, targetChannel);
                    sourceChannel.position(sourceChannel.position() + transferred);
                    count -= transferred;
            }
        }
    }

       当然,Java标准类库本身已经提供了几种Files.copy的实现。对于Copy的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From的方式 可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。

3.复制机制的分析

       首先,你需要理解用户态空间(User Space)和内核态空间(Kernel Space),这是操作系统层面的基本概念,操作系统内核、硬件驱动等运行在内核态空间,具有相对高的特权;而用户态空间,则是给普通应用和服务使用。你可以参考:https://en.wikipedia.org/wiki/User_space。当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。写入操作也是类似,仅仅是步骤相反。所以,这种方式会带来一定的额外开销,可能会降低IO效率。
       而基于NIOtransferTo的实现方式,在Linux和Unix上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。注意,transferTo不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行Socket发送,同样可以享受这种机制带来的性能和扩展性提高。JDK的源代码中,内部实现和公共API定义也不是可以能够简单关联上的,NIO部分代码甚至是定义为模板而不是Java源文件,在build过程自动生成源码,简单绍一下部分JDK代码机制和如何绕过隐藏障碍。
       • 首先,直接跟踪,发现FileSystemProvider只是个抽象类,阅读它的源码能够理解到,原来文件系统实际逻辑存在于JDK内部实现里,公共API其实是通过ServiceLoader机制加载一系列文件系统实现,然后提供服务。
       • 我们可以在JDK源码里搜索FileSystemProvider和nio,可以定位到sun/nio/fs,我们知道NIO底层是和操作系统紧密相关的,所以每个平台都有自己的部分特有文件系统逻辑。

image

       • 省略掉一些细节,最后我们一步步定位到UnixFileSystemProvider → UnixCopyFile.Transfer,发现这是个本地方法。
       • 最后,明确定位到UnixCopyFile.c,其内部实现清楚说明竟然只是简单的用户态空间拷贝!
       所以,我们明确这个最常见的copy方法其实不是利用transferTo,而是本地技术实现的用户态拷贝。如何提高类似拷贝等IO操作的性能,有一些宽泛的原则:在程序中,使用缓存等机制,合理减少IO次数(在网络通信中,如TCP传输,window大小也可以看作是类似思路);使用transferTo等机制,减少上下文切换和额外IO操作;尽量减少不必要的转换过程,比如编解码;对象序列化和反序列化,比如操作文本文件或者网络通信,如果不是过程中需要使用文本信息,可以考虑不要将二进制信息转换成字符串,直接传输二进制信息。

4.掌握NIO Buffer

       Java为每种原始数据类型都提供了相应的Buffer实现(布尔除外),所以掌握和使用Buffer是十分必要的,尤其是涉及Direct Buffer等使用,因为其在垃圾收集等方面的特殊性,更要重点掌握。

image

Buffer有几个基本属性:
       • capcity,它反映这个Buffer到底有多大,也就是数组的长度。
       • position,要操作的数据起始位置。
       • limit,相当于操作的限额。在读取或者写入时,limit的意义很明显是不一样的。比如,读取操作时,很可能将limit设置到所容纳数据的上限;而在写入时,则会设置容量或容量以下的可写限度。
       • mark,记录上一次postion的位置,默认是0,算是一个便利性的考虑,往往不是必须的。
       简单梳理下Buffer的基本操作:我们创建了一个ByteBuffer,准备放入数据,capcity当然就是缓冲区大小,而position就是0,limit默认就是capcity的大小;当我们写入几个字节的数据时,position就会跟着水涨船高,但是它不可能超过limit的大小;如果我们想把前面写入的数据读出来,需要调用flip方法,将position设置为0,limit设置为以前的position那里;如果还想从头再读一遍,可以调用rewind,让limit不变,position再次设置为0。

4.Direct Buffer和垃圾收集

       • Direct Buffer:如果我们看Buffer的方法定义,你会发现它定义了isDirect()方法,返回当前Buffer是否是Direct类型。这是因为Java提供了堆内和堆外(Direct)Buffer,我们可以以它的allocate或者allocateDirect方法直接创建。
       • MappedByteBuffer:它将文件按照指定大小直接映射为内存区域,当程序访问这个内存区域时将直接操作这块儿文件数据,省去了将数据从内核空间向用户空间传输的损耗。我们可以使用FileChannel.map创建MappedByteBuffer,它本质上也是种Direct Buffer。在实际使用中,Java会尽量对Direct Buffer仅做本地IO操作,对于很多大数据量的IO密集操作,可能会带来非常大的性能优势,因为:
       • Direct Buffer生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多IO操作会很高效。
       • 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高。
        但是请注意,Direct Buffer创建和销毁过程中,都会比一般的堆内Buffer增加部分开销,所以通常都建议用于长期使用、数据较大的场景。使用Direct Buffer,我们需要清楚它对内存和JVM参数的影响。首先,因为它不在堆上,所以Xmx之类参数,其实并不能影响DirectBuffer等堆外成员所使用的内存额度,我们可以使用下面参数设置大小:

-XX:MaxDirectMemorySize=512M

        从参数设置和内存问题排查角度来看,这意味着我们在计算Java可以使用的内存大小的时候,不能只考虑堆的需要,还有Direct Buffer等一系列堆外因素。如果出现内存不足,堆外内存占用也是一种可能性。另外,大多数垃圾收集过程中,都不会主动收集Direct Buffer,它的垃圾收集过程,就是基于我在专栏前面所介绍的Cleaner(一个内部实现)和幻象引用(PhantomReference)机制,其本身不是public类型,内部实现了一个Deallocator负责销毁的逻辑。对它的销毁往往要拖到full GC的时候,所以使用不当很容易导致OutOfMemoryError。对于Direct Buffer的回收:
       • 在应用程序中,显式地调用System.gc()来强制触发。
       • 另外一种思路是,在大量使用Direct Buffer的部分框架中,框架会自己在程序中调用释放方法,Netty就是这么做的,有兴趣可以参考其实现(PlatformDependent0)。
       • 重复使用Direct Buffer。

5.跟踪和诊断Direct Buffer内存占用?

       因为通常的垃圾收集日志等记录,并不包含Direct Buffer等信息,所以Direct Buffer内存诊断也是个比较头疼的事情。幸好,在JDK8之后的版本,我们可以方便地使用Native Memory Tracking(NMT)特性来进行诊断,你可以在程序启动时加上下面参数:

    -XX:NativeMemoryTracking={summary|detail}

       注意,激活NMT通常都会导致JVM出现5%~10%的性能下降,请谨慎考虑。运行时,可以采用下面命令进行交互式对比:

    // 打印NMT信息
    jcmd <pid> VM.native_memory detail 
    // 进行baseline,以对比分配内存变化
    jcmd <pid> VM.native_memory baseline
    // 进行baseline,以对比分配内存变化
    jcmd <pid> VM.native_memory detail.diff

       可以在Internal部分发现Direct Buffer内存使用的信息,这是因为其底层实际是利用unsafe_allocatememory。严格说,这不是JVM内部使用的内存,所以在JDK11以后,其实它是归类在other部分里。JDK 9的输出片段如下,“+”表示的就是diff命令发现的分配变化:

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

推荐阅读更多精彩内容