Netty Review - 直接内存的应用及源码分析

文章目录

  • Pre
  • 概述
  • 应用
    • 访问效率: 堆内存 VS 直接内存
    • 申请效率: 堆内存 VS 直接内存
    • 数据存储结构: 堆内存 VS 直接内存
    • 结论
  • ByteBuffer.allocateDirect 源码分析
    • unsafe.allocateMemory(size) ---> C++方法
  • JVM参数 -XX:MaxDirectMemorySize
  • 直接内存如何管理?
    • 理论
    • Code
  • 总结
    • 优点
    • 缺点

在这里插入图片描述

在这里插入图片描述


Pre

Netty Review - ServerBootstrap源码解析

Netty Review - NioServerSocketChannel源码分析

Netty Review - 服务端channel注册流程源码解析


概述

在Java中,数据通常存储在堆内存中。Java里用DirectByteBuffer可以分配一块直接内存(堆外内存),元空间对应的内存也叫作直接内存,它们对应的都是机器的物理内存。

在这里插入图片描述

但是,在某些情况下,直接操作系统的本地内存(off-heap memory)可能更有利,特别是对于需要进行大量I/O操作的应用程序,比如网络应用程序。Netty是一个用于构建高性能网络应用程序的框架,它提供了对直接内存的支持,以便更有效地处理数据传输。

直接内存的主要优势在于它的分配和释放不受Java堆内存管理的影响,因此可以避免堆内存的垃圾回收开销。由于直接内存是在操作系统层面分配和释放的,因此它不受Java虚拟机的堆内存大小限制,可以更灵活地管理大量的数据。


在Netty中,直接内存通常用于存储网络数据,例如接收到的字节数据或要发送的字节数据。通过使用直接内存,Netty能够更有效地进行数据传输,减少了数据在Java堆内存和操作系统内存之间的复制操作,提高了数据传输的效率和性能

为了有效地管理直接内存的分配和释放,Netty使用了内存池(Memory Pool)的概念。通过内存池,Netty可以重用已分配的直接内存,避免频繁地进行内存分配和释放操作,减少了系统的内存管理开销,并提高了系统的稳定性和可靠性。

总而言之,Netty的直接内存支持使得开发人员能够构建高性能、高效率的网络应用程序,通过更有效地利用操作系统的本地内存,提高了数据传输的速度和性能,同时降低了系统的内存管理开销。


应用

访问效率: 堆内存 VS 直接内存

package com.artisan.directbuffer;

import java.nio.ByteBuffer;

/**
 * 直接内存与堆内存的区别
 * @author artisan
 */
public class DirectMemoryTest {

    public static void heapAccess() {
        long startTime = System.currentTimeMillis();
        //分配堆内存
        ByteBuffer buffer = ByteBuffer.allocate(1000);
        for (int i = 0; i < 100000; i++) {
            for (int j = 0; j < 200; j++) {
                buffer.putInt(j);
            }
            buffer.flip();

            for (int j = 0; j < 200; j++) {
                buffer.getInt();
            }
            buffer.clear();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("堆内存访问:" + (endTime - startTime) + "ms");
    }

    public static void directAccess() {
        long startTime = System.currentTimeMillis();
        //分配直接内存
        ByteBuffer buffer = ByteBuffer.allocateDirect(1000);
        for (int i = 0; i < 100000; i++) {
            for (int j = 0; j < 200; j++) {
                buffer.putInt(j);
            }
            buffer.flip();
            for (int j = 0; j < 200; j++) {
                buffer.getInt();
            }
            buffer.clear();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("直接内存访问:" + (endTime - startTime) + "ms");
    }

  
    public static void main(String args[]) {
        for (int i = 0; i < 5; i++) {
            heapAccess();
            directAccess();
        }
    }
}

在这里插入图片描述


申请效率: 堆内存 VS 直接内存

package com.artisan.directbuffer;

import java.nio.ByteBuffer;

/**
 * 直接内存与堆内存的区别
 * @author artisan
 */
public class DirectMemoryTest {

    public static void heapAllocate() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            ByteBuffer.allocate(100);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("堆内存申请:" + (endTime - startTime) + "ms");
    }

    public static void directAllocate() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            ByteBuffer.allocateDirect(100);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("直接内存申请:" + (endTime - startTime) + "ms");
    }

