生产环境Java应用服务内存泄漏分析与解决

news2024/10/6 14:33:10

有个生产环境CRM业务应用服务,情况有些奇怪,监控数据显示内存异常。内存使用率99.%多。通过生产监控看板发现,CRM内存超配或内存泄漏的现象,下面分析一下这个问题过程记录。

1、服务器硬件配置部署情况

生产服务器采用阿里云ECS机器,配置是2HZ、8GB,单个应用服务独占,CRM应用独立部署,即单台服务器仅部署一个java应用服务。

用了6个节点6台机器,每台机器都差不多情况。

监控看板如下:

top命令查看物理占用情况

通过看板情况来看确实存在异常情况,下面进一步分析问题原因。

2、应用启动参数配置

应用启动配置参数如下:

 /usr/bin/java

-javaagent:/home/agent/skywalking-agent.jar

-Dskywalking.agent.service_name=xx-crm

 -XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/tmp/xx-crm.hprof

-Dspring.profiles.active=prod

-server -Xms4884m -Xmx4884m -Xmn3584m

-XX:MetaspaceSize=512m

-XX:MaxMetaspaceSize=512m

-XX:CompressedClassSpaceSize=128m

-jar /home/xxs-crm.jar

堆内:最大最小堆内存4884m约4.8G左右,其中新生代-Xmn3584m 约3.5G左右,

非堆: 元数据区配置 512M,类压缩空间 128M, Code Cache、buffer等代码缓存区240M(没有配置参数,通过监控看板看到的)。

3、内存分布统计

从监控看板的数据来看,我们简单统计一下内存分配数据情况。

通过JVM配置参数和监控看板数据可知:

堆内存:4.8G

非堆内存:(Metaspace)512M+(CompressedClassSpace)128M+(Code Cache、Buffer等)240M约等1GB左右。

堆内存(heap)+非堆内存(nonHeap)=5.8G

8GB物理内存除去操作系统本身占用大概占500M。即除了操作系统本身占用之外,还有7.5G可用内存。

但是 7.5-5.8=1.7GB,起码至少还有1~2GB空闲才合理呀!怎么内存占用率99%多,就意味着有1~2G不知道谁占去了,有点诡异!

4、问题分析

先看一下JVM内存模型,环境是使用JDK8

JVM内存数据分区如下图所示:

堆heap分配结构如下图所示:

堆大家都比较熟悉,也容易理解的,也是java程序接触得最多的一块,不存在什么数据上统计错误,或占用不算之类的。

那说明额外占用也非堆里面,只不过没有统计到非堆里面去,曾经一度怀疑监控prometheus展示的数据有误

其实不是监控统计的问题,那是什么问题呢?

