Java 堆外内存及调优

文章目录

  • 直接内存简介
    • 为什么DirectByteBuffer可以优化 IO 性能
  • 直接内存的分配
  • 直接内存的回收
  • 直接内存跟踪与诊断

直接内存简介

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分,并非Java虚拟机规范中定义的内存区域。但是这部分内存的频繁使用,也可能导致 OutOfMemoryError 异常。

直接内存的分配不受Java堆大小的限制,但是受限于本机总内存大小和处理器寻址空间。一般服务器运维人员会根据实际内存设置-Xmx等参数,但经常忽略直接内存,使得动态扩展时出现 OutOfMemeoryError 异常。

JDK 1.4中加入了NIO类,引入一种基于通道(Channel)缓冲区(Buffer)的I/O方式,它可以使用 Native 函数库直接分配堆外内存。这样在一些场景中能显著提高性能,避免在 Java 堆中和 Native 堆中来回复制数据


为什么DirectByteBuffer可以优化 IO 性能

普通 IO 流读取磁盘中数据时,内核态需要将磁盘中的数据拷贝到系统缓冲区 Page Cache(内核地址空间),再从内核态拷贝到用户空间中,C 程序里操作的就是用户态的内存。

JVM 启动时在用户态申请一块内存,这块内存中包含了 Java 堆,几乎所有创建的对象和数组都分配在堆上,堆上的实例受 GC 管理。除了Java堆,其余内存称为 堆外内存,如果使用JNI直接调用 C 函数申请堆外内存(直接内存),这块堆外内存不会进行垃圾回收(例如:Direct Memory 由 malloc 分配)。

Java 程序中进行文件的读操作:

  1. 首先在内核态,将数据从磁盘中读取到系统缓存区中
  2. 再从系统缓冲区拷贝到用户态的堆外内存(JVM实现)
  3. 然后再从堆外拷贝到 Java 堆内的 byte 数组(用户地址空间)。

读操作示意图如下:


上述传统 Java IO方式,经历了两次内存拷贝,而NIO中使用 DirectByteBuffer,不需要将数据从堆外拷贝到堆内,Java程序可以直接访问堆外的 Direct Memory,减少了一次内存拷贝,也减轻了 GC 压力,降低了Java堆内存占用。示意图如下:


为什么数据不能直接从系统缓冲区拷贝到 Java 堆
笔者认为原因主要在于 GC 会改变堆内对象的内存地址,例如:Young GC 时Eden 区存活对象会被拷贝到 Survivor 区。而内核态向用户态的数据拷贝是由内核完成的,并不受 Java 程序控制

因此,需要先拷贝到堆外内存(这个区域不会发生 GC,地址不改变),再从堆外内存拷贝数据到Java堆中。Java 堆内存和堆外内存同属用户地址空间,拷贝可由 Java 虚拟机完成。


Java Direct Buffer用于执行很大数据量的IO密集操作时,存在很大的性能优势

  • Direct Buffer 是使用malloc进行的堆外分配,生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多 IO 操作会很高效。
  • 减少了堆内对象存储的可能额外维护工作(例如:垃圾回收时位置的移动),所以访问效率可能有所提高。
  • Direct Buffer 的使用能提高网络和文件IO效率,因为省去了从本地堆到Java堆的拷贝,降低 Java 堆的内存占用从而减轻了GC压力。
    • Direct Buffer的创建和销毁比堆内Buffer增加部分开销,通常都建议用于长期使用、数据较大的场景

直接内存的分配

  1. 通过NIO中的DirectByteBuffer实例引用直接内存
public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocateDirect(1024);// ...
}

allocateDircet 方法返回 DirectByteBuffer 实例:

public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}
  1. DirectByteBuffer 类的构造函数中,通过Unsafe#allocateMemory分配直接内存空间,并且创建对应的 Cleaner 实例用于回收直接内存,Cleaner 实例是一个指向 DirectByteBuffer 实例的虚引用
