专栏首页渔夫Java-直接内存 DirectMemory 详解

Java-直接内存 DirectMemory 详解

Java 直接内存

1. 设计逻辑

 下面是 《深入理解 Java 虚拟机 第三版》2.2.7 小节 关于 Java 直接内存的描述。

 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。  在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。  显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置 -Xmx 等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

如果整理周志明对其描述的要的,那便是如下:

  • Direct Memory 并不是虚拟机运行时数据区的一部分;
  • 由于在 JDK 1.4 中引入了 NIO 机制,为此实现了一种通过 native 函数直接分配对外内存的,而这一切是通过以下两个概念实现的:
    • 通道(Channel);
    • 缓冲区(Buffer);
  • 通过存储在 Java 堆里面的 DirectByteBuffer 对象对这块内存的引用进行操作;
  • 因避免了 Java 堆和 Native 堆(native heap)中来回复制数据,所以在一些场景中显著提高了性能;
  • 直接内存出现 OutOfMemoryError 异常的原因是物理机器的内存是受限的,但是我们通常会忘记需要为直接内存在物理机中预留相关内存空间;

上述变化可以通过下面一图来表示:

  • 关于上述的内存:可以这么粗浅地认为:栈用于方法的层层调用、返回、顺序执行,堆负责存储对象。
  • 关于内存空间的称呼变化:JDK 1.4 之前的称呼 native heap 转为现在的称呼 directory memory 。之所以 heap 前加 native 来修饰,是因为要让其和虚拟机规范中的内存 heap,而现在称呼 directory memory 是因为我们能够直接通过引用访问对象,消除了拷贝操作。

 实际上,栈中访问一个对象还是要借助堆,stack 寻求一个对象还是和以前一样,会问:”堆,请把对象xxx给我“,而不会向 native 堆索要。所以这个直接性是不彻底的。真正的实现是这样的,Java 程序仍然需要使用在 Java heap 中的一个对象(实际上规定为 DirectByteBuffer 类型对象来操作),但是这个对象(buffer)所持有的数据实际上存储于 native memory 中,而 Java 堆只仅仅拥有着对 native heap 的一个引用。

2. 垃圾回收

 先的回答是不是的问题:直接内存也是会被 JVM 虚拟机管理进行完全不被引用的对象回收处理。

 但是直接内存中的对象并不是如普通对象样被 GC 管理,这方面细节会在下一小节中提到。

3. 为什么需要 DirectByteBuffer

 即使不清楚 Java 设计者们会提出一个新的内存概念,不过也不难推断出其原因:由于 JVM 规范中的 heap 不满足某些领域的使用要求。

首先,回答为什么需要 Buffer 。

我们知道 GC 会管理内存,大致上可以这么认为,其主要做两件事:

  • 回收不被引用的对象;
  • 整理内存空间(比如将有效的对象整理到一起);

 首先,JDK 告诉我们,NIO 操作并不适合直接在堆上操作。由于 heap 受到 GC 的直接管理,在 IO 写入的过程中 GC 可能会进行内存空间整理,这导致了一次 IO 写入的内存地址不完整。实际上,JNI(Java Native Inteface)在调用 IO 操作的 C 类库时,规定了写入时地址不能失效,这就导致了不能在 heap 上直接进行 IO 操作。在 IO 操作的时候禁止 GC 也是一个选项,如果 IO 时间过长,那么则可能会引起堆空间溢出。

 其次,回答 HeapByteBuffer 和 DirectByteBuffer 之间的区别。

 而 HeapByteBuffer 和 DirectByteBuffer 都是在堆外内存,即 native heap 上进行操作的。它们的区别是

  • HeapByteBuffer 实现方式是拷贝,也就是栈访问 Buffer 内的数据还是需要从堆外内存拷贝到 heap 内;
  • DirectByteBuffer 直接在堆内储存有对其的引用,不需要复制就能访问。

堆外内存的垃圾回收是比堆内垃圾回收复杂的,而且有着不同的回收机制。

