【JVM】详解直接内存

news/2024/12/4 6:58:44/

文章目录

  • 1. 直接内存概述
  • 2. 直接内存的使用
    • 2.1 Java缓冲区
    • 2.2 直接内存
  • 3. 直接内存的释放
    • 3.1 直接内存释放原理
  • 4. 禁用显式回收对直接内存的影响

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 异常。

直接内存常用于NIO操作,用于数据缓冲区。

但是直接内存分配回收成本较高,但读写性能高。

最后就是不受JVM内存回收管理


2. 直接内存的使用

下面的例子使用了两种方式来讲文件拷贝到另外一个地方

  1. 传统的Java缓冲区
  2. 直接内存
static final String FROM = "D:\\BaiduNetdiskDownload\\《MYSQL内核:INNODB存储引擎 卷1》.zip";
static final String TO = "D:\\BaiduNetdiskDownload\\《MYSQL内核:INNODB存储引擎 卷1》(1).zip";
static final int _1Mb = 1024 * 1024;public static void main(String[] args) {io();directBuffer();
}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);
}

image-20230127135245522


2.1 Java缓冲区

Java本身并不具备磁盘读写能力,若想使用磁盘读写的能力,就必须调用操作系统提供的函数

也就是内部会调用本地方法,同时CPU的状态会从用户态切换成内核态

在这里插入图片描述

同时内存也会作出相应变化,当切换到内核态的时候,会将磁盘文件先读进系统缓冲区(分次读取),Java是无法使用系统缓冲区,Java就会在堆内存中创建一个Java缓冲区(byte[] buf = new byte[_1Mb]),Java要想读取到系统缓冲区,就需要将系统缓冲区的数据间接读入到Java缓冲区,Java就能对Java缓冲区进行操作了。

image-20230127135942255

之所以用传统IO效率比较低,是因为磁盘文件需要先读入系统缓冲区,系统缓冲区再读入Java缓冲区,Java才能对磁盘文件进行处理,这里造成了不必要的数据复制。效率因而较低。


2.2 直接内存

当执行了ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);之后,操作系统会划分出一个1MB的内存。

这块内存Java代码可以直接访问,操作系统也可以直接访问。也就相当于Java和操作系统共享的一块内存。

这时候磁盘文件可以读入进直接内存,接着Java代码可以对直接内存进行操作,也就是比传统IO少了一次复制的操作,因而效率较高。

image-20230127140531917


3. 直接内存的释放

下面的代码分配一块1G的直接内存

public class Demo1_26 {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 GCSystem.in.read();}
}

当分配成功之后,在任务管理器可以看见Java程序的内存为1G多

image-20230127141209833

接着将byteBuffer设置为NULL,开始垃圾回收

image-20230127141321089

Java程序的内存直接下降1G左右,说明直接内存被释放了。


3.1 直接内存释放原理

前面不是说直接内存不受JVM内存回收管理嘛?为什么垃圾回收之后,直接内存就被释放了?

别急,先来介绍一下直接内存的释放原理

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

Unsafe是Java底层用来分配直接内存和释放直接的内存的类。但是一般并不建议使用这个类。

分配直接内存是靠Unsafe类的allocateMemory方法来实现的,其返回值就是分配内存的地址

而释放内存是靠Unsafe类的freeMemory方法实现的。这个方法需要传入需要释放的内存的地址。

也就是说,想要释放直接内存,需要主动调用freeMemory方法

接着,查看ByteBuffer.allocateDirect(_1Gb)的源码

public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}

其内部是new DirectByteBuffer(capacity),接着查看这个构造方法

DirectByteBuffer(int cap) {                   // package-privatesuper(-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 {//使用Unsafe类分配直接内存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 boundaryaddress = base + ps - (base & (ps - 1));} else {address = base;}cleaner = Cleaner.create(this, new Deallocator(base, size, cap));att = null;}

可以看见构造方法里使用unsafe.allocateMemory(size)来分配直接内存。

那么什么时候调用释放直接内存的方法呢?

需要关注这一行代码

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

其中Deallocator是一个回调任务对象

private static class Deallocatorimplements Runnable
{private static Unsafe unsafe = Unsafe.getUnsafe();private long address;private long size;private int capacity;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) {// Paranoiareturn;}//释放直接内存unsafe.freeMemory(address);address = 0;Bits.unreserveMemory(size, capacity);}}

查看其源码,可以发现其实现了Runnable,它的run方法调用了释放直接内存的方法unsafe.freeMemory(address);

看完这些,也就是说,想要释放直接内存,就必须调用Deallocator中的run方法

接着继续来说说ClearClear在Java类库中是一个特殊的类型,称为虚引用类型

当虚引用关联的对象被回收时,就会触发虚引用对象的clean方法

private Cleaner(Object var1, Runnable var2) {super(var1, dummyQueue);this.thunk = var2;
}public static Cleaner create(Object var0, Runnable var1) {return var1 == null ? null : add(new Cleaner(var0, var1));
}public void clean() {if (remove(this)) {try {this.thunk.run();} catch (final Throwable var2) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {if (System.err != null) {(new Error("Cleaner terminated abnormally", var2)).printStackTrace();}System.exit(1);return null;}});}}
}

