Java 堆外内存及调优

news2024/11/25 13:34:54

文章目录

  • 直接内存简介
    • 为什么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设置为0
    unsafe.setMemory(base, size, (byte) 0);
    // 如果需要直接内存对齐, 且基址base不整除pageSize, 则调整起始地址为base+pageSize减去base%pageSize
    if (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 waited
                return 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) {
        // Paranoia
        return;
    }
    // 释放直接内存, 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.coloradmin.cn/o/1561540.html

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

相关文章

动手机器学习支持向量机+习题

非参数化模型&#xff0c;当数据集规模增大时&#xff0c;其参数量也相应变多 希望从这无数个可以分隔两个点集的超平面中&#xff0c;挑选出与任意一点间隔&#xff08;margin&#xff09;的最小值最大的平面 支持向量机的数学描述 对上式来说&#xff0c;当w和b的大小同时变…

书生·浦语大模型实战营(第二期)-书生·浦语大模型全链路开源体系

课程来源&#xff1a;书生浦语大模型全链路开源体系 1.大模型成为发展通用人工智能的重要途径 专用模型:针对特定任务,一个模型解决一个问题 通用大模型:一个模型应对多种任务、多种模态 2.书生浦语大模型开源历程 书生浦语2.0面向不同的使用需求,每个规格包含三个模型版本:I…

golang语言系列:Authentication、OAuth、JWT 认证策略

云原生学习路线导航页&#xff08;持续更新中&#xff09; 本文是 golang语言系列 文章&#xff0c;主要对编程通用技能 Authentication、OAuth、JWT 认证策略 进行学习 1.Basic Authentication认证 每个请求都需要将 用户名密码 进行base64编码后&#xff0c;放在请求头的Aut…

04-MySQL数据库-权限管理

一、查看权限 1&#xff0c;查看系统所有权限 mysql> show privileges; 权限字段介绍 privileges #权限名称 context #对象&#xff0c;表示可以对数据库&#xff0c;那些资源、进行哪些操作&#xff1b; comment #描述&#xff0c;备注解释说明&#xff1b; Grant…

C++刷题篇——04找等值元素

一、题目 二、解题思路 1、分割后放进二维数组 2、使用map&#xff0c;key为数值&#xff0c;value为其坐标 3、遍历二维数组元素&#xff0c;再在map中找该元素对应的value值&#xff08;二维数组形式&#xff09;&#xff0c;倘若value.size为1&#xff0c;那直接返回-1&…

稳定性生产总结

本期我们来谈下稳定性生产这个话题&#xff0c;稳定性建设目标有两个&#xff1a;降发生、降影响&#xff0c; 在降发生中的措施是做到三点&#xff1a;系统高可用、 高性能、 高质量&#xff0c;三高问题确实是一个很热的话题&#xff0c;里面涉及很多点。 在降影响中要做到…

数据结构03:栈、队列和数组 队习题01[C++]

考研笔记整理~&#x1f95d;&#x1f95d; 之前的博文链接在此&#xff1a;数据结构03&#xff1a;栈、队列和数组_-CSDN博客~&#x1f95d;&#x1f95d; 本篇作为链表的代码补充&#xff0c;供小伙伴们参考~&#x1f95d;&#x1f95d; 第1版&#xff1a;王道书的课后习题…

Mamba和状态空间模型(SSM)的视觉指南:替代 Transformers 的语言建模方法

原文地址&#xff1a; A Visual Guide to Mamba and State Space Models 2024 年 2 月 19 日 论文地址&#xff1a;https://arxiv.org/pdf/2312.00752.pdf 这篇论文介绍了一种新型的线性时间序列模型Mamba&#xff0c;它通过选择性状态空间&#xff08;Selective State Space…

【Redis持久化】RDB、ROB介绍和使用

RDB、ROB介绍和使用 引言ROB介绍配置指令介绍使用指令&#xff1a;dump文件修复指令快照禁用 AOF工作流程&#xff1a;文件重写&#xff1a;三种写回策略&#xff1a; 混合使用 引言 持久化的目的&#xff0c;其实就是在Redis重启或者中途崩溃的时候能够依靠自身恢复数据&…