4. 内存分配问题

 直接内存的最大大小可以通过 -XX:MaxDirectMemorySize 来设置,默认是 64M。

 在 Java 中分配内存的方式一般是通过 sun.misc.Unsafe类的公共 native 方法实现的(比如 文件以及网络 IO 类,但是非常不建议开发者使用,使用时一定要确保安全),而类 DirectByteBuffer 类的也是借助于此向物理内存(比如 JVM 运行于 Linux 上,那么 Linux 的内存就被称为物理内存)。

 Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。

 而 ByteBuffer 提供的静态方法:java.nio.ByteBuffer#allocateDirect 将 Unsafe 类分配内存的相关操作封装好提供给开发者,如下图所示(图源:美团技术团队):

 从内存分配效率方面考虑的话,一般认为 HeapByteBuffer 以及 DirectByteBuffer 这种申请堆外内存的方式比堆内申请内存效率低,这主要是因为前者需要向物理机申请堆外内存,这是额外的开销。

延伸阅读

 美团技术团队:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

注意事项

 基地址并非是 JVM 运行环境的物理地址,而是虚拟内存地址,详细可见 R 大的回答:https://www.zhihu.com/question/53150867

建议

 Oracle JDK 没有 Unsafe 类的源码,所以需要 OpenJDK 才能知道 Unsafe 类中的方法详细返回类型,网址如下:

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/4db0e91b95c8/src/share/classes/sun/misc/Unsafe.java

5. ByteBuffer 和其他 Buffer 对比

 在 rt.jar 包的 java.nio 包下还有很多其他类型的 Buffer 类,比如 CharBuffer、IntBuffer 等等(每个基本类型*都对应一个 *Buffer,除了特殊的占一个 bit 的布尔类型)。但是唯独只有 ByteBuffer 类提供了输入参数为其他基本类型的 put 方法,这是为什么呢?

 这涉及了 Java 中整型基本类型存储的位数都是字节二进制位数的整数倍,如下表所示。

 有人这样认为 Java 中存储本质以字节 byte 为单位,这不完全对,因为二进制才是本质存储。但是换个说法就对了:Java 字节所占的二进制位数的整数倍恰好是其他基本类型所占的二进制位数,上表就是这个含义。所以所有基本类型都可以用字节表示,比如 char 类型就是以两个字节大小存储的,如果 char 类型的数据前 8 位和后 8 位完全可以分别用两个 byte 表示。换句话说 ByteBuffer 起到了 CharBuffer 的作用。

以下 java.nio.DirectByteBuffer#putChar(char)java.nio.DirectByteBuffer#getChar() 的源码:

  • putchar
 public ByteBuffer putChar(char x) {
        putChar(ix(nextPutIndex((1 << 1))), x);
        return this;
    }
  • getchar
public char getChar() {
        return getChar(ix(nextGetIndex((1 << 1))));
    }

 其中 nextPutIndex 以及 nextGetIndex 第一参数相当于待放入的基本类型要分为几份 byte 类型,这两个方法主要用于处理加入/拿出基本类型数据是对计数指针的加与减。以 putChar 为例,其做好统计指针操作后,又调用了本地的方法 putchar,其源代码如下:

    private ByteBuffer putChar(long a, char x) {
      
        if (unaligned) {
            char y = (x);
            unsafe.putChar(a, (nativeByteOrder ? y : Bits.swap(y)));
        } else {
            Bits.putChar(a, x, bigEndian);
        }
        return this;
    }

其最终会调用 native putChat() 方法:sun.misc.Unsafe#getChar(long)

注意事项

 上述方法利用了左移操作 left shift,整形数据左移一位代表*2,但是不用 2 而用 1<<1,这样不就意味着多进行了一层没有必要的运算了吗?实际上,使用左移运算体现了除 byte 类型的基本类型可以由 byte 类型整数倍表示,增加了可读性,这是这样写代码的主要原因。其次左移操作并不会带来执行时的额外运算,因为左移这类计算表达式在编译期间会直接优化为结果值。

