JDK堆外内存使用

JDK堆外内存使用

jdk:Java开发工具包 (Java Development Kit ) 的缩写
1、堆内内存(on-heap memory)
堆外内存和堆内内存是相对的二个概念,其中堆内内存Java虚拟机内存
2、堆外内存(off-heap memory)
和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受本地系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
使用堆外内存,就是为了能直接分配和释放内存,以提高效率,反复读写,去除它的GC的影响
-XX:MaxDirectMemorySize=512m:可以通过指定JVM参数来确定堆外内存大小限制
堆外内存溢出:申请的堆外内存大于jvm参数设定,例如
-XX:MaxDirectMemorySize=1m

public static void main(String[] args) throws Exception{
//申请直接内存 2m
ByteBuffer buffer1 = ByteBuffer.allocateDirect(102410242);
}
执行结果:
Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.mininglamp.cobra.NIOTest.main(NIOTest.java:20)

优势:
1、减小了垃圾回收
2、加快了复制的速度。堆内在flush到远程时,会先复制到直接内存(非堆内存),而后在发送;而堆外内存至关于省略掉了这个工做。
缺点:
内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由本身来管理,当发生内存溢出时排查起来很是困难。
Java 堆外内存操作方式
1、利用unsafe直接操作(危险性比较高,官方不推荐)
2、利用NIO得ByteBuffer,JVM会进行堆外内存管理,当堆内得引用对象被回收时会自动回收相关得堆外内存(推荐)
基本的属性值
它有几个关键的指标:
mark<position<limit<capacity
position——当前位置,position是插入数据当前位置。position可以通过position()获得,也可以通过position(int)设置。注意:position的位置要比limit小,比mark大。
capacity——空间容量,capacity是当前申请的直接内存的容量,它是申请后就不会改变的。capacity则可以通过capacity()方法获得。
limit——限制大小,我们可能想要改变这段直接内存的大小,因此可以通过一个叫做Limit的属性设置。limit则可以通过limit()获得,通过limit(int)进行设置。注意limit要比mark和position大,比capacity小。
mark——标记位置, mark,就是一个标记为而已,记录当前的position的值。常用的场景,就是记录某一次插入数据的位置,方便下一次进行回溯。
remaing——剩余空间,remaing则表示当前的剩余空间,remaining=limit-position
常用方法
mark(),可以使用方法进行标记,mark方法标记当前的position,默认为-1
reset(),使用方法进行清除,reset方法重置position的位置为mark的位置,mark不能小于0,否则会出错
rewind(),使用方法进行初始化,重置mark为-1.position为0
clear(),这个方法用于清除mark和position,还有limit的位置。
flip(),这个方法主要用于改变limit为当前的position,position为0,主要是用于读取操作。
compact(),这个方法在读取一部分数据的时候比较常用。它会把当前的Position移到0,然后position+1移到1,即类似substring(当前位置)的效果。
isDirect()这个方法用于判断是否是直接内存。如果是返回true,如果不是返回false。
rewind()这个方法用于重置mark标记。
插入数据方法:

读取数据方法:

代码示例:
public static void main(String[] args) throws Exception{
//申请直接内存
ByteBuffer buffer1 = ByteBuffer.allocateDirect(1024);
add(buffer1);
getByts(buffer1);
}

public static void add(ByteBuffer buffer1) {
String context = “北京欢迎你”;
byte[] bys = context.getBytes();
buffer1.put(bys);
System.out.println("插入完数据 " + buffer1);
bufferMap.put(“buffer1”,buffer1);
}

public static void getByts(ByteBuffer buffer1) {
System.out.println("获取buffer: " + buffer1);
buffer1.flip();
System.out.println("flip后: " + buffer1);
byte[] outA = new byte[buffer1.limit()];
buffer1.get(outA);
System.out.println("get后: " + buffer1);
System.out.println(“获取数据:”+new String(outA));
buffer1.clear();
System.out.println("clear后: " + buffer1);
}

执行结果:
插入完数据 java.nio.DirectByteBuffer[pos=15 lim=1024 cap=1024]
获取buffer: java.nio.DirectByteBuffer[pos=15 lim=1024 cap=1024]
flip后: java.nio.DirectByteBuffer[pos=0 lim=15 cap=1024]
get后: java.nio.DirectByteBuffer[pos=15 lim=15 cap=1024]
获取数据:北京欢迎你

回收
由于堆外内存并不直接控制于JVM,因此只能等到full GC的时候才能垃圾回收!(direct buffer归属的的JAVA对象是在堆上且能够被GC回收的,一旦它被回收,JVM将释放direct buffer的堆外空间。前提是没有关闭DisableExplicitGC)
堆外内存回收的方法:
1、Full GC,一般发生在年老代垃圾回收以及调用System.gc的时候;
2、调用ByteBuffer的cleaner的clean(),内部也是调用System.gc(),前提是不要-XX:+DisableExplicitGC
-XX:+DisableExplicitGC
这是一个开关参数,默认是开启的,其作用是禁止代码中显示的调用GC。代码如何显示调用GC呢,通过System.gc()函数调用。如果加上了这个JVM启动参数,那么代码中调用System.gc()将没有任何效果
存在问题:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。
代码示例
public static void main(String[] args) throws Exception{
//申请直接内存
ByteBuffer buffer1 = ByteBuffer.allocateDirect(1024);
System.out.print("回收前: ");
collectDirectMemoryInfo();
gc(buffer1);
System.out.print("回收后: ");
collectDirectMemoryInfo();
}