    public static void main(String args[]) {
 
        for (int i = 0; i < 5; i++) {
            heapAllocate();
            directAllocate();
        }
    }
}

在这里插入图片描述


数据存储结构: 堆内存 VS 直接内存

在这里插入图片描述

在这里插入图片描述


结论

优点:

  • 不占用堆内存空间,减少了发生GC的可能
  • java虚拟机实现上,本地IO会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)

缺点:

  • 初始分配较慢
  • 没有JVM直接帮助管理内存,容易发生内存溢出。为了避免一直没有FULL GC,最终导致直接内存把物理内存耗完。我们可以指定直接内存的最大值,通过-XX:MaxDirectMemorySize来指定,当达到阈值的时候,调用system.gc来进行一次FULL GC,间接把那些没有被使用的直接内存回收掉

从程序运行结果看出直接内存申请较慢,但访问效率高。在java虚拟机实现上,本地IO一般会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)。

直接内存在申请时可能会比在堆内存中分配的速度慢一些,这是因为在堆内存中分配只涉及Java堆内存管理系统的操作,而在直接内存中分配则涉及到操作系统的系统调用,因此可能会有更多的开销。但是,一旦分配完成,直接内存的访问效率通常会比堆内存高,因为它可以直接被操作系统访问,而不需要经过Java堆内存管理系统的复制操作。

在Java虚拟机的实现中,对于本地IO操作,如果使用直接内存,则可以直接操作直接内存,然后通过系统调用将数据传输到硬盘或网卡。这样可以避免额外的内存复制操作,提高了IO操作的效率。而对于堆内存中的数据,需要先将数据复制到直接内存中,然后再进行系统调用传输到硬盘或网卡,这就需要进行额外的数据拷贝,导致了额外的开销和性能损失。

因此,对于需要进行大量IO操作的应用程序,使用直接内存通常能够获得更好的性能表现。然而,直接内存的使用也需要注意管理和释放,以避免内存泄漏和其他潜在的问题。


ByteBuffer.allocateDirect 源码分析

public static ByteBuffer allocateDirect(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new DirectByteBuffer(capacity);
}

这是DirectByteBuffer类的构造函数实现,用于创建直接字节缓冲区对象。

DirectByteBuffer(int cap) { // 包私有

    super(-1, 0, cap, cap); // 调用父类构造函数,设置position和limit为0,capacity为cap

    // 判断是否需要在直接内存页对齐
    boolean pa = VM.isDirectMemoryPageAligned();
    // 获取操作系统页面大小
    int ps = Bits.pageSize();
    // 计算需要分配的内存大小,如果需要页对齐,则在容量上加上一个页面大小
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));


//判断是否有足够的直接内存空间分配,可通过‐XX:MaxDirectMemorySize=<size>参数指定直接内存最大可分配空间,如果不指定默认为最大堆内存大小,

//在分配直接内存时如果发现空间不够会显示调用System.gc()触发一次full gc回收掉一部分无用的直接内存的引用对象,同时直接内存也会被释放掉. 如果释放完分配空间还是不够会抛出异常java.lang.OutOfMemoryError

// 预留直接内存,如果分配失败,抛出OutOfMemoryError
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        // 分配内存,并返回分配的内存地址
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        // 如果分配失败,释放预留的内存,并抛出异常
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    // 初始化分配的内存为0
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // 如果需要页对齐,并且分配的内存地址不在页面边界上,将地址向上舍入到页面边界
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 创建Cleaner对象,用于释放内存
    // 使用Cleaner机制注册内存回收处理函数,当直接内存引用对象被GC清理掉时,会提前调用这里注册的释放直接内存的Deallocator线程对象的run方法
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