先看一下dump文件数据,这里使用MAT工具(一个开源免费的内存分析工具,个人认为比较好用,推荐大家使用。下载地址:https://www.eclipse.org/mat/downloads.php)。

通过下载内存dump镜像观察到,如下图所示:

有个offHeapStore,这个东西堆外内存,可以初步判断是 ehcahe引起的。

通过ehcahe源码分析,发现ehcache里面也使用了netty的NIO方法内存,ehcache磁盘缓存写数据时会用到DirectByteBuffer。

DirectByteBuffer是使用非堆内存,不受GC影响。

  在网络编程中,为避免频繁的在用户空间与内核空间拷贝数据,通常会直接从内核空间中申请内存,存放数据,在Java中,把内核空间的内存称之为直接内存,nio包中的ByteBuffer的allocateDirect方法,就是申请直接内存。

直接打开JDK源代码 

ByteBuffer
 /**
     * Allocates a new direct byte buffer.
     *
     * <p> The new buffer's position will be zero, its limit will be its
     * capacity, its mark will be undefined, and each of its elements will be
     * initialized to zero.  Whether or not it has a
     * {@link #hasArray backing array} is unspecified.
     *
     * @param  capacity
     *         The new buffer's capacity, in bytes
     *
     * @return  The new byte buffer
     *
     * @throws  IllegalArgumentException
     *          If the <tt>capacity</tt> is a negative integer
     */
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

DirectByteBuffer对象是ByteBuffer的子类,对于直接内存的分配,就是在这个类中实现的。

在java中

  • 直接内存的申请与释放是通过Unsafe类的allocateMemory方法和freeMemory方法
  • 处置从allocateMemory或reallocateMemory获得的本地内存块。 传递给此方法的地址可以为null,在这种情况下,不采取任何措施。

分配给定大小的新本地内存块(以字节为单位)。 存储器的内容未初始化; 它们通常是垃圾。 结果本机指针永远不会为零,并且将针对所有值类型进行对齐。 通过调用freeMemory处理此内存,或使用reallocateMemory调整其大小。

DirectByteBuffer帮我们简化了直接内存的使用,我们不需要直接操作Unsafe类来进行直接内存的申请与释放,那么其是如何实现的呢?

直接内存的申请:

在DirectByteBuffer实例通过构造方法创建的时候,会通过Unsafe类的allocateMemory方法 帮我们申请直接内存资源。

直接内存的释放:

DirectByteBuffer本身是一个Java对象,其是位于堆内存中的,JDK的GC机制可以自动帮我们回收,但是其申请的直接内存,不再GC范围之内,无法自动回收。好在JDK提供了一种机制,可以为堆内存对象注册一个钩子函数(其实就是实现Runnable接口的子类),当堆内存对象被GC回收的时候,会回调run方法,我们可以在这个方法中执行释放DirectByteBuffer引用的直接内存,即在run方法中调用Unsafe 的freeMemory 方法。注册是通过sun.misc.Cleaner类来实现的。


    // Primary constructor
    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)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;



    }

从代码中我们可以看到构造方法中的确是用了unsafe.allocateMemory方法帮我们分配了直接内存,另外,在构造方法的最后,通过 Cleaner.create方法注册了一个钩子函数,用于清除直接内存的引用。

Cleaner.create方法声明如下所示:

public static Cleaner create(Object heapObj, Runnable task)

其中第一个参数是一个堆内存对象,第二个参数是一个Runnable任务,表示这个堆内存对象被回收的时候,需要执行的回调方法。我们可以看到在DirectByteBuffer的最后一行中,传入的这两个参数分别是this,和一个Deallocator(实现了Runnable接口),其中this表示就是当前DirectByteBuffer实例,也就是当前DirectByteBuffer被回收的时候,回调Deallocator的run方法

Deallocator就是用于清除DirectByteBuffer引用的直接内存,代码如下所示:


    private static class Deallocator
        implements 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) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

在DirectByteBuffer实例创建时,分配内存之前调用了Bits.reserveMemory,如果分配失败调用了Bits.unreserveMemory,同时在Deallocator释放完直接内存的时候,也调用了Bits.unreserveMemory方法。

这两个方法,主要是记录jdk已经使用的直接内存的数量,当分配直接内存时,需要进行增加,当释放时,需要减少,源码如下:

static void reserveMemory(long size, int cap) {

        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }

        // optimist!
        if (tryReserveMemory(size, cap)) {
            return;
        }

        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

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

        // a retry loop with exponential back-off delays
        // (this gives VM some time to do it's job)
        boolean interrupted = false;
        try {
            long sleepTime = 1;
            int sleeps = 0;
            while (true) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        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();
            }
        }
    }


//释放内存时,减少引用直接内存的计数
static void unreserveMemory(long size, int cap) {
        long cnt = count.decrementAndGet();
        long reservedMem = reservedMemory.addAndGet(-size);
        long totalCap = totalCapacity.addAndGet(-cap);
        assert cnt >= 0 && reservedMem >= 0 && totalCap >= 0;
    }

通过上面代码的分析,可以认为Bits类是直接内存的分配担保,当有足够的直接内存可以用时,增加直接内存应用计数,否则,调用System.gc,进行垃圾回收,需要注意的是,System.gc只会回收堆内存中的对象,但是 DirectByteBuffer对象被回收时,那么其引用的直接内存也会被回收,试想现在刚好有其他的DirectByteBuffer可以被回收,那么其被回收的直接内存就可以用于本次DirectByteBuffer直接的内存的分配。

