Jvm-直接内存的使用和回收

直接内存(Direct Memory)

特点

  1. 直接内存并不属于jvm的内存管理,而是属于操作系统内存。

  2. 常见于NIO操作,用于数据缓冲区,分配回收成本高,但是读写性能高

  3. 通过DirectByteBuffer对象操作Native内存,访问速度优于Java堆

  4. 不受jvm垃圾回收管理

基本使用

演示一下分别使用ByteBuffer和阻塞io拷贝文件

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 演示 ByteBuffer 作用
 */
public class Demo1_9 {
    static final String FROM = "D:\\Files\\科大讯飞开发套件客户附送资料V2.0_20220826\\3.开发套件关键传感器使用资料\\M1系列麦克风阵列(M160C)\\1.ROS语音功能包视频教程\\语音视频教程4:ROS功能包功能详解.mp4";
    static final String TO = "D:\\Files\\科大讯飞开发套件客户附送资料V2.0_20220826\\3.开发套件关键传感器使用资料\\M1系列麦克风阵列(M160C)\\1.ROS语音功能包视频教程\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:147.2171
        directBuffer(); // directBuffer 用时:73.9831
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -1) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_1Mb];    //分配缓冲区
            while (true) {
                int len = from.read(buf);
                if (len == -1) {
                    break;
                }
                to.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

从运行结果我们可以看到ByteBuffer的效率是阻塞io的两倍
在这里插入图片描述

原理

那么为什么ByteBuffer在读写大文件的时候比io效率高呢

我们先来看一下io的读写过程,java本身不具备读写能力,要进行磁盘读写的话必须调用操作系统提供的函数(Native方法),此时CPU的运行状态会从用户态切换到内核态,CPU会先将磁盘文件的内容读到操作系统内存中的系统缓冲区,java无法直接操作操作系统的缓冲区,因此会在堆内存中先分配一段java缓冲区,对应代码中 “ byte[] buf = new byte[_1Mb]; ”部分,最后再从系统缓冲区把数据读到Java缓冲区,这样就造成了不必要的数据复制,因此效率很低。

在这里插入图片描述

使用DirectBuffer对象后,当代码运行到“ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);“时,会在操作系统内存中划分一段直接内存,跟之前不一样的地方在于这段内存Java可以直接访问(操作系统和java都可以直接访问),CPU会将磁盘文件直接读到直接内存,java直接从直接内存中取数据,跟普通io相比少了一次缓冲区的复制操作,因此速度得到了成倍的提升。

在这里插入图片描述

内存溢出

前面说到直接内存的特点是不受jvm垃圾回收管理,jvm垃圾回收时不会直接释放直接内存(划重点!是不会直接释放!下节会解释!),那么直接内存会不会出现内存溢出呢,答案是会的。下面代码我们每次分配100M直接内存,并且为了长时间占用把它加入到生命周期更长的一个list中,一直循环。

public class outOfmMemory{
    static int _100Mb = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
    }
}

报错:

在这里插入图片描述

内存释放

先来演示一下直接内存的内存释放,为了效果明显,我们直接分配1GB直接内存

import java.io.IOException;
import java.nio.ByteBuffer;

public class releaseMemory {
    static int _1Gb = 1024 * 1024 * 1024;
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
        System.in.read();
    }
}

可以看到分配内存后,
在这里插入图片描述

释放内存之后
在这里插入图片描述

那是不是垃圾回收把直接内存释放掉了呢,

不是的!

直接内存的分配和释放是通过unsafe对象来管理的,接下来我们从源码的角度分析一下

DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;



}

可以看到在DirectByteBuffer的构造器里调用了“unsafe对象的allocateMemory(size)”方法实现了对直接内存的分配。

注意看这行代码:

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

cleaner是一个虚引用类型,它的特点是当他所关联的对象被回收时,会触发clean方法。会调用new Deallocator(base, size, cap))

private Deallocator(long address, long size, int capacity) {
    assert (address != 0);
    this.address = address;
    this.size = size;
    this.capacity = capacity;
}

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

可以看到“unsafe.freeMemory(address);”,unsafe对象主动调用freeMemory释放了直接内存。

因此在内存释放代码中,并不是垃圾回收直接释放掉了直接内存,而是,垃圾回收回收了byteBuffer对象,触发了clean方法,unsafe对象调用了freeMemory()方法导致了直接内存的释放!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HotSpot的垃圾回收机制采用分代回收,堆分为年轻代和老年代,非堆就是持久带。面对不同的代,采用不同的回收策略。 而年轻代又可以分为1个Eden和2个Survivor。对于Eden,大多数对象都是先在此区域开辟空间,存储年轻对象的实例数据。Eden相对于Old带,空间是比较小的。所以对象数据不断地在此开辟空间,Eden不够了,年轻代发生了回收。放置(稍后会说怎么放置)到S1或者S2。S1或者S2不够放了,直接放到Old带。根据年轻代的特点,空间小,发生回收事件频率较高,那么就采用标记-复制的算法将Eden中的对象实例数据克隆到S1或者S2或者直接克隆到Old带。标记-复制算法优点是:快速、节省内存碎片。缺点是:内存消耗的空间需要变为原来的50%,另一块空间作为复制的目标。 老年代因为空间比较大,存储的对象又是比较长寿的对象,所以采用标记-整理或者也称作标记-压缩算法。这样不必开辟另外50%的空间用于复制目标,也不用担心这个较为辽阔的内存空间产生占用碎片的问题。缺点就是又标记、又压缩的。对于Old带比较费时间。 持久带虽然资料上没有明说,但是根据持久带的作用和特点以及触发该区回收的情景可以推算,持久带采用的回收算法应该是标记-整理/压缩算法。 JVM内存回收只要是针对这3个区域来说的。像NIO的直接文件内存读取,使用的是直接内存,只有出发了FullGC方能回收该区域!JNI调用本地库,本地代码所消耗的内存需要操作系统额外开辟内存

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值