该构造函数执行以下操作:

  1. 调用父类构造函数,初始化ByteBuffer的position和limit为0,capacity为cap。
  2. 判断是否需要进行直接内存页对齐。
  3. 获取操作系统的页面大小。
  4. 计算需要分配的内存大小,如果需要页对齐,则在容量上加上一个页面大小。
  5. 预留直接内存。
  6. 分配内存,并返回分配的内存地址。
  7. 初始化分配的内存为0。
  8. 如果需要进行页对齐,并且分配的内存地址不在页面边界上,将地址向上舍入到页面边界。
  9. 创建Cleaner对象,用于释放内存。
  10. 最后,att字段设置为null,该字段用于存储可选的附件对象。

unsafe.allocateMemory(size) —> C++方法

UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
    // 使用UnsafeWrapper宏定义的函数进行包装
    UnsafeWrapper("Unsafe_AllocateMemory");

    // 将传入的jlong类型的size转换为size_t类型的sz,这是为了在C++中使用
    size_t sz = (size_t)size;

    // 检查size是否为负数或超出了jlong类型的范围,如果是,则抛出IllegalArgumentException异常
    if (sz != (julong)size || size < 0) {
        THROW_0(vmSymbols::java_lang_IllegalArgumentException());
    }

    // 如果size为0,则直接返回0,不进行内存分配
    if (sz == 0) {
        return 0;
    }

    // 将sz向上舍入到HeapWordSize的倍数,确保内存对齐
    sz = round_to(sz, HeapWordSize);

    // 调用os::malloc函数分配内存。os::malloc是JVM调用操作系统的内存分配函数,通常是malloc
    void* x = os::malloc(sz, mtInternal);

    // 如果内存分配失败(返回NULL),则抛出OutOfMemoryError异常
    if (x == NULL) {
        THROW_0(vmSymbols::java_lang_OutOfMemoryError());
    }

    // 将分配的内存地址转换为Java对象引用并返回
    return addr_to_java(x);
UNSAFE_END

这段代码是在C++中实现的Unsafe_AllocateMemory函数, 通过Unsafe类的allocateMemory方法在本地堆上分配内存的底层实现。


JVM参数 -XX:MaxDirectMemorySize

在Java应用的默认情况下,可以使用的最大直接内存取决于系统的限制和JVM参数的设置。一般来说,默认情况下,JVM并不限制直接内存的使用,但是操作系统可能会对进程的虚拟内存大小进行限制。

在某些操作系统上,进程可以使用的虚拟内存大小受到32位或64位系统的限制,以及特定操作系统的配置和限制。另外,有些操作系统还可能会限制单个进程可分配的直接内存的大小。

通常情况下,如果没有显式设置直接内存的大小(例如通过-XX:MaxDirectMemorySize参数),Java应用程序可以使用的最大直接内存大小与堆内存大小没有直接关系。默认情况下,Java应用程序可以根据操作系统和硬件配置使用尽可能多的直接内存,但是受到操作系统的限制。

也有一种说法,默认能够使用的最大的直接内存 = 最大堆内存。 待考证。


直接内存如何管理?

理论

ByteBuffer.allocateDirect() 方法用于申请直接内存,这种内存是由操作系统分配的,而不是由JVM的堆内存管理器分配的。因此,直接内存的管理和回收与堆内存不同。

直接内存的管理和回收通常由操作系统来完成。当调用 allocateDirect() 方法时,会向操作系统请求一块内存区域。这个区域的分配和释放由操作系统的内存管理机制来管理,而不是由JVM的垃圾回收器来管理。