DirectByteBuffer(int cap) {                   super(-1, 0, cap, cap);boolean pa = VM.isDirectMemoryPageAligned();int ps = Bits.pageSize();// 多配分一个内存页, 用于直接内存起始地址对齐long size = Math.max(1L, (long)cap + (pa ? ps : 0));// 尝试保留size大小的内存, 如果内存不够, 处理pending链表上的引用// 内存仍然不足,则显式GC, 将不可达的引用放入pending链表中, 再从pending回收内存// 内存不够, 则抛出OOM错误Bits.reserveMemory(size, cap);long base = 0;try {// base为直接内存的基址base = unsafe.allocateMemory(size);} catch (OutOfMemoryError x) {Bits.unreserveMemory(size, cap);throw x;}// 将分配到的直接内存每一个Byte设置为0unsafe.setMemory(base, size, (byte) 0);// 如果需要直接内存对齐, 且基址base不整除pageSize, 则调整起始地址为base+pageSize减去base%pageSizeif (pa && (base % ps != 0)) {// address为ByteBuffer缓冲区可使用部分的起始地址address = base + ps - (base & (ps - 1));} else {address = base;}// CLeaner 持有 DirectByteBuffer 的幻影(虚)引用// Deallocator实现Runnable接口, 执行释放直接内存的操作cleaner = Cleaner.create(this, new Deallocator(base, size, cap));att = null;
}

直接内存的回收

Cleaner类继承虚引用 PhantomReference,虚引用的referent字段指向 DirectByteBuffer 实例。

虚引用:最弱的引用关系,一个对象是否有虚引用存在不对其生存时间构成影响,也无法通过虚引用获取对象实例,get 方法返回null

为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到系统通知。

public class Cleaner extends PhantomReference<Object> {...// Cleaner.create: var1传入DirectByteBuffer引用, var2传入Deallocator实例private Cleaner(Object var1, Runnable var2) {super(var1, dummyQueue);// DirectByteBuffer作为虚引用this.thunk = var2; // }public static Cleaner create(Object var0, Runnable var1) {return var1 == null ? null : add(new Cleaner(var0, var1));}
}

DirectByteBuffer 实例不存在强引用后,垃圾回收时它的 PhantomReference 实例会被放入 pending 链表,等待 ReferenceHandler 线程将它从 pending 链表中取出,加入到引用队列queue中。

ReferenceHandler 线程执行逻辑实现于 tryHandlePending 方法:

从 pending 链表中取出头部的 Reference 实例,如果引用实例为 Cleaner 类型,需要调用它的 clean 方法释放直接内存。随后,将 Reference 实例加入到引用队列 queue 中。

