文章目录

  • 1、前言
  • 2、直接内存
  • 3、本机直接内存溢出
  • 4、小结


1、前言

  前面学习方法区的时候了解到:JDK8 的元空间使用的是直接内存。那么,直接内存是什么意思呢?它和虚拟机内存有什么区别吗?

  直接内存不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。直接内存是在 Java 堆外的、直接向系统申请的内存区间。

java内存马会生成class_jvm

2、直接内存

  一般来说,Java 虚拟机里面有一个运行时数据区,JVM 运行期间产生的信息都是在 JVM 管理的内存里面,但是在一些场景里,会使用到本地的内存,例如元空间。

  我们的 JVM 要从物理磁盘中对文件进行操作,先是通过 JVM 的地址空间,然后经过操作系统的内核地址空间才操作物理磁盘。读写文件需要与磁盘交互,需要由用户态切换到内核态。如下图所示,需要两份内存存储重复数据,效率低。

java内存马会生成class_jvm_02


  在 JDK1.4 中加入了一个 NIO 类(New Input/Output),引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

java内存马会生成class_操作系统_03


  我们运行以下代码:

package pers.klb.JvmDemo2020;

import java.nio.ByteBuffer;
import java.util.Scanner;

public class BufferTest {
    private static final int BUFFER = 1024 * 1024 * 1024;//1GB

    public static void main(String[] args){
        //直接分配本地内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("直接内存分配完毕,请求指示!");

        Scanner scanner = new Scanner(System.in);
        scanner.next();

        System.out.println("直接内存开始释放!");
        byteBuffer = null;
        System.gc();
        scanner.next();
    }
}

  我们通过任何 JDK 的内存工具都看不到它占用的这 1GB 内存。

java内存马会生成class_java_04


  但是可以从任务管理器当中看到:

java内存马会生成class_java_05


  上面的情况进一步证明直接内存是不受 JVM 管理的。

  显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还会受到本机总内存大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略直接内存,是的各个内存区域总大小超过限制。

  我们可以简单理解为:Java 运行总内存 = heap + native memory。

java内存马会生成class_Java_06

3、本机直接内存溢出

  直接内存的容量可以通过参数-XX:MaxDirectMemorySize参数来指定,默认是和 Java 堆最大值一致。

  虽然使用 DirectBuffer 分配内存也会抛出内存溢出异常,但是它抛出异常的时候并没有真正向操作系统申请分配内存,而是通过计算得知直接内存无法分配,然后在代码里手动抛出溢出异常。

  正是因为是计算它将会溢出,而不是做了才溢出,在 Heap Dump 文件我们就不会看到有什么内存异常的情况,如果你发现代码发生了内存溢出,然而 Dump 文件又很正常,那肯定是程序直接或者间接使用了直接内存,然后 JVM 计算出的内存不足,手动抛出的异常,而不是真的发生了异常。可以直接去检查直接内存方面的问题了。

4、小结

  至此,我们明白了虚拟机里面的内存是如何划分的,哪部分区域、什么样的代码和操作可能导致内存溢出异常。下面将会继续学习 JVM 的其他组成部分。