直接内存的释放不是由 Java 的垃圾回收器来处理的,而是由操作系统的内存管理机制来处理。当不再需要直接内存时,ByteBuffer 对象可以被垃圾回收器回收,但是直接内存本身并不会被立即释放。相反,直接内存的释放可能会延迟到 JVM 关闭时,或者在应用程序调用 System.gc() 进行垃圾回收时。

另外,可以使用 ByteBufferclear() 方法或者手动调用 System.gc() 来提示 JVM 尽快释放直接内存。但是这并不能保证直接内存会立即被释放,因为直接内存的释放时间是由操作系统来决定的。


Code

一个简单的案例和代码实现,演示了如何手动管理直接内存的分配和释放

package com.artisan.directbuffer;

import java.nio.ByteBuffer;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class DirectMemoryManager {


    private ByteBuffer buffer;

    public DirectMemoryManager(int capacity) {
        // 申请直接内存
        buffer = ByteBuffer.allocateDirect(capacity);
    }

    // 使用直接内存进行读写操作
    public void writeToBuffer(byte[] data) {
        buffer.put(data);
    }

    
    public byte[] readFromBuffer() {
        // 将缓冲区的位置设为 0,限制设为当前位置
        buffer.flip();
        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);
        return data;
    }

    // 释放直接内存
    public void releaseMemory() {
        // 释放 buffer 对象
        buffer.clear();
        // 将 buffer 设置为 null,便于垃圾回收
        buffer = null;
        // 强制调用垃圾回收器进行内存回收
        System.gc();
    }

    public static void main(String[] args) {
        // 创建直接内存管理器并分配直接内存
        DirectMemoryManager memoryManager = new DirectMemoryManager(1024);

        // 使用直接内存进行读写操作
        byte[] data = "Hello, Direct Memory!".getBytes();
        memoryManager.writeToBuffer(data);
        byte[] readData = memoryManager.readFromBuffer();
        System.out.println("Read from buffer: " + new String(readData));

        // 释放直接内存
        memoryManager.releaseMemory();
    }
}
    

在这个例子中,DirectMemoryManager 类负责管理直接内存。它通过调用 ByteBuffer.allocateDirect(capacity) 方法来申请直接内存,并通过 writeToBuffer()readFromBuffer() 方法进行读写操作。

最后,通过调用 releaseMemory() 方法释放直接内存。

在释放直接内存时,首先调用 buffer.clear() 方法清空缓冲区,然后将 buffer 对象置为 null,最后强制调用 System.gc() 方法触发垃圾回收。

请注意,这并不能保证直接内存会立即被释放,因为直接内存的释放时间是由操作系统来决定的。

在这里插入图片描述


总结

优点

  1. 减少了垃圾回收的影响: 直接内存不受 Java 堆内存大小的限制,可以避免频繁的垃圾回收,提高应用的性能和稳定性。

  2. 减少了数据拷贝: 直接内存与操作系统进行了直接交互,减少了数据在 Java 堆内存与操作系统内存之间的拷贝次数,提高了 I/O 操作的效率。

缺点

  1. 初始分配较慢: 直接内存的分配过程通常比堆内存的分配要慢,因为它需要调用操作系统的原生方法来申请内存空间。

  2. 内存管理复杂: 直接内存的管理需要手动进行,没有 JVM 自动进行内存回收的机制,容易导致内存泄漏或者内存溢出的问题。

  3. 容易导致内存溢出: 如果不合理地使用直接内存,可能会导致操作系统的物理内存被耗尽,从而引发应用程序的崩溃。

  4. 难以调试: 直接内存的内存溢出问题比较难以调试和定位,需要借助一些专业的内存分析工具来进行排查。

总的来说,直接内存适用于需要频繁进行 I/O 操作或者数据量较大的场景,但需要开发人员在使用时注意合理管理和控制直接内存的大小,避免出现性能问题或者内存溢出的情况。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

视觉slam十四讲学习笔记(五)非线性优化