/**

  • 通过查看Bits的maxMemory和reservedMemory属性来监控使用情况.
  • maxMemory 最大内存, reservedMemory 保留内存
    */
    private static void collectDirectMemoryInfo() {
    try {
    Class c = Class.forName(“java.nio.Bits”);
    Field maxMeory = c.getDeclaredField(“maxMemory”);
    maxMeory.setAccessible(true);
    Field reservedMemory = c.getDeclaredField(“reservedMemory”);
    reservedMemory.setAccessible(true);
    synchronized © {
    Long maxMemoryValue = getByteForField(maxMeory);
    Long reservedMemoryValue = getByteForField(reservedMemory);
    System.out.println(“maxMemoryValue:” + maxMemoryValue / (1024 * 1024) + “mb” +
    " ,reservedMemoryValue:" + reservedMemoryValue / 1024 + “kb”);
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

public static void gc(final ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
((DirectBuffer)byteBuffer).cleaner().clean();
}
}

执行结果:
回收前: maxMemoryValue:1820mb ,reservedMemoryValue:1kb
回收后: maxMemoryValue:1820mb ,reservedMemoryValue:0kb

目前关于堆外缓存的开源实现:
Ehcache 3.0:3.0基于其商业公司一个非开源的堆外组件的实现。
Chronical Map:OpenHFT包括不少类库,使用这些类库不多产生垃圾,而且应用程序使用这些类库后也不多发生Minor GC。类库主要包括:Chronicle Map,Chronicle Queue等等。
OHC:来源于Cassandra 3.0, Apache v2。
Ignite: 一个规模宏大的内存计算框架,属于Apache项目。

Netty零拷贝
1、Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
2、Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
public static void bufferAdd() {
ByteBuf header = Unpooled.directBuffer(1024);
header.writeBytes(“北京欢迎你”.getBytes());
ByteBuf body = Unpooled.directBuffer(1024);
body.writeBytes(" hello world “.getBytes());
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
//true, 它表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex,
// 若没有这个参数,那么 compositeByteBuf 的 writeIndex 仍然是0, 就不可能从 compositeByteBuf 中读取到数据
compositeByteBuf.addComponents(true, header, body);
compositeByteBuf.writeBytes(” all ".getBytes());

byte[] bytes = new byte[33];
compositeByteBuf.readBytes(bytes);
System.out.println(new String(bytes));
/*除了直接使用 CompositeByteBuf 类外, 还可以使用      Unpooled.wrappedBuffer 方法, 它底层封装了 CompositeByteBuf 操作
ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);
byte[] bt = new byte[28];
allByteBuf.readBytes(bt);
System.out.println(new String(bt));*/

}

3.通过 FileRegion 包装的FileChannel.tranferTo方法 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环write方式导致的内存拷贝问题。
public static void copyFileWithFileChannel() throws Exception {
try {
RandomAccessFile srcFile = new RandomAccessFile("/Users/mlamp/Desktop/cs.txt",“r”);//打开文件
FileChannel srcFileChannel = srcFile.getChannel();

    RandomAccessFile destFile = new RandomAccessFile("/Users/mlamp/Desktop/mu.txt","rw");
    FileChannel destFileChannel = destFile.getChannel();
    long position = 0;
    long count = srcFileChannel.size();
    srcFileChannel.transferTo(position, count, destFileChannel);

    //将 FileChannel 封装成一个 DefaultFileRegion 可以直接通过它将文件的内容直接写入 Channel 中
    //DefaultFileRegion ed = new DefaultFileRegion(srcFileChannel, 0, count);
    //ed.transferTo(destFileChannel,position);
} catch (Exception e) {
    throw new Exception(e.getMessage(),e);
}

}
//“r” 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
//“rw” 打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
//“rws” 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
//“rwd” 打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。

4.通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。
public static void sliceTest() {
ByteBuf byteBuf = Unpooled.directBuffer(1024);
byteBuf.writeBytes(“北京欢迎你”.getBytes());
//slice 操作可以将一个 ByteBuf 切片为多个共享一个存储区域的 ByteBuf 对象.
ByteBuf header = byteBuf.slice(0,6);
byte[] bytes = new byte[6];
header.readBytes(bytes);
System.out.println(“header:”+new String(bytes));
ByteBuf body = byteBuf.slice(6,9);
byte[] bytes1 = new byte[9];
body.readBytes(bytes1);
System.out.println(“body:”+new String(bytes1));
//产生的 header 和 body 的过程是没有拷贝操作的, header 和 body 对象在内部其实是共享了 byteBuf 存储空间的不同部分而已
header.setBytes(0, “天”.getBytes());
header.readerIndex(0);
byte[] bytes2 = new byte[6];
header.readBytes(bytes2);
System.out.println(“header:”+new String(bytes2));
byte[] bytes3 = new byte[15];
byteBuf.readBytes(bytes3);
System.out.println(“byteBuf:”+new String(bytes3));
}

思考
1、目前项目占用内存较大且持久的文件(例如模型) 是否可考虑放到堆外内存使用。
2、比较好用的有netty的内存管理模块和"chronicle-bytes", 之前改的fst加载就是用的chronicle-bytes. lucene的词典查询, 是比较好的堆外内存使用场景, 可以继续改造下.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值