今天听了杨晓峰老师的java 36讲,感受IO这块是特别欠缺的,因此讲义摘录以下:html
欢迎你们去订阅:java
本文章转自:https://time.geekbang.org/column/article/8369linux
IO 一直是软件开发中的核心部分之一,伴随着海量数据增加和分布式系统的发展,IO 扩展能力愈发重要。幸运的是,Java 平台 IO 机制通过不断完善,虽然在某些方面仍有不足,但已经在实践中证实了其构建高扩展性应用的能力。面试
今天我要问你的问题是,Java 提供了哪些 IO 方式? NIO 如何实现多路复用?编程
Java IO 方式有不少种,基于不一样的 IO 抽象模型和交互方式,能够进行简单区分。windows
首先,传统的 java.io 包,它基于流模型实现,提供了咱们最熟知的一些 IO 功能,好比 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动做完成以前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。api
java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。数组
不少时候,人们也把 java.net 下面提供的部分网络 API,好比 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,由于网络通讯一样是 IO 行为。缓存
第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,能够构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操做系统底层的高性能数据操做方式。安全
第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有不少人叫它 AIO(Asynchronous IO)。异步 IO 操做基于事件和回调机制,能够简单理解为,应用操做直接返回,而不会阻塞在那里,当后台处理完成,操做系统会通知相应线程进行后续工做。
我上面列出的回答是基于一种常见分类方式,即所谓的 BIO、NIO、NIO 2(AIO)。
在实际面试中,从传统 IO 到 NIO、NIO 2,其中有不少地方能够扩展开来,考察点涉及方方面面,好比:
基础 API 功能与设计, InputStream/OutputStream 和 Reader/Writer 的关系和区别。
NIO、NIO 2 的基本组成。
给定场景,分别用不一样模型实现,分析 BIO、NIO 等模式的设计和实现原理。
NIO 提供的高性能数据操做方式是基于什么原理,如何使用?
或者,从开发者的角度来看,你以为 NIO 自身实现存在哪些问题?有什么改进的想法吗?
IO 的内容比较多,专栏一讲很难可以说清楚。IO 不只仅是多路复用,NIO 2 也不只仅是异步 IO,尤为是数据操做部分,会在专栏下一讲详细分析。
首先,须要澄清一些基本概念:
区分同步或异步(synchronous/asynchronous)。简单来讲,同步是一种可靠的有序运行机制,当咱们进行同步操做时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其余任务不须要等待当前调用返回,一般依靠事件、回调等机制来实现任务间次序关系。
区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操做时,当前线程会处于阻塞状态,没法从事其余任务,只有当条件就绪才能继续,好比 ServerSocket 新链接创建完毕,或数据读取、写入操做完成;而非阻塞则是无论 IO 操做是否结束,直接返回,相应操做在后台继续处理。
不能一律而论认为同步或阻塞就是低效,具体还要看应用和系统特征。
对于 java.io,咱们都很是熟悉,我这里就从整体上进行一下总结,若是须要学习更加具体的操做,你能够经过教程等途径完成。整体上,我认为你至少须要理解:
IO 不只仅是对文件的操做,网络编程中,好比 Socket 通讯,都是典型的 IO 操做目标。
输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操做图片文件。
而 Reader/Writer 则是用于操做字符,增长了字符编解码等功能,适用于相似从文件中读取或者写入文本信息。本质上计算机操做的都是字节,无论是网络通讯仍是文件读取,Reader/Writer 至关于构建了应用逻辑和原始数据之间的桥梁。
BufferedOutputStream 等带缓冲区的实现,能够避免频繁的磁盘读写,进而提升 IO 处理效率。这种设计利用了缓冲区,将批量数据进行一次操做,但在使用中千万别忘了 flush。
参考下面这张类图,不少 IO 工具类都实现了 Closeable 接口,由于须要进行资源的释放。好比,打开 FileInputStream,它就会获取相应的文件描述符(FileDescriptor),须要利用 try-with-resources、 try-finally 等机制保证 FileInputStream 被明确关闭,进而相应文件描述符也会失效,不然将致使资源没法被释放。利用专栏前面的内容提到的 Cleaner 或 finalize 机制做为资源释放的最后把关,也是必要的。
下面是我整理的一个简化版的类图,阐述了平常开发应用较多的类型和结构关系。
1.Java NIO 概览
首先,熟悉一下 NIO 的主要组成部分:
Buffer,高效的数据容器,除了布尔类型,全部原始数据类型都有相应的 Buffer 实现。
Channel,相似在 Linux 之类操做系统上看到的文件描述符,是 NIO 中被用来支持批量式 IO 操做的一种抽象。
File 或者 Socket,一般被认为是比较高层次的抽象,而 Channel 则是更加操做系统底层的一种抽象,这也使得 NIO 得以充分利用现代操做系统底层机制,得到特定场景的性能优化,例如,DMA(Direct Memory Access)等。不一样层次的抽象是相互关联的,咱们能够经过 Socket 获取 Channel,反之亦然。
Selector,是 NIO 实现多路复用的基础,它提供了一种高效的机制,能够检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。
Selector 一样是基于底层操做系统机制,不一样模式、不一样版本都存在区别,例如,在最新的代码库里,相关实现以下:
Linux 上依赖于 epoll(http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java)。
Windows 上 NIO2(AIO)模式则是依赖于 iocp(http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java)。
Chartset,提供 Unicode 字符串定义,NIO 也提供了相应的编解码器等,例如,经过下面的方式进行字符串到 ByteBuffer 的转换:
Charset.defaultCharset().encode("Hello world!"));
2.NIO 能解决什么问题?
下面我经过一个典型场景,来分析为何须要 NIO,为何须要多路复用。设想,咱们须要实现一个服务器应用,只简单要求可以同时服务多个客户端请求便可。
使用 java.io 和 java.net 中的同步、阻塞式 API,能够简单实现。
public class DemoServer extends Thread { private ServerSocket serverSocket; public int getPort() { return serverSocket.getLocalPort(); } public void run() { try { serverSocket = new ServerSocket(0); while (true) { Socket socket = serverSocket.accept(); RequestHandler requestHandler = new RequestHandler(socket); requestHandler.start(); } } catch (IOException e) { e.printStackTrace(); } finally { if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } ; } } } public static void main(String[] args) throws IOException { DemoServer server = new DemoServer(); server.start(); try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream())); bufferedReader.lines().forEach(s -> System.out.println(s)); } } } // 简化实现,不作读取,直接发送字符串 class RequestHandler extends Thread { private Socket socket; RequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try (PrintWriter out = new PrintWriter(socket.getOutputStream());) { out.println("Hello world!"); out.flush(); } catch (Exception e) { e.printStackTrace(); } } }
其实现要点是:
服务器端启动 ServerSocket,端口 0 表示自动绑定一个空闲端口。
调用 accept 方法,阻塞等待客户端链接。
利用 Socket 模拟了一个简单的客户端,只进行链接、读取、打印。
当链接创建后,启动一个单独线程负责回复客户端请求。
这样,一个简单的 Socket 服务器就被实现出来了。
思考一下,这个解决方案在扩展性方面,可能存在什么潜在问题呢?
你们知道 Java 语言目前的线程实现是比较重量级的,启动或者销毁一个线程是有明显开销的,每一个线程都有单独的线程栈等结构,须要占用很是明显的内存,因此,每个 Client 启动一个线程彷佛都有些浪费。
那么,稍微修正一下这个问题,咱们引入线程池机制来避免浪费。
serverSocket = new ServerSocket(0); executor = Executors.newFixedThreadPool(8); while (true) { Socket socket = serverSocket.accept(); RequestHandler requestHandler = new RequestHandler(socket); executor.execute(requestHandler); }
这样作彷佛好了不少,经过一个固定大小的线程池,来负责管理工做线程,避免频繁建立、销毁线程的开销,这是咱们构建并发服务的典型方式。这种工做方式,能够参考下图来理解。
若是链接数并非很是多,只有最多几百个链接的普通应用,这种模式每每能够工做的很好。可是,若是链接数量急剧上升,这种实现方式就没法很好地工做了,由于线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。
NIO 引入的多路复用机制,提供了另一种思路,请参考我下面提供的新的版本。
public class NIOServer extends Thread { public void run() { try (Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 建立 Selector 和 Channel serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888)); serverSocket.configureBlocking(false); // 注册到 Selector,并说明关注点 serverSocket.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select();// 阻塞等待就绪的 Channel,这是关键点之一 Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); // 生产系统中通常会额外进行就绪状态检查 sayHelloWorld((ServerSocketChannel) key.channel()); iter.remove(); } } } catch (IOException e) { e.printStackTrace(); } } private void sayHelloWorld(ServerSocketChannel server) throws IOException { try (SocketChannel client = server.accept();) { client.write(Charset.defaultCharset().encode("Hello world!")); } } // 省略了与前面相似的 main }
这个很是精简的样例掀开了 NIO 多路复用的面纱,咱们能够分析下主要步骤和元素:
首先,经过 Selector.open() 建立一个 Selector,做为相似调度员的角色。
而后,建立一个 ServerSocketChannel,而且向 Selector 注册,经过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的链接请求。
注意,为何咱们要明确配置非阻塞模式呢?这是由于阻塞模式下,注册操做是不容许的,会抛出 IllegalBlockingModeException 异常。
Selector 阻塞在 select 操做,当有 Channel 发生接入请求,就会被唤醒。
在 sayHelloWorld 方法中,经过 SocketChannel 和 Buffer 进行数据操做,在本例中是发送了一段字符串。
能够看到,在前面两个样例中,IO 都是同步阻塞模式,因此须要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,经过高效地定位就绪的 Channel,来决定作什么,仅仅 select 阶段是阻塞的,能够有效避免大量客户端链接时,频繁线程切换带来的问题,应用的扩展能力有了很是大的提升。下面这张图对这种实现思路进行了形象地说明。
在 Java 7 引入的 NIO 2 中,又增添了一种额外的异步 IO 模式,利用事件和回调,处理 Accept、Read 等操做。 AIO 实现看起来是相似这样子:
AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr); serverSock.accept(serverSock, new CompletionHandler<>() { // 为异步操做指定 CompletionHandler 回调函数 @Override public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) { serverSock.accept(serverSock, this); // 另一个 write(sock,CompletionHandler{}) sayHelloWorld(sockChannel, Charset.defaultCharset().encode ("Hello World!")); } // 省略其余路径处理方法... });
鉴于其编程要素(如 Future、CompletionHandler 等),咱们尚未进行准备工做,为避免理解困难,我会在专栏后面相关概念补充后的再进行介绍,尤为是 Reactor、Proactor 模式等方面将在 Netty 主题一块儿分析,这里我先进行概念性的对比:
基本抽象很类似,AsynchronousServerSocketChannel 对应于上面例子中的 ServerSocketChannel;AsynchronousSocketChannel 则对应 SocketChannel。
业务逻辑的关键在于,经过指定 CompletionHandler 回调接口,在 accept/read/write 等关键节点,经过事件机制调用,这是很是不一样的一种编程思路
今天我要问你的问题是,Java 有几种文件拷贝方式?哪种最高效?
Java 有多种比较典型的文件拷贝实现方式,好比:
利用 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); } } }
或者,利用 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 的方式可能更快,由于它更能利用现代操做系统底层机制,避免没必要要拷贝和上下文切换。
今天这个问题,从面试的角度来看,确实是一个面试考察的点,针对我上面的典型回答,面试官还可能会从实践角度,或者 IO 底层实现机制等方面进一步提问。这一讲的内容从面试题出发,主要仍是为了让你进一步加深对 Java IO 类库设计和实现的了解。
从实践角度,我前面并无明确说 NIO transfer 的方案必定最快,真实状况也确实未必如此。咱们能够根据理论分析给出可行的推断,保持合理的怀疑,给出验证结论的思路,有时候面试官考察的就是如何将猜想变成可验证的结论,思考方式远比记住结论重要。
从技术角度展开,下面这些方面值得注意:
不一样的 copy 方式,底层机制有什么区别?
为何零拷贝(zero-copy)可能有性能优点?
Buffer 分类与使用。
Direct Buffer 对垃圾收集等方面的影响与实践选择。
接下来,咱们一块儿来分析一下吧。
1. 拷贝实现机制分析
先来理解一下,前面实现的不一样拷贝方法,本质上有什么明显的区别。
首先,你须要理解用户态空间(User Space)和内核态空间(Kernel Space),这是操做系统层面的基本概念,操做系统内核、硬件驱动等运行在内核态空间,具备相对高的特权;而用户态空间,则是给普通应用和服务使用。你能够参考:https://en.wikipedia.org/wiki/User_space。
当咱们使用输入输出流进行读写时,其实是进行了屡次上下文切换,好比应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。
写入操做也是相似,仅仅是步骤相反,你能够参考下面这张图。
因此,这种方式会带来必定的额外开销,可能会下降 IO 效率。
而基于 NIO transferTo 的实现方式,在 Linux 和 Unix 上,则会使用到零拷贝技术,数据传输并不须要用户态参与,省去了上下文切换的开销和没必要要的内存拷贝,进而可能提升应用拷贝性能。注意,transferTo 不只仅是能够用在文件拷贝中,与其相似的,例如读取磁盘文件,而后进行 Socket 发送,一样能够享受这种机制带来的性能和扩展性提升。
transferTo 的传输过程是:
2.Java IO/NIO 源码结构
前面我在典型回答中提了第三种方式,即 Java 标准库也提供了文件拷贝方法(java.nio.file.Files.copy)。若是你这样回答,就必定要当心了,由于不多有问题的答案是仅仅调用某个方法。从面试的角度,面试官每每会追问:既然你提到了标准库,那么它是怎么实现的呢?有的公司面试官以喜欢追问而出名,直到追问到你说不知道。
其实,这个问题的答案还真不是那么直观,由于实际上有几个不一样的 copy 方法。
public static Path copy(Path source, Path target, CopyOption... options) throws IOException public static long copy(InputStream in, Path target, CopyOption... options) throws IOException public static long copy(Path source, OutputStream out) throws IOException
能够看到,copy 不只仅是支持文件之间操做,没有人限定输入输出流必定是针对文件的,这是两个很实用的工具方法。
后面两种 copy 实现,可以在方法实现里直接看到使用的是 transferTo,你能够直接看源码;而对于第一种方法的分析过程要相对麻烦一些,能够参考下面片断。简单起见,我只分析同类型文件系统拷贝过程。
public static Path copy(Path source, Path target, CopyOption... options) throws IOException { FileSystemProvider provider = provider(source); if (provider(target) == provider) { // same provider provider.copy(source, target, options);// 这是本文分析的路径 } else { // different providers CopyMoveHelper.copyToForeignTarget(source, target, options); } return target; }
我把源码分析过程简单记录以下,JDK 的源代码中,内部实现和公共 API 定义也不是能够可以简单关联上的,NIO 部分代码甚至是定义为模板而不是 Java 源文件,在 build 过程自动生成源码,下面顺便介绍一下部分 JDK 代码机制和如何绕过隐藏障碍。
首先,直接跟踪,发现 FileSystemProvider 只是个抽象类,阅读它的源码可以理解到,原来文件系统实际逻辑存在于 JDK 内部实现里,公共 API 实际上是经过 ServiceLoader 机制加载一系列文件系统实现,而后提供服务。
咱们能够在 JDK 源码里搜索 FileSystemProvider 和 nio,能够定位到sun/nio/fs,咱们知道 NIO 底层是和操做系统紧密相关的,因此每一个平台都有本身的部分特有文件系统逻辑。
省略掉一些细节,最后咱们一步步定位到 UnixFileSystemProvider → UnixCopyFile.Transfer,发现这是个本地方法。
最后,明肯定位到UnixCopyFile.c,其内部实现清楚说明居然只是简单的用户态空间拷贝!
因此,咱们明确这个最多见的 copy 方法其实不是利用 transferTo,而是本地技术实现的用户态拷贝。
前面谈了很多机制和源码,我简单从实践角度总结一下,如何提升相似拷贝等 IO 操做的性能,有一些宽泛的原则:
在程序中,使用缓存等机制,合理减小 IO 次数(在网络通讯中,如 TCP 传输,window 大小也能够看做是相似思路)。
使用 transferTo 等机制,减小上下文切换和额外 IO 操做。
尽可能减小没必要要的转换过程,好比编解码;对象序列化和反序列化,好比操做文本文件或者网络通讯,若是不是过程当中须要使用文本信息,能够考虑不要将二进制信息转换成字符串,直接传输二进制信息。
3. 掌握 NIO Buffer
我在上一讲提到 Buffer 是 NIO 操做数据的基本工具,Java 为每种原始数据类型都提供了相应的 Buffer 实现(布尔除外),因此掌握和使用 Buffer 是十分必要的,尤为是涉及 Direct Buffer 等使用,由于其在垃圾收集等方面的特殊性,更要重点掌握。
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 和垃圾收集
我这里重点介绍两种特别的 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 之类参数,其实并不能影响 Direct Buffer 等堆外成员所使用的内存额度,咱们可使用下面参数设置大小:
-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 内存诊断也是个比较头疼的事情。幸亏,在 JDK 8 以后的版本,咱们能够方便地使用 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 内部使用的内存,因此在 JDK 11 之后,其实它是归类在 other 部分里。
JDK 9 的输出片断以下,“+”表示的就是 diff 命令发现的分配变化:
-Internal (reserved=679KB +4KB, committed=679KB +4KB) (malloc=615KB +4KB #1571 +4) (mmap: reserved=64KB, committed=64KB)
注意:JVM 的堆外内存远不止 Direct Buffer,NMT 输出的信息固然也远不止这些,我在专栏后面有综合分析更加具体的内存结构的主题。
今天我分析了 Java IO/NIO 底层文件操做数据的机制,以及如何实现零拷贝的高性能操做,梳理了 Buffer 的使用和类型,并针对 Direct Buffer 的生命周期管理和诊断进行了较详细的分析。