已经知道&#xff0c;方程中的位姿可以由变换矩阵来描述&#xff0c;然后用李代数进行优化。观测方程由相机成像模型给出&#xff0c;其中内参是随相机固定的&#xff0c;而外参则是相机的位姿。 目录 前言 一、状态估计问题 1 最大后验与最大似然 2 最小二乘的引出 二、非…

JavaScript中null和undefined的区别

JavaScript中null和undefined是两个特殊的值&#xff0c;经常在编程中遇到。虽然它们经常被混淆&#xff0c;但它们有着不同的含义和用法。本文将详细介绍JavaScript中null和undefined的区别&#xff0c;帮助开发者更好地理解和使用它们。 首先&#xff0c;让我们来了解一下nu…

css篇---移动端适配的方案有哪几种

移动端适配 移动端适配是指同一个页面可以在不同的移动端设备上都有合理的布局。主流实现的方案有 响应式布局通过rem或者vw,vh 等实现不同设备有相同的比例而实现适配 首先需要了解viewport 【视口】 视口代表了一个可看见的多边形区域&#xff08;通常来说是矩形&#xff0…

函数递归与迭代附n的阶乘+顺序打印一个整数的每一位数+求第n个斐波那契数

1. 什么是递归&#xff1f; 递归其实是一种解决问题的方法&#xff0c;在C语言中&#xff0c;递归就是函数自己调用自己。 下面是一个最简单的C语言递归代码&#xff1a; #include <stdio.h> int main() {printf("hehe\n");main();//main函数中⼜调⽤了main函数…

BBC英式口语~发音练习~笔记整理

参考资料 原视频地址&#xff1a; https://www.bilibili.com/video/BV1D7411n7bS/?spm_id_from333.1245.0.0&vd_source5986fc7c8e6d754f3ca44233573aeaff 笔记图片

2.11题目

#include <stdio.h> int main() { char a; while((a getchar()) ! -1) { if(a > A && a < Z) a32; putchar(ch); } return 0;} ———————————————— 版权声明&#xff1a;本文为博主原创文章…

阿里云/腾讯云幻兽帕鲁服务器据点最大帕鲁工作数量最多15个,改成20不生效?

例如&#xff0c;在阿里云的计算巢管理中&#xff0c;找到你的这台部署幻兽帕鲁的服务器实例&#xff0c;选择右上角的“修改游戏配置” 然后选择“基地内工作帕鲁的最大数量”改成20 有人说更改上面的数字&#xff0c;根本不起作用。原因可能如下&#xff1a; 参考资料&#…

css篇---分辨率物理像素和逻辑像素

物理分辨率和逻辑分辨率 物理分辨率是生产屏幕时就固定的&#xff0c;它是不可改变的 -----电脑像素 逻辑分辨率是由软件决定的 【电脑的设置中可以修改分辨率】----css像素 设备像素比 dpr同一方向上的物理像素/css像素 &#xff08;缩放比是1的情况&#xff09; 假设dpr4/…

网络安全最典型基础靶场-DVWA-本地搭建与初始化

写在前面&#xff1a; 之前也打过这个 DVWA 靶场&#xff0c;但是是在虚拟机环境下的一个小块分区靶场&#xff1b; 本篇博客主要介绍在本地搭建 DVWA 靶场以及靶场的初始化&#xff0c;后续会陆续更新通关教程。 由于我们是在本地搭建&#xff0c;则需要基于你已经装好 phpstu…

cefsharp121(cef121.3.7Chromium121.0.6167.160)升级测试及其他H264版本

一、版本说明 1.1 本此版本 此版本CEF 121.3.7+g82c7c57+chromium-121.0.6167.160 / Chromium 121.0.6167.160 1.2 其他支持H264版本 支持H264推荐版本:V100,V109,V111,V119版本,其他V114,V115,V108,V107 支持win7/win8/win8.1最后版本v109.x 支持NET4.5.2最后版本v114.x …