public void run() {while (true) {tryHandlePending(true);}
}static boolean tryHandlePending(boolean waitForNotify) {Reference<Object> r;Cleaner c;try {synchronized (lock) {if (pending != null) {r = pending;// Cleaner继承了虚引用, 需要调用clean方法, 因此特判。c = r instanceof Cleaner ? (Cleaner) r : null;// pending头节点更新为r的下一个节点pending = r.discovered;r.discovered = null;} else {// pending链表中元素为空, wait-notify等待唤醒if (waitForNotify) {lock.wait();}// retry if waitedreturn waitForNotify;}}}// ...// 如果Reference类型为Cleaner, 需要调用clean方法, 直接内存此时会被回收if (c != null) {c.clean();return true;}// 将Reference实例加入到引用队列中ReferenceQueue<? super Object> q = r.queue;// 注册了引用队列, 则入队, 入队后修改r.queue = ReferenceQueue.ENQUEUED, next指向队列中的后继if (q != ReferenceQueue.NULL) q.enqueue(r);return true;
}

从 pending 链表取出时,会调用 Cleaner#clean方法,clean方法会调用运行 Unsafe#freeMemory 释放直接内存。

// Cleaner
public void clean() {if (remove(this)) {try {this.thunk.run(); // thunk为Deallocator实例} // catch}
}// private static class Deallocator implements Runnable
public void run() {if (address == 0) {// Paranoiareturn;}// 释放直接内存, address为直接内存基址unsafe.freeMemory(address);address = 0;Bits.unreserveMemory(size, capacity);
}

Direct Buffer 性能优化方面的建议:

  • 应用程序中,System.gc() 触发Full GC,将 DirectByteBuffer 回收时调用 Cleaner#clean 方法释放直接内存。
    不要开启 -XX:+DisableExplicitGC 禁用显式GC,默认不禁用;
    使用 -XX:+ExplicitGCInvokesConcurrent 改变 Full GC 的行为(配合 CMS 使用)。添加该选项后,垃圾收集线程在可达性标记阶段与用户线程并发运行,减少了STW的时间

  • 另一种思路是,在大量使用Direct Buffer的部分框架中,框架会自己程序中显式地调用Unsafe#freeMemory方法,例如Netty。(使用反射获取 Unsafe 实例,再调用成员方法 freeMemory)

  • 重复利用 Direct Buffer,减少它的创建和销毁。

直接内存跟踪与诊断

直接内存的容量大小可通过 -XX:MaxDirectMemorySize 参数指定,默认与 Java堆最大值一致。
使用反射越过 DirectByteBuffer 类,直接通过反射获取 Unsafe 实例(theUnsafe静态属性),进行内存分配。

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
// theUnsafe为static final字段
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
// 分配直接内存
long address = unsafe.allocateMemory(1024);
unsafe.freeMemory(address);

由直接内存导致的内存溢出,在Heap Dump文件中不会看见明显的异常情况。如果发现内存溢出后,产生的Dump文件很小,而程序中直接或间接使用了Direct Memory(NIO),就可以考虑检查直接内存溢出


通常的垃圾收集日志等记录,并不包含 Direct Buffer 等信息。从JDK 1.8开始,可以使用 Native Memory Tracking(NMT) 特性来进行诊断,可以在程序启动时加上下面参数:

-XX:NativeMemoryTracking={summary|detail}

运行时,采用如下命令交互式对比:

// 打印NMT信息
jcmd <pid> VM.native_memory detail// 进行baseline,以对比分配内存变化
jcmd <pid> VM.native_memory baseline// 对比baseline, 显示出各个部分内存的变化
jcmd <pid> VM.native_memory detail.diff

下面案例中,先使用 VM.native_memory 的 baseline 命令,作为对比的参照;当打印出 Begin allocate 后,执行detail.diff,进行对比。

public class DirectMemory {public static void main(String[] args) {try {Thread.sleep(40000);// 进行baseline, 作为比对的参照System.out.println("Begin allocate: ...");ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 3);    Thread.sleep(40000);    } catch (Exception e) {e.printStackTrace();}}
}

结果如下图所示,Internal部分的内存增加了3078KB,3MB = 3072KB

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/785737.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【LeetCode】三月题解

文章目录 [2369. 检查数组是否存在有效划分](https://leetcode.cn/problems/check-if-there-is-a-valid-partition-for-the-array/)思路&#xff1a;代码&#xff1a; [1976. 到达目的地的方案数](https://leetcode.cn/problems/number-of-ways-to-arrive-at-destination/) 思路…

C++教学——从入门到精通 5.单精度实数float

众所周知&#xff0c;三角形的面积公式是(底*高)/2 那就来做个三角形面积计算器吧 到吗如下 #include"bits/stdc.h" using namespace std; int main(){int a,b;cin>>a>>b;cout<<(a*b)/2; } 这不对呀&#xff0c;明明是7.5而他却是7&#xff0c;…

让IIS支持.NET Web Api PUT和DELETE请求

前言 有很长一段时间没有使用过IIS来托管应用了&#xff0c;今天用IIS来托管一个比较老的.NET Fx4.6的项目。发布到线上后居然一直调用不同本地却一直是正常的&#xff0c;关键是POST和GET请求都是正常的&#xff0c;只有PUT和DELETE请求是有问题的。经过一番思考忽然想起来了I…

YOLOv9改进策略 :主干优化 | 极简的神经网络VanillaBlock 实现涨点 |华为诺亚 VanillaNet

💡💡💡本文改进内容: VanillaNet,是一种设计优雅的神经网络架构, 通过避免高深度、shortcuts和自注意力等复杂操作,VanillaNet 简洁明了但功能强大。 💡💡💡引入VanillaBlock GFLOPs从原始的238.9降低至 165.0 ,保持轻量级的同时在多个数据集验证能够高效涨点…

每日学习笔记:C++ STL算法分类

非更易型 更易型 移除型 变序型 排序型 已排序区间算法 数值型算法

【滑动窗口】Leetcode 将 x 减到 0 的最小操作数

题目解析 1658. 将 x 减到 0 的最小操作数 算法讲解 这道题按照题目要求的话会变得很难&#xff0c;因为不仅需要考虑数字减到0&#xff0c;还需要考虑最小的操作数。正难则反&#xff0c;按照这个思路&#xff0c;我们来解析题目 这道题本质上无非就是在左边寻找一段区间&a…

HCIP第三次作业(综合)

一、实验要求 二、实验步骤 1、配置IP地址部分 PC1&#xff1a; PC2&#xff1a; PC3&#xff1a; PC4&#xff1a; R1&#xff1a; R2&#xff1a; R3&#xff1a; R4&#xff1a; R5&#xff1a; 环回&#xff1a; 2.通过配置缺省路由让公网互通 [R1]ip route-static 0.0.…

代码随想录第27天| 39. 组合总和

39. 组合总和 39. 组合总和 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 带你学透回溯算法-组合总和&#xff08;对应「leetcode」力扣题目&#xff1a;39.组合总和&#xff09;| 回溯法精讲&#xff01;_哔哩哔哩_bilibili 给你一个 无重复元…

思考:开启MMU瞬间可能出现的多种问题以及多种解决方案

快速链接: 【精选】ARMv8/ARMv9架构入门到精通-[目录] &#x1f448;&#x1f448;&#x1f448; (说明本文的介绍都是基于armv8-aarch64或armv9硬件架构) 在mmu未开启阶段&#xff0c;PC操作的都是物理地址执行程序&#xff0c;这样看起来一切正常&#xff0c;没啥问题。 例如…

Windows Server 2022 使用ApacheDS用户远程桌面登录服务器

Windows Server 2022 使用ApacheDS用户远程桌面登录服务器 1、接上篇 Windows Server 2022 使用ApacheDS用户认证 使用Administrator用户远程登录192.168.1.100windows server&#xff0c;打开pGina软件 2、输入刚刚在ApacheDS中的新添加的用户测试一下&#xff0c;会自动添加…

如何在极狐GitLab 配置 邮件功能

本文作者&#xff1a;徐晓伟 GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 本文主要讲述了在极狐GitLab 用户…

【带你了解下前端开发语言有那些】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

爱上数据结构:二叉树的基本概念

​ ​ &#x1f525;个人主页&#xff1a;guoguoqiang. &#x1f525;专栏&#xff1a;数据结构 ​ 一、树的基本概念 1.概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因 为它看起…

计算机网络数据链路层知识总结

物理层知识总结传送门 计算机网络物理层知识点总结-CSDN博客 功能 功能概述 一些基本概念 结点:主机、路由器链路﹔网络中两个结点之间的物理通道&#xff0c;链路的传输介质主要有双绞线、光纤和微波。分为有线链路、无线链路。数据链路︰网络中两个结点之间的逻辑通道&a…

Prometheus+grafana环境搭建rabbitmq(docker+二进制两种方式安装)(二)

搭建完Prometheusgrafana基础环境后参见&#xff1a;Prometheusgrafana环境搭建方法及流程两种方式(docker和源码包)(一)-CSDN博客&#xff0c;对我本地的一些常用法人服务进行一个监控。基本都可以根据官方文档完成搭建&#xff0c;因为docker和二进制方式安装各有优缺点。 d…

隐私计算实训营学习七:隐语SCQL的架构详细拆解

文章目录 一、SCQL Overview1.1 SCQL背景1.2 SCQL Overview 二、SCQL CCL三、SCQL架构 一、SCQL Overview 1.1 SCQL背景 SCQL&#xff1a;属于隐私计算BI范畴&#xff0c;允许多个互不信任参与方在不泄露各自隐私数据的条件下进行联合数据分析。 如下数据在不同机构&#xf…

Unity 学习日记 12.小球撞击冰块游戏

目录 1.准备场景 2.让小球动起来 3.用鼠标把小球甩出去 4.加入鼠标点击小球的判断 5.小球与冰块的碰撞测试 6.撞击后销毁冰块 ​编辑 7.显示游戏计时 8.显示扔球次数 9.显示剩余冰块个数 10.游戏结束 11.完整代码 下载源码 UnityPackage 最终效果&#xff1a; 1.准…

基于springboot+vue实现的房源出租信息系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

HQL,SQL刷题,尚硅谷(初级)

目录 相关表数据&#xff1a; 题目及思路解析&#xff1a; 多表连接 1、课程编号为"01"且课程分数小于60&#xff0c;按分数降序排列的学生信息 2、查询所有课程成绩在70分以上 的学生的姓名、课程名称和分数&#xff0c;按分数升序排列 3、查询该学生不同课程的成绩…

网络中的网络(NiN)

文章目录 NiN块 NiN块 NiN的想法是在每个像素位置&#xff08;针对每个高度和宽度&#xff09;应用一个全连接层。果我们将权重连接到每个空间位置&#xff0c;我们可以将其视为1x1卷积层&#xff0c;或作为在每个像素位置上独立作用的全连接层。 从另一个角度看&#xff0c;即…