惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

文章列表

设计模式-备忘录模式 - OXOXTECH 牛牛技术客栈 设计模式-中介者模式 - OXOXTECH 牛牛技术客栈 Linux【Ubuntu】修改ssh默认端口 - OXOXTECH 牛牛技术客栈 设计模式-迭代器模式 - OXOXTECH 牛牛技术客栈 scheduled定时任务的三种基本实现方式 - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(三) - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(二) - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(一) - OXOXTECH 牛牛技术客栈 基于JavaFX的桌面端网络调试工具 - OXOXTECH 牛牛技术客栈 Golang Channel的原理介绍 - OXOXTECH 牛牛技术客栈 Go语言Map的原理分析 - OXOXTECH 牛牛技术客栈 Go语言错误处理(panic)的最佳实践 - OXOXTECH 牛牛技术客栈 设计模式-解释器模式 - OXOXTECH 牛牛技术客栈 Redis报错Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. go-webpbin库在Linux报错failed to encode image to WebP: exit status 1.......的问题 exe4j 打包加密的jar - OXOXTECH 牛牛技术客栈 Go生成图形验证码示例 - OXOXTECH 牛牛技术客栈 澳门一天游:一日尽享东方与西方的交融之美 - OXOXTECH 牛牛技术客栈 设计模式-命令模式 - OXOXTECH 牛牛技术客栈 别再自己瞎写工具类了,SpringBoot内置工具类应有尽有 - OXOXTECH 牛牛技术客栈 中山一日游 - OXOXTECH 牛牛技术客栈 设计模式-责任链模式 - OXOXTECH 牛牛技术客栈 起舞吧,齐舞吧 - OXOXTECH 牛牛技术客栈 设计模式-组合模式 - OXOXTECH 牛牛技术客栈 Go语言Web开发|GoFrame框架入门笔记 - OXOXTECH 牛牛技术客栈 Java打包exe教程 - OXOXTECH 牛牛技术客栈 设计模式-代理模式 - OXOXTECH 牛牛技术客栈 MySQL存储过程的优缺点有哪些? - OXOXTECH 牛牛技术客栈 前端渲染优化有哪些? - OXOXTECH 牛牛技术客栈 HTTP状态码及其含义 - OXOXTECH 牛牛技术客栈 从浏览器地址栏输入url到显示页面的步骤 - OXOXTECH 牛牛技术客栈 TypeScript事件派发管理器 - OXOXTECH 牛牛技术客栈 MQTT保留消息的使用方法 - OXOXTECH 牛牛技术客栈 世界工程-港珠澳大桥游 - OXOXTECH 牛牛技术客栈 Golang逃逸分析 - OXOXTECH 牛牛技术客栈 设计模式-享元模式 - OXOXTECH 牛牛技术客栈 牛牛成长记录 - OXOXTECH 牛牛技术客栈 ffmpeg常用命令 - OXOXTECH 牛牛技术客栈 设计模式-外观模式 - OXOXTECH 牛牛技术客栈 设计模式-装饰器模式 - OXOXTECH 牛牛技术客栈 设计模式-桥接模式 - OXOXTECH 牛牛技术客栈 5周年恋爱纪念日 - OXOXTECH 牛牛技术客栈 2024新年快乐,龙腾四海 - OXOXTECH 牛牛技术客栈 迎接新年:除夕的美好时刻 - OXOXTECH 牛牛技术客栈 设计模式-适配器模式 - OXOXTECH 牛牛技术客栈 设计模式-原型模式 - OXOXTECH 牛牛技术客栈 设计模式-建造者模式 - OXOXTECH 牛牛技术客栈 设计模式-工厂模式 - OXOXTECH 牛牛技术客栈 设计模式-单例模式 - OXOXTECH 牛牛技术客栈 SpringBoot在Linux环境下发送163邮件失败(No appropriate protocol (protocol is disabled or cipher suites are inappropriate)) 海与日落 - OXOXTECH 牛牛技术客栈 Swagger比较常用的注解 - OXOXTECH 牛牛技术客栈 猫🐱牛 - OXOXTECH 牛牛技术客栈 2023年最后一个晚霞 - OXOXTECH 牛牛技术客栈 Linux(Centos)部署Nginx教程 - OXOXTECH 牛牛技术客栈 Linux MySQL下载安装详细教程(CentOS版) - OXOXTECH 牛牛技术客栈 JavaFx打包成exe - OXOXTECH 牛牛技术客栈 Flux脚本语言入门教程 - OXOXTECH 牛牛技术客栈 演唱会出图 - OXOXTECH 牛牛技术客栈 Netty TCP解决粘包拆包 - OXOXTECH 牛牛技术客栈 SpringBoot实现订单超时取消的几种方案 - OXOXTECH 牛牛技术客栈 详解Java并发中的各种锁 - OXOXTECH 牛牛技术客栈 SpringBoot集成支付宝支付 - OXOXTECH 牛牛技术客栈 雪花算法:分布式系统唯一ID生成算法 - OXOXTECH 牛牛技术客栈 Java解决空指针的神器Optional - OXOXTECH 牛牛技术客栈 与兴一起 - OXOXTECH 牛牛技术客栈 Java17新特性详解与安装 - OXOXTECH 牛牛技术客栈 Jdk17安装+环境配置详细教程 - OXOXTECH 牛牛技术客栈 孤注一掷 - OXOXTECH 牛牛技术客栈 解决WinSCP经常断线重连 - OXOXTECH 牛牛技术客栈 内存不足导致Tomcat崩溃问题排查与解决办法 - OXOXTECH 牛牛技术客栈 influxDB初识,一个高效的时序数据库 - OXOXTECH 牛牛技术客栈 SpringBoot 服务接口限流方案 - OXOXTECH 牛牛技术客栈 Docker 安装 Portainer - OXOXTECH 牛牛技术客栈 Linux 安装Docker - OXOXTECH 牛牛技术客栈 物料宣传 - OXOXTECH 牛牛技术客栈 Java使用EMQX实现MQTT通信 - OXOXTECH 牛牛技术客栈 Java实现常见的排序算法 - OXOXTECH 牛牛技术客栈 FreeSwitch Windows安装教程 - OXOXTECH 牛牛技术客栈 MQTT单向SSL数据加密 - OXOXTECH 牛牛技术客栈 随性 - OXOXTECH 牛牛技术客栈 mysql报错Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre的解决方案 Git Push项目报 push to origin/master was rejected 错误解决方案 游行记——珠海金沙滩与金湖公园之行 - OXOXTECH 牛牛技术客栈 Tomcat:解决Tomcat启动警告:"无法将资源添加到Web应用程序缓存中....请考虑增加缓存空间" 的问题 - OXOXTECH 牛牛技术客栈 励骏庞都广场,迷一般的皇宫 - OXOXTECH 牛牛技术客栈 Docker 常用命令集合 - OXOXTECH 牛牛技术客栈 ElasticSearch Windows版-安装教程 - OXOXTECH 牛牛技术客栈 Java去除对象中为null的字段 - OXOXTECH 牛牛技术客栈 我和我的青春 - OXOXTECH 牛牛技术客栈 Java实现螺旋矩阵算法: - OXOXTECH 牛牛技术客栈 FreeSwitch将默认数据库迁移至MySQL 别错过路上的风景,别错过刹那间的深情! Viewer.js:一款强大的图片预览组件
Java直接内存分配和释放的理解 - OXOXTECH 牛牛技术客栈
Mr.Potato · 2023-04-13 · via

