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

  • Post author:
  • Post category:其他



直接内存(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()方法导致了直接内存的释放!



版权声明:本文为m0_43439210原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。