953: 单链表的删除操作的实现

学习版 【C语言】 【C】 #include <iostream>class MyLinkedList{public:struct LinkedNode{int val;LinkedNode* next;LinkedNode(int x):val(x),next(NULL){};}; MyLinkedList(){dummyHeadnew LinkedNode(0);length0;}~MyLinkedList(){while (dummyHead){LinkedNode…

vulhub打靶记录——driftingbox

文章目录 主机发现端口扫描目录扫描爆破子域名提权总结 主机发现 使用nmap扫描局域网内存活的主机&#xff0c;命令如下&#xff1a; nmap -sP 192.168.56.0/24192.168.56.1&#xff1a;主机IP&#xff1b;192.168.56.100&#xff1a;DHCP服务器IP&#xff1b;192.168.56.101…

代码随想录第二十六天 | 回溯算法P3 |● 39. ● 40.● 131.

39. 组合总和 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target &#xff0c;找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 &#xff0c;并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制重…

windows下部署llama.cpp

下载cmake 下载地址 解压&#xff0c;设置Path环境变量D:\CMake\bin 打开cmd输入cmake -version 安装mingw powershell下执行 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser iex "& {$(irm get.scoop.sh)} -RunAsAdmin" scoop bucket add extras s…

蓝桥杯算法题——暴力枚举法

先估算这个数小于3的50次方 cnt0 for i in range(50):for j in range(50):for k in range(50):a3**ib5**jc7**kif a*b*c<59084709587505:cnt1 print(cnt-1)#当ijk都为0时&#xff0c;a*b*c1不是幸运数字所以要减去

C++笔记:命名空间

引入&#xff1a; 平常&#xff0c;我们在进行C编写时&#xff0c;一般我们都会默认在开始去写这样的代码&#xff1a; #include<iostream>//包含头文件using namespace std;//展开命名空间 这里就出现了与C语言不同的地方&#xff1a;这里的命名空间就是C对于C语言进…

深度学习| DiceLoss解决图像数据不平衡问题

图像数据不平衡问题 图像数据不平衡&#xff1a;在进行图像分割时&#xff0c;二分类问题中&#xff0c;背景过大&#xff0c;前景过小&#xff1b;多分类问题中&#xff0c;某一类别的物体体积过小。在很多图像数据的时候都会遇到这个情况&#xff0c;尤其是在医学图像处理的…

如何快速掌握数字化运维方法,构建数字化运维体系?

⛳️ 写在前面参与规则&#xff01;&#xff01;&#xff01; ✅参与方式&#xff1a;关注博主、点赞、收藏、评论&#xff0c;任意评论&#xff08;每人最多评论三次&#xff09; ⛳️本次送书1~4本【取决于阅读量&#xff0c;阅读量越多&#xff0c;送的越多】 主要内容读者…

操作符:左移(<<)右移(>>)

在介绍移位操作符前&#xff0c;我们先介绍一下原码反码和补码 这里要讲的左移和右移牵扯到原码补码和反码&#xff0c; 原码即这个整数转化为2进制时的一串&#xff0c; 正整数的原码、反码、补码相同&#xff0c; 10&#xff08;int类型&#xff09;的原码&#xff1a;00…

day4 linux上部署第一个nest项目(java转ts全栈/3R教室)

背景&#xff1a;上一篇吧nest-vben-admin项目&#xff0c;再开发环境上跑通了&#xff0c;并且build出来了dist文件&#xff0c;接下来再部署到linux试试吧 dist文件夹是干嘛的&#xff1f; 一个pnpn install 直接生成了两个dist文件夹&#xff0c;前端admin项目一个&#xf…

黑马鸿蒙笔记 3

目录 11.ArkUI组件-Column和Row 12.ArkUI组件-循环控制 13.ArkUI组件-List 14.ArkUI组件-自定义组件 15.ArkUI组件-状态管理State装饰器 16.ArkUI组件-状态管理-任务统计案例 17.ArkUI组件-状态管理-PropLinkProvideConsume 11.ArkUI组件-Column和Row Colum和Row的交叉…