前言

直接内存是分配在JVM堆外的,那JVM是怎么对它进行管理的呢?本文主要介绍一下在Java中,直接内存的空间分配和释放的机制。

直接内存和堆内存的比较

在比较两者的性能时,我们分两方面来说

  • 申请空间的耗时:堆内存比较快
  • 读写的耗时:直接内存比较快

直接内存申请空间其实是比较消耗性能的,所以并不适合频繁申请。但直接内存在IO读写上的性能要优于堆内存,所以直接内存特别适合申请以后进行多次读写。

为什么在申请空间时,堆内存会更快?堆内存的申请是直接从已分配的堆空间中取一块出来使用,不经过内存申请系统调用,而直接内存的申请则需要本地方法通过系统调用完成。

而为什么在IO读写时,直接内存比较快?因为直接内存使用的是零拷贝技术。

所以直接内存一般有两个使用场景:

  • 复制很大的文件
  • 频繁的IO操作,例如网络并发场景

直接内存由于是直接分配在堆外的,所以不受JVM堆的大小限制。但还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制,所以还是有可能会抛出OutOfMemoryError异常。

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

直接内存的分配和释放

在Java中,分配直接内存有三种方式:

  • Unsafe.allocateMemory()
  • ByteBuffer.allocateDirect()
  • native方法

Unsafe

Java提供了Unsafe类用来进行直接内存的分配与释放:

public long allocateMemory(long bytes);
public void freeMemory(long address);

DirectByteBuffer类

虽然Java提供了Unsafe类用来操作直接内存的分配和释放,但Unsafe无法直接使用,需要通过反射来获取。Unsafe更像是一个底层设施。

DirectByteBuffer类里面使用了Unsafe,它对Unsafe进行了封装,所以更适合开发者使用。它分配内存和释放内存是通过一下方法来实现的。