回到本问题上来,当有文件需要暂存到ehcache的磁盘缓存时,使用到了NIO中的FileChannel来读取文件,默认ehcache使用了堆内的HeapByteBuffer来给FileChannel作为读取文件的缓冲,FileChannel读取文件使用的IOUtil的read方法,针对HeapByteBuffer底层还用到一个临时的DirectByteBuffer来和操作系统进行直接的交互。

ehcache使用HeapByteBuffer作为读文件缓冲:

IOUtil对于HeapByteBuffer实际会用到一个临时的DirectByteBuffer来和操作系统进行交互。

DirectByteBuffer泄漏根因分析

默认情况下这个临时的DirectByteBuffer会被缓存在一个ThreadLocal的bufferCache里不会释放,每一个bufferCache有一个DirectByteBuffer的数组,每次当前线程需要使用到临时DirectByteBuffer时会取出自己bufferCache里的DirectByteBuffer数据,选取一个不小于所需size的,如果bufferCache为空或者没有符合的,就会调用Bits重新创建一个,使用完之后再缓存到bufferCache里。

这里的问题在于 :这个bufferCache是ThreadLocal的,意味着极端情况下有N个调用线程就会有N组 bufferCache,就会有N组DirectByteBuffer被缓存起来不被释放,而且不同于在IO时直接使用DirectByteBuffer,这N组DirectByteBuffer连GC时都不会回收。我们的文件服务在读写ehcache的磁盘缓存时直接使用的tomcat的worker线程池,

这个worker线程池的配置上限是2000,我们的配置中心上的配置的参数:

所以,这种隐藏的问题影响所有使用到HeapByteBuffer的地方而且很隐秘,由于在CRM服务中大量使用了ehcache存在较大的sizeIO且调用线程比较多的场景下容易暴露出来。

获取临时DirectByteBuffer的逻辑:

bufferCache从ByteBuffer数组里选取合适的ByteBuffer:

将ByteBuffer回种到bufferCache:

NIO中的FileChannelSocketChannelChannel默认在通过IOUtil进行IO读写操作时,除了会使用HeapByteBuffer作为和应用程序的对接缓冲,但在底层还会使用一个临时的DirectByteBuffer来和系统进行真正的IO交互,为提高性能,当使用完后这个临时的DirectByteBuffer会被存放到ThreadLocal的缓存中不会释放,当直接使用HeapByteBuffer的线程数较多或者IO操作的size较大时,会导致这些临时的DirectByteBuffer占用大量堆外直接内存造成泄漏。

那么除了减少直接调用ehcache读写的线程数有没有其他办法能解决这个问题?并发比较高的场景下意味着减少业务线程数并不是一个好办法。

在Java1.8_102版本开始,官方提供一个参数jdk.nio.maxCachedBufferSize,这个参数用于限制可以被缓存的DirectByteBuffer的大小,对于超过这个限制的DirectByteBuffer不会被缓存到ThreadLocal的bufferCache中,这样就能被GC正常回收掉。唯一的缺点是读写的性能会稍差一些,毕竟创建一个新的DirectByteBuffer的代价也不小,当然通过测试验证对比分析,性能也没有数量级的差别。

增加参数:

-XX:MaxDirectMemorySize=1600m
-Djdk.nio.maxCachedBufferSize=500000    ---注意不能带单位

就是调整了-Djdk.nio.maxCachedBufferSize=500000(注意这里是字节数,不能用mkg等单位)。

增加调整参数之后,运行一段时间,持续观察整体DirectByteBuffer稳定控制在1.5G左右,性能也几乎没有衰减。

一切恢复正常,再看监控看板没有看到占满内存告警。

5、解决办法

调整应用启动参数配置,业务系统调整后的启动命令参数如下:

 java

-javaagent:/home/agent/skywalking-agent.jar

-Dskywalking.agent.service_name=xx-crm

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/tmp/xx-crm.hprof

-Dspring.profiles.active=prod

-server -Xms4608m -Xmx4608m -Xmn3072m

-XX:MetaspaceSize=300m

-XX:MaxMetaspaceSize=512m

-XX:CompressedClassSpaceSize=64m

-XX:MaxDirectMemorySize=1600m

-Djdk.nio.maxCachedBufferSize=500000

-jar /home/xx-crm.jar

6、总结