查看clean源码,可以发现在执行Cleaner.create方法的时候,会将new Deallocator(base, size, cap)作为参数传递给Runnable var1,在create方法调用new Cleaner(var0, var1),将this.thunk赋值Runnable var1

也就是说在clean方法中的this.thunk.run();调用的就是Deallocator中的run方法。从而释放直接内存。

也就是ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner clean 方法调 用 freeMemory 来释放直接内存


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

可以通过下面的参数禁用显式回收

-XX:+DisableExplicitGC 显式的

什么是显式回收?下面就是显式回收的一个例子

System.gc(); // 显式的垃圾回收,Full GC

禁用显式回收之后,这行代码就变成无效了

这行代码无效,可能会直接影响到直接内存的释放

public class Demo1_26 {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 GCSystem.in.read();}
}

同样,就这个例子来说,当执行到System.gc(); 并不会触发垃圾回收。

于是,byteBuffer对象虽然为NULL,但是并不会被回收掉,于是前面申请的1GB直接内存也不会被释放。

这个byteBuffer只能等到真正的垃圾回收触发时才会被回收,这个直接内存也随之释放。

这样造成的后果就是直接内存占用比较大。

对应的解决办法就是手动通过Unsafe释放直接内存。



http://www.ppmy.cn/news/20170.html

相关文章

Npm开发自己的第一个库

目录开发并上传第一个库账号注册与登陆新库创建上传插件更新试试你的第一个插件吧&#xff01;开发并上传第一个库 账号注册与登陆 官网注册一个自己的账户&#xff0c;务必记住用户名&#xff0c;之后链接到 npm 需要使用 安装 nodejs&#xff0c;他会自动帮我们把 npm 添加…

codeforces签到题之div3

前言 第一次&#xff43;&#xff4f;&#xff44;&#xff45;&#xff46;&#xff4f;&#xff52;&#xff43;&#xff45;&#xff53;&#xff0c;发现几个问题&#xff1a; 1,不知道选&#xff4c;&#xff41;&#xff4e;&#xff47;&#xff55;&#xff41;&…

C++基础入门丨7. 指针——一文搞懂指针

操作系统&#xff1a;Windows 10 IDE&#xff1a;Visual Studio 2019 文章目录1 什么是指针2 指针变量的定义和使用3 指针所占用的空间4 空指针和野指针5 const修饰指针6 指针和数组7 指针和函数8 指针、函数、数组1 什么是指针 我们知道每一个变量都有一个内存位置&#xff0…

elementUI Form表单多个form验证都通过后,再执行后面的操作

一、具体情况&#xff1a; formA、formB、formC。其中&#xff0c;formA的rules验证规则中&#xff0c;含有异步操作&#xff0c;会先调用后台接口&#xff0c;验证身份证是否存在。如下&#xff1a; data () { let isIdcardExist (rule, value,callback)>{ if(!this.formE…

机器自动翻译古文拼音 - 十大宋词 - 扬州慢 淮左名都 姜夔

扬州慢淮左名都 南宋姜夔 淮左名都&#xff0c;竹西佳处&#xff0c;解鞍少驻初程。 过春风十里&#xff0c;尽荠麦青青。 自胡马窥江去后&#xff0c;废池乔木&#xff0c;犹厌言兵。 将黄昏&#xff0c;清角吹寒&#xff0c;都在空城。 杜郎俊赏&#xff0c;算而今重到须惊…

C++绑定器

前言 在学习中&#xff0c;有句bind相关的代码看了一天终于懂了意思。。记录下 1. 问题引入 当时我看的部分是muduo的简单使用&#xff0c;卡在了这两句上 //给服务器注册用户连接的创建和断开回调 _server.setConnectionCallback(std::bind(&ChatServer::onConnection, …

零食商城|基于springboot的零食商城

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 收藏点赞不迷路 关注作者有好处 文末获取源…

高级通讯录(C语言)

目录 前言 为何要实现高级通讯录 高级通讯录实现&#xff1a; 创建通讯录 打印菜单 初始化通讯录 实现加载功能 实现添加功能 实现增容功能 实现删除功能 实现查询功能 实现修改功能 实现查询所有联系人功能 实现排序功能 实现清空功能 实现保存功能 实现退出功能 通讯录总代码…