缓冲区的分类:非直接缓冲区和直接缓冲区别
在了解直接缓冲区和非直接缓冲区之前先说几个概念:
Java虚拟机分配的内存是物理内存,不是虚拟内存。
先看物理内存和虚拟内存的概念:
物理内存:物理内存(Physical memory)是相对于虚拟内存而言的。物理内存指通过物理内存条而获得的内存空间。
虚拟内存 : 虚拟内存则是指将硬盘的一块区域划分来作为内存。
内核地址空间和用户地址空间区别和联系:操作系统和驱动程序运行在内核空间,应用程序运行在用户空间。在电脑开机之前,内存就是一块原始的物理内存。什么也没有。开机加电,系统启动后,就对物理内存进行了划分。当然,这是系统的规定,物理内存条上并没有划分好的地址和空间范围。这些划分都是操作系统在逻辑上的划分。不同版本的操作系统划分的结果都是不一样的。为什么要划分用户空间和系统空间呢?当然是有必要的。操作系统的数据都是存放于系统空间的,用户进程的数据是存放于用户空间的。这是第一点,不同的身份,数据放置的位置必然不一样,否则大混战就会导致系统的数据和用户的数据混在一起,系统就不能很好的运行了。分开来存放,就让系统的数据和用户的数据互不干扰,保证系统的稳定性。分开存放,管理上很方便,而更重要的是,将用户的数据和系统的数据隔离开,就可以对两部分的数据的访问进行控制。这样就可以确保用户程序不能随便操作系统的数据,这样防止用户程序误操作或者是恶意破坏系统。处于用户态的程序只能访问用户空间,而处于内核态的程序可以访问用户空间和内核空间。
非直接缓冲区:我们之前说过NIO通过通道连接磁盘文件与应用程序,通过缓冲区存取数据进行双向的数据传输。物理磁盘的存取是操作系统进行管理的,与物理磁盘的数据操作需要经过内核地址空间;而我们的Java应用程序是通过JVM分配的内存空间,属于应用程序的内存空间。数据需要在内核地址空间和用户地址空间,在操作系统和JVM之间进行数据的来回拷贝,无形中增加的中间环节使得效率与后面要提的之间缓冲区相比偏低。
读操作:当有数据读取的时候,os系统现将数据先读入内核地址空间中,然后将内核空间的数据复制一份到用户地址空间中,然后再读入用户程序。
写操作:当有数据要写入的时候,现将数据通过管道写入用户地址空间中,然后将用户地址空间的数据复制一份到内核地址空间中,然后再由os写入磁盘。(复制到内核地址空间后数据什么时候写入磁盘,不是程序所决定的)
直接缓冲区:直接缓冲区则不再通过内核地址空间和用户地址空间的缓存数据的复制传递,而是在物理内存中申请了一块空间,这块空间映射到应用程序和物理磁盘,不再经过内核地址空间和用户地址空间,应用程序与磁盘之间的数据存取之间通过这块直接申请的物理内存进行,起到了中间媒介的作用。
可以通过 FileChannel 的 map() 方法获得:直接字节缓冲区可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 java中提供了3种内存映射模式,即:只读(readonly)、读写(read_write)、专用(private) ,对于只读模式来说,如果程序试图进行写操作,则会抛出ReadOnlyBufferException异常。 第二种的读写模式表明了通过内存映射文件的方式写或修改文件内容的话是会立刻反映到磁盘文件中去的。 专用(private)模式采用的是OS的"写时拷贝"原则,即在没有发生写操作的情况下,多个进程之间都是共享文件的同一块物理内存(进程各自的虚拟地址指向同一片物理地址),一旦某个进程进行写操作,那么将会将要写的物理内存中的文件数据单独拷贝一份到进程的私有缓冲区中,然后在私有缓冲区中对文件进行操作,不会反映到物理文件中去,只是改变的进程内存空间的副本。
allocateDirect() 工厂方法来创建:直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。
使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,在垃圾回收时并不能用程序区控制堆外内存的回收,因为不属于虚拟机,只能是垃圾回收机制按需对堆内的DirectByteBuffer对象进行回收,回收后将与直接缓存区失去联系,也就意味着直接缓冲区被回收。
在直接缓冲区谨慎使用原因:
(1)不安全;
(2)消耗更多,因为它不是在JVM中直接开辟空间。这部分内存的回收只能依赖于垃圾回收机制,垃圾什么时候回收不受我们控制;
(3)数据写入物理内存缓冲区中,程序就失去了对这些数据的管理,即什么时候这些数据被最终写入从磁盘只能由操作系统来决定,应用程序无法再干涉。
(4)堆外空间分配比较耗时。
最后看一下jdk对于缓冲区和非缓冲区的说明:
直接与 非直接缓冲区
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用此类的 allocateDirect 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 mapping 将文件区域直接映射到内存中来创建。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect 方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。