碰到这类非堆内存问题有两种解决办法:

1、在业务允许条件下减少IO线程数。

2、调整配置应用启动参数,-Djdk.nio.maxCachedBufferSize=xxx 记住 -XX:MaxDirectMemorySize 参数也要配置上。

如果不配置 -XX:MaxDirectMemorySize 这个参数(最大直接内存),JVM就默认取-Xmx的值当作它的最大值(可能就会像我一样遇到超配的情况)。

3、遇到问题多读一下JDK源码,并学会内存分析工具。

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

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

相关文章

【命令参数】MSBuild - 环境配置及常用命令参数

目录 环境配置 基本语法 参数指令 对各类程序的命令参数的掌握是软件工程师必修课之一&#xff0c;它是通往自动化、高效化开发测试的必经之路。对于MSBuild&#xff0c;我们可以借助它以一种轻量级的形式去完成对于项目又或解决方案的生成&#xff0c;而避开使用繁大的IDE进…

Linux系统之部署Etherpad文档编辑器

Linux系统之部署Etherpad文档编辑器 一、Etherpad介绍1.Etherpad简介2.Etherpad特点 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本3.3 检查系统是否安装Node.js 四、部署Node.js 环境4.1 下载Node.js安装包…

【吴恩达老师《机器学习》】课后习题3之【逻辑回归解决多分类】与【神经网络】笔记(代码注释详细)

本次习题所用到的数据&#xff0c;#数据集&#xff1a;ex3data1.mat&#xff0c;参数&#xff1a;ex3weights.mat。在文章开头&#xff0c;下载即可&#xff01; 逻辑回归解决多分类问题 二分类VS多分类 在机器学习中&#xff0c;分类是一种监督学习任务&#xff0c;其中我们…

从机缘到成就

机缘 在这1825天的创作之旅中&#xff0c;我收获了许多宝贵的机遇和经验。起初&#xff0c;我只是一个对技术有着浓厚兴趣的普通人&#xff0c;遇到了一个在eclipse导入工程后出现中文乱码的问题。而我决定将这个问题记录下来&#xff0c;并分享给其他可能遇到相同困扰的人们。…

数据库系统概述——第三章 关系数据库标准语言SQL(知识点复习+练习题)

&#x1f31f;博主&#xff1a;命运之光 &#x1f984;专栏&#xff1a;离散数学考前复习&#xff08;知识点题&#xff09; &#x1f353;专栏&#xff1a;概率论期末速成&#xff08;一套卷&#xff09; &#x1f433;专栏&#xff1a;数字电路考前复习 &#x1f99a;专栏&am…

Linux基础内容(23)—— 信号补充与多线程交接知识

Linux基础内容&#xff08;22&#xff09;—— 信号_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/130835485 目录 1.可重入函数 1.情况假设 2.volatile 3.SIGCHLD信号 1.SIGCHLD介绍 2.信号的确认 3.wait的处理 1.可重入函数 1.情况假设…

插件 - 通过SPI方式实现插件管理

文章目录 SPI概念基本原理使用步骤优点缺点Code真实使用场景案例JDBC(Java Database Connectivity)Servlet API日志框架SPI概念 SPI(Service Provider Interface)是Java提供的一种服务扩展机制,它允许应用程序在运行时动态加载和发现提供者(Providers),并与它们进行交…

Proteus仿真之UART通信(点亮LED灯)

1.UART通信简介&#xff1a;通用异步收发传输器UART(Universal Asynchronous Receiver/Transmitter)是负责处理数据总线和串口之间的串/并通信的设备。UART通信规定了数据帧的格式&#xff1a;起始位、数据位、校验位、停止位等。UART异步通信只需要通信双方设置好数据帧的格式…

房屋装修选择自装,如何寻找水电工人,比价并施工(水电阶段)

环境&#xff1a; 地点&#xff1a;杭州 装修类型&#xff1a;自装 面积&#xff1a;建面135平方 进度&#xff1a;水电阶段 问题描述&#xff1a; 房屋装修选择自装&#xff0c;如何寻找水电工人&#xff0c;比价并施工 解决方案&#xff1a; 一、了解水电相关知识 水…

Python3+RIDE+RobotFramework自动化测试框架搭建