一览大模型长文本能力

前言 如今的大模型被应用在各个场景&#xff0c;其中有些场景则需要模型能够支持处理较长文本的能力(比如8k甚至更长)&#xff0c;其中已经有很多开源或者闭源模型具备该能力比如GPT4、Baichuan2-192K等等。 那关于LLM的长文本能力&#xff0c;目前业界通常都是怎么做的&…

STM32 寄存器操作 GPIO 与下降沿中断

一、如何使用stm32寄存器点灯&#xff1f; 1.1 寄存器映射表 寄存器本质就是一个开关&#xff0c;当我们把芯片寄存器配置指定的状态时即可使用芯片的硬件能力。 寄存器映射表则是开关的地址说明。对于我们希望点亮 GPIO_B 的一个灯来说&#xff0c;需要关注以下的两个寄存器…

leetcode hot100不同路径

本题可以采用动态规划来解决。还是按照五部曲来做 确定dp数组&#xff1a;dp[i][j]表示走到&#xff08;i&#xff0c;j&#xff09;有多少种路径 确定递推公式&#xff1a;我们这里&#xff0c;只有两个移动方向&#xff0c;比如说我移动到&#xff08;i&#xff0c;j&#x…

【机器学习】数据清洗之识别重复点

&#x1f388;个人主页&#xff1a;甜美的江 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;机器学习 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步…

React入门到精通:掌握前端开发的必备技能!

介绍&#xff1a;React是一个由Facebook开发和维护的JavaScript库&#xff0c;用于构建用户界面&#xff0c;特别是用于构建单页应用程序和移动应用程序的用户界面。以下是对React的详细介绍&#xff1a; 虚拟DOM&#xff1a;React通过使用虚拟DOM&#xff08;Document Object …

Rust 数据结构与算法:3栈:用栈实现符号匹配

1、符号匹配 如&#xff1a; (56)(78)/(43)、{ { ( [ ] [ ])}}、(ab)(c*d)func() 等各类语句的符号匹配。 这里我们关注的不是数字而是括号&#xff0c;因为括号更改了操作优先级&#xff0c;限定了语言的语义&#xff0c;这是非常重要的。如果括号不完整&#xff0c;那么整个…

C语言指针(初阶)

文章目录 1:内存与地址1.1内存1.2:如何理解编址 2:指针变量与地址2.1:指针变量与解引用操作符2.1.1:指针变量2.1.2:如何拆解指针类型2.1.3:解引用操作符 2.2:指针变量的大小 3:指针变量类型的意义代码1解引用修改前解引用修改后 代码2解引用修改前解引用修改后 4:const修饰指针…

RSIC-V“一芯”学习笔记(三)——读后感以及部分PA0工作

文章目录 一、别像弱智一样提问二、提问的智慧三、安装linux以及配置问题3.1 关于问题配置 一、别像弱智一样提问 提问前&#xff0c;应该清晰问自己几个问题&#xff0c;1. 是否尝试了在搜索引擎进行搜索过2. 相关的手册和文档是否看了3. 找找有没有常见的问题文档&#xff0…

Android Jetpack:提高开发效率的终极工具集

Android Jetpack&#xff1a;提高开发效率的终极工具集 1. 引言 Android Jetpack是一套为Android应用程序开发提供帮助的工具集。它旨在简化开发流程&#xff0c;提高开发效率&#xff0c;并提供一致的用户体验。无论您是新手还是经验丰富的开发者&#xff0c;Jetpack都可以为…

命令行参数和环境变量

命令行参数 命令行参数是在用户在命令行中输入命令时&#xff0c;跟随命令一起输入的一些附加信息。这些参数可以用来配置命令的行为或传递一些数据给命令。 让同样的程序在不同的命令行参数下运行出不同的结果&#xff01; 将这些命令和参数可以传给 main 函数生&#xff0…