JVM 03:直接内存

134 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情

简介

直接内存 Direct Memory

  • 常见于 NIO 操作,用于数据缓冲区
  • 属于系统内存,分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

基本使用

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

/**
 * 演示 ByteBuffer 作用
 */
public class DirectMemoryUse {
    static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io(); // io 用时:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
    }

    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);
    }
}

JVM 相当于系统中的系统,Java 本身不具备读写能力,通过调用原生方法,将读写指令移交给计算机完成。由于 JVM 和计算机内存不互通,所以先要读写到计算机内存,然后复制到 JVM 内存。

202204111012 JVM 03:直接内存 00.png

使用 ByteBuffer.allocateDirect 将划分一块直接内存作为缓冲区,让 Java 代码可以直接访问,而不用复制一份。这块内存区域由系统和 JVM 共享。

202204111012 JVM 03:直接内存 01.png

内存溢出

下面的代码会抛异常:

  • java.lang.OutOfMemoryError: Direct buffer memory
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示直接内存溢出
 */
public class DirectMemoryOverflow {
    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);
        }
        // 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
        //                  jdk8 对方法区的实现称为元空间
    }
}

直接内存的分配与释放原理

直接内存的分配和释放,在底层是通过 Unsafe 对象管理的,并且借助了虚引用对象 Cleaner,在 JVM 回收 ByteBuffer 对象的时候,ReferenceHandler 线程会调用虚引用对象的 clean() 方法,从而触发直接内存的释放。

Unsafe 对象的两个方法:

  • allocateMemory 分配内存
  • freeMemory 释放内存
import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;

/**
 * 直接内存分配的底层原理:Unsafe
 */
public class DirectMemoryUnsafe {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

禁用显式回收对直接内存的影响

JVM 调优时,考虑到 System.gc() 进行的 Full GC 会对性能有影响,所以禁用显示垃圾回收,也就是设置虚拟机选项 -XX:+DisableExplicitGC

但是这样会影响直接内存的回收,比如虽然 ByteBuffer 对象已经被置为 null,但是由于内存相对充裕,所以没有回收掉它,此时直接内存也不会被回收。解决办法就是由程序员来手动管理直接内存的回收。

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

/**
 * 禁用显式回收对直接内存的影响
 */
public class DirectMemoryAllocate {
    static int _1Gb = 1024 * 1024 * 1024;

    /*
     * -XX:+DisableExplicitGC 禁用显式的垃圾回收
     */
    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();
    }
}

参考资料