Python2.7已于2020年1月1日开始停用&#xff0c;之前RF做自动化都是基于Python2的版本。 没办法&#xff0c;跟随时代的脚步&#xff0c;我们也不得不升级以应用新的控件与功能。 升级麻烦&#xff0c;直接全新安装。 一、Python安装 最新版Python下载地址&#xff1a;http…

Qt连接Access数据库

Qt自带有QODBC驱动&#xff08;封装了ODBC驱动接口&#xff09;&#xff0c;通过windows平台上提供的ODBC驱动访问支持ODBC的数据库&#xff0c;如Ms Access、SQL Server等 (Windows XP 自带有Access和SQL Server的ODBC Driver)。我们就用QODBC对Access数据库进行访问。 Acces…

别再瞎搞了,耳朵都竖起来听我说,新手小白开发应该如何选择最合适你的JetBrains IDE版本类型和版本号! 今天一次性给你说清楚!

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&#x1…

windows环境下搭建redis集群

下面记录一下windows10环境下搭建redis3主3从集群&#xff0c;将过程分享出来&#xff0c;仅供学习研究使用。 1、redis集群 Redis集群关键点就是去掉中心化(与哨兵模式的区别)&#xff0c;当主机宕机&#xff0c;从节点回自动升级为主节点&#xff0c;具体请参考官网或相关大…

机器学习——KNN算法(手动代码,含泪)

徒手实现代码的过程&#xff0c;真是含泪和心酸&#xff0c;浪费了生命中的三天&#xff0c;以及工作中的划水一小时 终于滤清思路后&#xff0c;自己实现了KNN 都说KNN是最基础&#xff0c;最简单的分类器 放屁&#xff01;骗纸&#xff01;&#xff01;&#xff01;它的想法是…

第八章——向量代数与空间解析几何

目录 一、运算公式 二、平面的法线向量 注&#xff1a;加粗体为向量 一、运算公式 1.若a//b&#xff0c;那么aλb 若a⊥b&#xff0c;那么a*b0 2.若A(x1,y1,z1)&#xff0c;B(x2,y2,z2) 中点坐标&#xff1a;AB中点M(x1x2/2,y1y2/2,z1z2/2) 两点间的距离和模的计算&#x…

第3章 信息系统治理

文章目录 3.1.1 IT治理基础1. IT治理的驱动因素2. IT治理的目标价值3. IT治理的管理层次 3.1.2 IT治理体系1. IT治理关键决策2. IT治理体系框架3. IT治理核心内容4. IT治理机制经验&#xff08;建立IT治理机制的原则&#xff1a;简单、透明、适合&#xff09; 3.1.3 IT治理任务…

工作流引擎Flowable

这里写目录标题 1.Flowable基础1.1 入门学习 2.流程图设计器2.1 FlowableUI2.1.1 绘制流程图 1.Flowable基础 官方手册 1.1 入门学习 一、依赖 <dependencies><dependency><groupId>org.flowable</groupId><artifactId>flowable-engine</…

jenkins——Git版本管理

这里写目录标题 一、Jenkins Git 版本管理1、Git 的集成2、在执行job的机器上安装好Git3、无法连接仓库&#xff0c;问题解决解决方法1&#xff1a;&#xff08;不推荐&#xff09;1、把仓库设置成公开的&#xff0c;然后重新添加仓库地址 解决方法2&#xff1a;通过凭证的方式…

打破Spring的垄断,云原生Java框架Micronaut

文章目录 什么是Micronaut&#xff1f;Micronaut的功能特性相较于Spring的优势 Micronaut框架的使用安装Micronaut cli创建Micronaut项目 Micronaut应用的部署micronaut反应式编程 MCNU云原生&#xff0c;文章首发地&#xff0c;欢迎微信搜索关注&#xff0c;更多干货&#xff…

基于springboot的文件的上传到本地和云上传(阿里云)

1.文件上传 1.介绍 文件上传&#xff0c;是指将本地图片、视频、音频等文件上传到服务器&#xff0c;供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛&#xff0c;我们经常发微博、发微信朋友圈都用到了文件上传功能。 2.前端的文件上传–form表单 将静态的页面…