本机内存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.(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.(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,支持阻塞和非阻塞两种模式。