本文相关引用:

  • http://fibrevillage.com/sysadmin/325-understanding-java-heap-memory-and-java-direct-memory
  • https://www.ibm.com/developerworks/library/j-nativememory-linux/
  • https://yasinshaw.com/articles/59
  • https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:https://blog.csdn.net/li_xunhuan复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • Java直接(堆外)内存使用详解

    本篇主要讲解如何使用直接内存(堆外内存),并按照下面的步骤进行说明: 相关背景-->读写操作-->关键属性-->读写实践-->扩展-->参考说明 希望对想使用直...

    用户1154259
  • Java直接内存与非直接内存性能测试

    什么是直接内存与非直接内存 根据官方文档的描述: A byte buffer is either direct or non-direct. Given a d...

    用户1154259
  • JAVA面试50讲之10:直接(堆外)内存原理及使用

    HeapByteBuffer是堆内ByteBuffer,使用byte[]存储数据,是对数组的封装,比较简单。DirectByteBuffer是堆外ByteBuf...

    用户1205080
  • 聊聊jvm的-XX:MaxDirectMemorySize

    code4it
  • JVM-3. OutOfMemoryError

    GC Roots到对象之间有可达路径,垃圾回收机制就不会清除这些对象,在对象数量达到最大堆容积限制就会产生内存溢出。 设置堆最小值参数-Xms和最大值参数-X...

    悠扬前奏
  • java堆内存详解

    参考文献: http://www.importnew.com/14630.html Java 堆内存 http://blog.csdn.net/ylyg0...

    用户7798898
  • Java通过JNI申请直接内存

    将 JAVA_HOME/include/jni.h 和 JAVA_HOME/include/linux/jni_md.h 两个文件拷贝到2022-3-14目录下

    书唐瑞
  • Java 内存模型详解

    Java的内存模型(Java Memory Model )简称JMM。首先应该明白,Java内存模型是一个规范,主要规定了以下两点:

    乱敲代码
  • Java 内存区域详解

    学习Java也有一段时间了,总感觉有些东西学的不是很精通。例如Java内存区域到底是怎么样的?程序是怎么跑的?对象是怎么存放的?这些都影响了我对自己的程序运行的...

    zhangheng
  • Java 内存区域详解

    对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作,...

    Vincent-yuan
  • 详解Java内存模型

    ​ JVM虚拟机规范中曾经试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都可以达到一致性的内存访问效果...

    挖掘开源的价值
  • 详解Java内存模型

    ​ JVM虚拟机规范中曾经试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都可以达到一致性的内存访问效果...

    挖掘开源的价值
  • Java内存区域详解

    Java虚拟机在执行Java程序时,会把它管理的内存划分为若干个不同的数据区域。JDK1.8和之前版本略有不同。

    Vincent-yuan
  • (代码篇)从基础文件IO说起虚拟内存,内存文件映射,零拷贝

    JAVA虚拟机内部便会调用OS底层的 read()系统调用完成操作,在调用 in.read()的时候就是从内核缓冲区直接返回数据了。

    intsmaze-刘洋
  • Flink任务中断:Container is running beyond physical memory limits

    某用户反馈,Flink(版本1.9)任务中断,查看日志发现用户使用的是Flink on yarn,错误日志提示如下:

    Yannic
  • JVM内存模型之直接内存

    直接内存 又称堆外内存,也就是说这不是jvm运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但这部分也会被频繁的使用,而且也可能导致OOM。

    shengjk1
  • 【Flink】第十八篇:Direct Memory 一箩筐

    Flink的内存管理是基于JVM内存模型的,所以,在内存调优或者解决各种OOM等问题时JVM内存管理是绕不开的话题。本文以Direct Memory为切入点,探...

    章鱼carl
  • 大型跨境电商 JVM 调优经历

    前提: 某大型跨境电商业务发展非常快,线上机器扩容也很频繁,但是对于线上机器的运行情况,特别是jvm内存的情况,一直没有一个统一的标准来给到各个应用服务的ow...

    Java学习123
  • Java 内存模型 JMM 详解!

    Java技术栈

扫码关注云+社区

领取腾讯云代金券