构造方法:

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));
    
    // 告诉内存管理器要分配内存
    Bits.reserveMemory(size, cap);

    // 分配内存
    long base = 0;
    try {
        base = UNSAFE.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    UNSAFE.setMemory(base, size, (byte) 0);
    
    // 计算内存地址
    if (pa && (base % ps != 0)) {
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    
    // 创建Cleaner
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

这里有两个概念。一个是Bits类里面的reserveMemory,另一个是Cleaner创建的Deallocator。

reserveMemory方法源码:

static void reserveMemory(long size, int cap) {
	// 初始化maxMemory,如果没有指定-XX:MaxDirectMemorySize,
    // 就使用VM.maxDirectMemory()的值:64M
    if (!MEMORY_LIMIT_SET && VM.initLevel() >= 1) {
        MAX_MEMORY = VM.maxDirectMemory();
        MEMORY_LIMIT_SET = true;
    }

    // 第一次先采取乐观的方式尝试告诉Bits要分配内存
    if (tryReserveMemory(size, cap)) {
        return;
    }

    final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
    boolean interrupted = false;
    try {

        // 尝试释放内存,直到内存空间足够
        boolean refprocActive;
        do {
            try {
                refprocActive = jlra.waitForReferenceProcessing();
            } catch (InterruptedException e) {
                interrupted = true;
                refprocActive = true;
            }
            if (tryReserveMemory(size, cap)) {
                return;
            }
        } while (refprocActive);

        // trigger VM's Reference processing
        System.gc();

        // 按照1ms,2ms,4ms,...,256ms的等待间隔尝试9次分配内存
        long sleepTime = 1;
        int sleeps = 0;
        while (true) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
            if (sleeps >= MAX_SLEEPS) {
                break;
            }
            try {
                if (!jlra.waitForReferenceProcessing()) {
                    Thread.sleep(sleepTime);
                    sleepTime <<= 1;
                    sleeps++;
                }
            } catch (InterruptedException e) {
                interrupted = true;
            }
        }

        // no luck 还是没有足够的空间可以分配
        throw new OutOfMemoryError("Direct buffer memory");

    } finally {
        if (interrupted) {
            // don't swallow interrupts
            Thread.currentThread().interrupt();
        }
    }
}

内存释放是通过Deallocator来实现的。

private static class Deallocator
    implements Runnable
    {
        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) {
                // Paranoia
                return;
            }
            // 使用unsafe释放内存
            UNSAFE.freeMemory(address);
            address = 0;
            // 利用Bits管理内存的释放,就是标记一下该内存已释放
            Bits.unreserveMemory(size, capacity);
        }
    }
    

很简单的一个Runnable,主要通过Cleaner来进行调度。Cleaner的数据结构为一个双向链表,采用“头插法”,每次插入新的结点是插入到“头结点”的。Cleaner继承了PhantomReference,其referent为DirectByteBuffer:

public class Cleaner
    extends PhantomReference<Object>
{

    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

    private static Cleaner first = null;

    private Cleaner
        next = null,
        prev = null;

    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);
        this.thunk = thunk;
    }
    
    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        return add(new Cleaner(ob, thunk));
    }
    
    // other methods...
}

这里用到了JVM的虚引用。JVM有四种引用类型,分别是:强引用,弱引用,软引用,虚引用

PhantomReference的get方法总是返回null,因此无法访问对应的引用对象;其意义在于说明一个对象已经进入finalization阶段,可以被GC回收。

GC过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在GC完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,这里是调用Cleaner的clean方法:

// Cleaner类
public void clean() {
    if (!remove(this))
        return;
    try {
        // 调用Deallocator的run方法,
        // 进而通过Unsafe类释放内存。
        thunk.run();
    } catch (final Throwable x) {
        AccessController.doPrivileged(new PrivilegedAction<>() {
            public Void run() {
                if (System.err != null)
                    new Error("Cleaner terminated abnormally", x)
                    .printStackTrace();
                System.exit(1);
                return null;
            }});
    }
}

总结成一张图:

202304132009226368.webp

native方法

我们知道,Java可以通过native方法来直接调用C/C++的接口。那native方法中分配的内存是否是属于DirectByteBuffer对象呢?掘金上有一篇文章《Java直接内存分配与释放原理》写了一个Demo进行了实验,发现native方法分配的内存并不会产生DirectByteBuffer对象,同样的也不受-XX:MaxDirectMemorySize影响。

所以如果你使用native方法来操作直接内存的话,也需要使用native方法来自己进行直接内存的管理。

总结

通常来说,我们是使用DirectByteBuffer类来操作直接内存的比较多,所以可以了解一下DirectByteBuffer对直接内存的分配和回收的流程,这样如果以后遇到因为直接内存引起的性能瓶颈或者OOM异常,可以进行快速排查。