从DirectMemory谈谈Java NIO

本机内存DirectMemory,属于C Heap,可以通过参数-XX:MaxDirectMemorySize指定。
如果不指定,该参数的默认值为Xmx的值减去1个Survior区的值。如设置启动参数-Xmx20M -Xmn10M -XX:SurvivorRatio=8,那么申请20M-1M=19M的DirectMemory是没有问题的。

/*VM Args: -Xmx20M -Xmn10M -XX:MaxDirectMemorySize=10M*/
public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args){
        ByteBuffer.allocateDirect(11*_1MB);
    }
}

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:658)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at DirectMemoryOOM.main(DirectMemoryOOM.java:16)

修改上面的代码:

/*VM Args: -Xmx20M -Xmn10M -XX:MaxDirectMemorySize=10M -XX:+PrintGCDetails*/
public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args){
        ByteBuffer.allocateDirect(10*_1MB);
        ByteBuffer.allocateDirect(_1MB);
    }
}

[GC (System.gc()) [PSYoungGen: 983K->632K(9216K)] 983K->640K(19456K), 0.0039667 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 632K->0K(9216K)] [ParOldGen: 8K->505K(10240K)] 640K->505K(19456K), [Metaspace: 2520K->2520K(1056768K)], 0.0073120 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 82K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 1% used [0x00000000ff600000,0x00000000ff614968,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 505K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 4% used [0x00000000fec00000,0x00000000fec7e6c8,0x00000000ff600000)
 Metaspace       used 2527K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 271K, capacity 386K, committed 512K, reserved 1048576K

先申请10M,再申请1M,此时会发现JVM不会出现OOM的现象。

可以发现发生了一个FullGC,FullGC后面的system关键字代表这一次FullGC是由System.gc()引起的。

于是,跟踪源码,在分配内存时,间接调用了java.nio.Bits类的reserveMemory方法,而该方法体中显示调用了System.gc():

这里需要说明的是,如果DirectByteBuffer的空间够用,那么System.gc()是不会触发FullGC的。

而如果使用了参数-XX:+DisableExplicitGC,那么调用System.gc()时也不会触发FullGC。

修改代码:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10*_1MB);
byteBuffer.allocateDirect(_1MB);

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:658)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at DirectMemoryOOM.main(DirectMemoryOOM.java:16)

说明DirectBuffer的GC规则与堆对象的回收规则是一样的,只有垃圾对象才会被回收,而判定是否为垃圾对象依然是根据引用树中的存活节点来判定。

在垃圾收集时,虽然虚拟机会对DirectMemory进行回收,但是DirectMemory却不像新生代和老年代那样,发现空间不足了就通知收集器进行垃圾回收,它只能等待老年代满了后FullGC,然后“顺便地”帮它清理掉内存中废弃的对象。否则,只能等到抛出内存溢出异常时,在catch块里调用System.gc()。

难道DirectBuffer只有等到它满的时候才能回收吗?其实可以通过下面的代码主动回收:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10*_1MB);
((DirectBuffer)byteBuffer).cleaner().clean(); //释放内存
byteBuffer.allocateDirect(_1MB);

在了解了DirectMemory后,下面我们看看Java NIO里是如何使用它的。

NIO提供了一种将文件映射到内存的方法进行I/O操作,是内容映射模式,这个操作由FileChannel.map()方法实现。

MappedByteBuffer使用FileChannel的map方法,将文件映射到内存:

public abstract MappedByteBuffer map(MapMode mode,
                                     long position, long size)
    throws IOException;

NIO的Buffer还提供了一个可以直接访问系统物理内存,而不需要进行用户态和内核态之间的拷贝,即Direct Memory的类-DirectByteBuffe,是Direct I/O模式。
通过byteBuffer.allocateDirect(capacity)在DirectMemory上进行分配。

Buffer

Buffer有四个状态量: mask,position,limit和capacity

mask,the index to which its position will be reset when the reset method is invoked。就是说当reset方法调用后,buffer的position将会被置为mask的值,如果mask存在的话。

position,当前缓存区的位置

limit,缓存区的实际上限

capacity,缓存区的总容量上限

Buffer的几个方法:
clear,makes a buffer ready for a new sequence of channel-read or relative put operations: It sets the limit to the capacity and the position to zero.

就是说要往buffer写数据时,先执行buffer.clear(),使position=0,limit=capacity

flip,makes a buffer ready for a new sequence of channel-write or relative get operation:It sets the limit to the current position and then sets the position to zero.

就是说当从buffer中读数据写入channel时,先执行flip(),使limit=position,position=0

rewind,makes a buffer ready for re-reading the data that it already contains:It leaves the limit unchanged and sets the position to zero.

当要重新读取buffer中的内容时,先执行rewind(),将position=0.否则报java.nio.BufferUnderflowException异常

 public ByteBuffer get(byte[] dst, int offset, int length) {
    checkBounds(offset, length, dst.length);
    if (length > remaining())
        throw new BufferUnderflowException();
    int end = offset + length;
    for (int i = offset; i < end; i++)
        dst[i] = get();
    return this;
}
 public final int remaining() {
    return limit - position;
}

reset,resets this buffer’s position to the previously-marked position.

Channel - A channel for reading,writing,mapping,and manipulating a file.

A file channel is created by invoking one of the {@link #open open}
methods defined by this class. A file channel can also be obtained from an
existing {@link java.io.FileInputStream#getChannel FileInputStream}, {@link
java.io.FileOutputStream#getChannel FileOutputStream}, or {@link
java.io.RandomAccessFile#getChannel RandomAccessFile} object by invoking
that object’s getChannel method, which returns a file channel that
is connected to the same underlying file.

Channel是一个通道,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的之类),而通道可以用于读、写或者两者同时进行。

因为Channel是全双工的,所以它可以比流更好的映射底层操作系统API。特别是在Unix网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

实际上,Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。

应用程序中,不能直接对Channel进行读写操作,而必须通过Buffer来进行:

与Socket和ServerSocket类相对性,NIO提供了SocketChannel和ServerSocketChannel,支持阻塞和非阻塞两种模式。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