关于内存泄露:使用mtrace追踪JVM堆外内存泄露

原创:扣钉日记(微信公众号ID:codelogs),欢送分享,非公众号转载保留此申明。简介在上篇文章中,介绍了应用tcmalloc或jemalloc定位native内存泄露的办法,但应用这个办法相当于更换了原生内存分配器,以至于应用时会有一些顾虑。 通过一些摸索,发现glibc自带的ptmalloc2分配器,也提供有追踪内存泄露的机制,即mtrace,这使得产生内存泄露时,可间接定位,而不须要额定装置及重启操作。 mtrace追踪内存泄露glibc中提供了mtrace这个函数来开启追踪内存调配的性能,开启后每次应用程序调用malloc或free函数时,会将内存调配开释操作记录在MALLOC_TRACE环境变量所指的文件外面,如下: $ pid=`pgrep java`# 配置gdb不调试信号,防止JVM收到信号后被gdb暂停$ cat <<"EOF" > ~/.gdbinithandle all nostop noprint passhandle SIGINT stop print nopassEOF# 设置MALLOC_TRACE环境变量,将内存调配操作记录在malloc_trace.log里$ gdb -q -batch -ex 'call setenv("MALLOC_TRACE", "./malloc_trace.log", 1)' -p $pid# 调用mtrace开启内存调配追踪$ gdb -q -batch -ex 'call mtrace()' -p $pid# 一段时间后,调用muntrace敞开追踪$ gdb -q -batch -ex 'call muntrace()' -p $pid而后查看malloc_trace.log,内容如下: 能够发现,在开启mtrace后,glibc将所有malloc、free操作都记录了下来,通过从日志中找出哪些地方执行了malloc后没有free,即是内存泄露点。 于是glibc又提供了一个mtrace命令,其作用就是找出下面说的执行了malloc后没有free的记录,如下: $ mtrace malloc_trace.log | less -nMemory not freed:----------------- Address Size Caller0x00007efe08008cc0 0x18 at 0x7efe726e8e5d0x00007efe08008ea0 0x160 at 0x7efe726e8e5d0x00007efe6cabca40 0x58 at 0x7efe715dc4320x00007efe6caa9ad0 0x1bf8 at 0x7efe715e4b880x00007efe6caab6d0 0x1bf8 at 0x7efe715e4b880x00007efe6ca679c0 0x8000 at 0x7efe715e4947# 按Caller分组统计一下,看看各Caller各泄露的次数及内存量$ mtrace malloc_trace.log | sed '1,/Caller/d'|awk '{s[$NF]+=strtonum($2);n[$NF]++;}END{for(k in s){print k,n[k],s[k]}}'|column -t0x7efe715e4b88 1010 72316000x7efe715dc432 1010 888800x7efe715e4947 997 326696960x7efe726e8e5d 532 3098000x7efe715eb2f4 1 720x7efe715eb491 1 38能够发现,0x7efe715e4b88这个调用点,泄露了1010次,那怎么晓得这个调用点在哪个函数里呢? ...

September 23, 2023 · 2 min · jiezi

关于内存泄漏:JavaScript-内存泄漏

生存可能不像你设想的那么好,然而也不会像你设想的那么蹩脚。人的软弱和刚强都超乎了本人的设想,有时候可能软弱的一句话就泪流满面,有时候你发现自己咬着牙,曾经走过了很长的路如何防止 JavaScript 中的内存透露像 C 语言这样的底层语言个别都有底层的内存治理接口,比方 malloc()和free()。相同,JavaScript 是在创立变量(对象,字符串等)时主动进行了分配内存,并且在不应用它们时“主动”开释。开释的过程称为垃圾回收。这个“主动”是凌乱的本源,并让 JavaScript(和其余高级语言)开发者谬误的感觉他们能够不关怀内存治理。 什么是内存透露?简而言之,内存透露是 JavaScript 引擎无奈回收的已分配内存。当您在应用程序中创建对象和变量时,JavaScript 引擎会分配内存,当您不再须要这些对象时,它会十分聪慧地革除内存。内存透露是因为逻辑缺点引起的,它们会导致应用程序性能不佳。 在深入探讨不同类型的内存透露之前,让咱们先理解一下JavaScript 中的内存治理和垃圾回收。 内存生命周期在任何编程语言中,内存生命周期都蕴含三个步骤: 内存调配:操作系统在执行过程中依据须要为程序分配内存应用内存:您的程序应用以前调配的内存,您的程序能够对内存执行read和操作write开释内存:工作实现后,调配的内存将被开释并变为闲暇。在 JavaScript 等高级语言中,内存开释由垃圾收集器解决如果您理解 JavaScript 中的内存调配和开释是如何产生的,那么解决应用程序中的内存透露就非常容易。内存调配JavaScript 有两种用于内存调配的存储选项。一个是栈,一个是堆。所有根本类型,如number、Boolean和undefined都将存储在堆栈中。堆是对象、数组和函数等援用类型存储的中央。 动态调配和动态分配编译代码时,编译器能够查看原始数据类型,并提前计算它们所需内存。而后将所需的数量调配给调用堆栈中的程序。这些变量调配的空间称为堆栈空间(stack space),因为函数被调用,它们的内存被增加到现有内存(存储器)的顶部。它们终止时,它们将以LIFO(后进先出)程序被移除。 援用类型变量须要多少内存无奈在编译时确定,须要在运行时依据理论应用状况分配内存,此内存是从堆空间(heap space) 调配的。 Static allocationDynamic allocation编译时内存大小确定编译时内存大小不确定编译阶段执行运行时执行调配给栈(stack space)调配给堆(heap stack)FILO没有特定的程序栈Stack 遵循 LIFO 办法分配内存。所有根本类型,如number、Boolean和undefined都能够存储在栈中: 堆对象、数组和函数等援用类型存储在堆中。援用类型的大小无奈在编译时确定,因而内存是依据对象的应用状况调配的。对象的援用存储在栈中,理论对象存储在堆中: 在上图中,otherStudent变量是通过复制student变量创立的。在这种状况下,otherStudent是在堆栈上创立的,但它指向堆上的student援用。 咱们曾经看到,内存周期中内存调配的次要挑战是何时开释调配的内存并使其可用于其余资源。在这种状况下,垃圾回收就派上用场了。 垃圾回收器应用程序内存透露的次要起因是不须要的援用造成的。而垃圾回收器的作用是找到程序不再应用的内存并将其开释回操作系统以供进一步调配。 要晓得什么是不须要的援用,首先,咱们须要理解垃圾回收器是如何辨认一块内存是不可用的。垃圾回收器次要应用两种算法来查找不须要的援用和无法访问的代码,那就是援用计数和标记革除。 援用计数援用计数算法查找没有援用的对象。如果不存在指向对象的援用,则能够开释该对象。让咱们通过上面的示例更好地了解这一点。共有三个变量,student, otherStudent,它是 student 的正本,以及sports,它从student对象中获取sports数组: let student = { name: 'Joe', age: 15, sports: ['soccer', 'chess']}let otherStudent = student;const sports = student.sports;student = null;otherStudent = null; 在下面的代码片段中,咱们将student和otherStudent变量调配给空值,通知咱们这些对象没有对它的援用。在堆中为它们调配的内存(红色)能够轻松开释,因为它是零援用。 另一方面,咱们在堆中还有另一块内存,它不能被开释,因为它有对象sports援用。 当两个对象都援用本人时,援用计数算法就有问题了。简略来说,如果存在循环援用,则该算法无奈辨认闲暇对象。 在上面的示例中,person和employee变量互相援用: ...

June 30, 2023 · 3 min · jiezi

关于内存泄漏:内存泄露腾讯工程师2个压箱底的方法和工具

导读|蒙受内存泄露往往是令开发者头疼的问题,传统剖析工具 gdb、Valgrind在解决内存泄露问题上效率较低。本文特地邀请到了腾讯后盾开发工程师邢孟棒以 TDSQL理论生产中mysql-proxy内存泄露问题作为剖析对象,分享其基于动静追踪技术的通用内存泄露(增长)分析方法。其中将具体介绍内存分配器行为剖析、缺页异样事件剖析,涵盖应用程序内存调配的常见过程。浏览完本文后,开发者仅需关注多数可能导致内存泄露的代码门路,就能无效晋升定位内存泄露(增长)问题的效率。 背景 某个 TDSQL 私有化环境中, 中间件 mysql-proxy 进行大量申请转发时,内存占用量持续增长导致 OOM 景象,最终影响了用户业务的失常应用 。自己剖析该问题的过程中发现一个较为广泛的业务痛点:传统剖析工具(gdb、Valgrind 等)效率绝对较低,在私有化场景中尤其突出。针对这一痛点,我将提供绝对通用的内存泄露(增长)分析方法,帮助各位开发者更高效地定位产生泄露的代码门路,以期最大化缩小人力投入老本并升高对用户业务体验的影响。 根底概念 在开展讲述内存泄露(增长)分析方法之前,咱们先理解一些相干的根底概念。 内存泄露包含内核内存泄露、应用程序内存泄露两大类。内核内存泄露能够通过 kmemleak 进行检测,本文次要关注应用程序的内存泄露。应用程序的内存泄露又能够细分为:堆内存(Heap)泄露、内存映射区(Memory Mappings)泄露。咱们平时提及的内存泄露,次要是指物理内存的泄露(继续调配、映射理论的物理内存,且始终未开释),危害较大,须要立刻修复。 另外,虚拟内存的泄露(继续调配虚拟内存,但未调配、映射理论的物理内存)容易被忽视,尽管危害绝对较小,但也需额定关注(过程的内存映射区总数量有下限,默认 1w)。 通常,应用程序内存调配波及的步骤大抵如下图所示: 第一,应用程序通过内存分配器(例如 libc)提供的 malloc 及其变体函数申请内存,free 函数开释相应内存。第二,内存分配器(例如 libc)外部通过零碎调用 brk 扩大堆内存(小块内存调配)。第三,内存分配器(例如 libc)外部通过零碎调用 mmap 分配内存映射区域(大块内存调配,默认不小于 128 KB)第四,二或三已申请的虚拟内存在首次写入时触发缺页异样,OS 调配理论物理页面,并将虚拟内存与其相关联,记录至页表。 其中,步骤一至三均为虚拟内存,步骤四调配理论物理内存并创立相应页表。 传统剖析工具 gdb、Valgrind 在定位 mysql-proxy 内存泄露(增长)问题的过程中,开发人员尝试应用了 Valgrind Memcheck、gdb 进行帮助剖析。最终前者实际效果不太现实;我通过后者剖析出泄露起因,但整个过程消耗了较多工夫。 gdb 是罕用的程序调试工具,益处不必赘述。但对于内存泄露或增长问题,gdb 毛病也较为显著,大抵如下:烦扰程序失常运行,不适宜生产环境间接定位比拟艰难,且要求对源码有肯定理解。 Valgrind Memcheck 是一款知名度较高的内存泄露剖析工具,十分弱小,开发调试过程中可能疾速发现场景的内存泄露问题。不过开发者在应用之前,倡议对以下状况有所理解: 第一,须要重启程序,且作为 Valgrind 子过程运行。不适宜剖析正在产生内存增长的过程。 第二,代替默认的 malloc/free 等调配函数,指标过程运行速度减慢 20~30 倍。 第三,不能很好的反对 tcmalloc、jemalloc 内存分配器。(mysql-proxy 采纳了 jemalloc 内存分配器) ...

December 23, 2022 · 2 min · jiezi

关于内存泄漏:JavaScript内存泄漏

在传统的网页开发时无需过多思考内存治理,通常也不会产生重大的结果。因为当用户点击链接关上新页面或者刷新页面,页面内的信息就会从内存中清理掉。随着SPA(Single Page Application)利用的增多,迫使咱们在编码时须要更多的关注内存。因为如果利用应用的内存逐步增多会间接影响到网页的性能,甚至导致浏览器标签页解体。这篇文章,咱们将钻研JavaScript编码导致内存透露的场景,提供一些内存治理的倡议。什么是内存透露?咱们晓得浏览器会把object保留在堆内存中,它们通过索引链能够被拜访到。GC(Garbage Collector) 是一个JavaScript引擎的后盾过程,它能够甄别哪些对象是曾经处于无用的状态,移除它们,开释占用的内存。 本该被GC回收的变量,如果被其余对象索引,而且能够通过root拜访到,这就意味着内存中存在了冗余的内存占用,会导致利用的性能降级,这时也就产生了内存透露。 怎么发现内存透露? 内存透露个别不易觉察和定位,借助浏览器的内建工具能够帮忙咱们剖析是否存在内存透露,和导致呈现的起因。 开发者工具关上开发者工具-Performance选项卡,能够剖析以后页面的可视化数据。Chrome 和 Firefox 都有杰出的内存剖析工具,通过剖析快照为开发者提供内存的分配情况。 JS导致内存透露的常见情景未被留神的全局变量全局变量能够被root拜访,不会被GC回收。一些非严格模式下的局部变量可能会变成全局变量,导致内存透露。 给没有申明的变量赋值this 指向全局对象function createGlobalVariables() { leaking1 = 'I leak into the global scope'; // assigning value to the undeclared variable this.leaking2 = 'I also leak into the global scope'; // 'this' points to the global object};createGlobalVariables();window.leaking1; // 'I leak into the global scope'window.leaking2; // 'I also leak into the global scope'如何防止?应用严格模式。 闭包闭包函数执行实现后,作用域中的变量不会被回收,可能会导致内存透露: function outer() { const potentiallyHugeArray = []; return function inner() { potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable console.log('Hello'); };};const sayHello = outer(); // contains definition of the function innerfunction repeat(fn, num) { for (let i = 0; i < num; i++){ fn(); }}repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray // now imagine repeat(sayHello, 100000)定时器应用setTimeout 或者 setInterval: ...

April 25, 2022 · 2 min · jiezi

关于内存泄漏:难受nginx-worker进程内存持续飘升

背景前两篇文章讲了云主机上lua openresty我的项目容器化的历程,在测试环境通过一段时间的验证,所有都比较顺利,就在线上开始灰度。 然而,好景不长。灰度没多久,应用top pod查看时,发现内存满了,最开始狐疑k8s的resources limit memory(2024Mi)调配小了,放大后(4096Mi),重启pod,没多久又满了。 紧接着,狐疑是放量较大,负载高了引起的,扩充hpa,再重启,好家伙,不到两炷香工夫,又满了。 到此,就把眼光投向了pod外部的程序了,估摸着又是哪里呈现内存透露了。 坑在哪里每过一段时间,内存忽上忽下,并且nginx worker(子过程)id号一直减少,是谁杀的过程?云主机没有呈现这样的问题,难道是k8s引起的?pod的resources limit memory减少和hpa减少都没有解决问题。nginx -s reload能够开释内存,然而没多久后又满了。解决办法谁杀的过程?命令:ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDroot 1 0.0 0.0 252672 5844 ? Ss Jun11 0:00 nginx: master process /data/openresty/bin/openresty -g daemon off;nobody 865 10.1 0.3 864328 590744 ? S 14:56 7:14 nginx: worker processnobody 866 13.0 0.3 860164 586748 ? S 15:13 7:02 nginx: worker processnobody 931 15.6 0.2 759944 486408 ? R 15:31 5:37 nginx: worker processnobody 938 13.3 0.1 507784 234384 ? R 15:49 2:23 nginx: worker process发现woker过程号曾经靠近1000了,那么必定一直的被kill,而后再调起,那到底是谁干的呢? 通过 dmesg命令: ...

June 15, 2021 · 3 min · jiezi

关于内存泄漏:年轻人你以为内存泄漏的时候其实问题可能只是在线程数上

John Miiler 是ebay团队的高级后端工程师,负责各种我的项目,包含结账和领取零碎。作为公司解脱繁多业务的致力的一部分,他的团队正试图将业务逻辑一块一块地提取到独自的微服务中。他分享了他的团队如何解决在提取图像处理微服务时遇到的内存应用问题。 最近提取的microservice是一种图像处理服务,它对图像进行大小调整、裁剪、从新编码和执行其余解决操作。这个服务是一个在Docker容器中应用springboot构建的Java应用程序,并部署到AWS托管的Kubernetes集群中。在实现该服务时,咱们偶尔发现了一个微小的问题:该服务存在内存应用问题。本文将探讨咱们辨认和解决这些问题的办法。我将从对个别记忆问题的简要介绍开始,而后深入研究解决这个问题的过程。 我认为是内存泄露。然而在应用内存分析器(MAT)时,当我比拟一个快照和另一个快照的内存应用状况时,我的“惊喜”时刻到来了,我意识到问题在于springboot产生的线程数 内存问题概述有许多类型的谬误间接或间接地影响应用程序。本文次要探讨其中的两个问题: OOM (内存不足)谬误和内存透露。考察这类谬误可能是一项艰巨的工作,咱们将具体介绍在咱们开发的服务中修复此类谬误所采取的步骤。 1. 理解OOM谬误并确定其起因OOM谬误代表第一类内存问题。它能够归结为一个试图在堆上分配内存的应用程序。然而,因为各种起因,操作系统或虚拟机(对于JVM应用程序)无奈满足该申请,因而,应用程序的过程会立刻进行。 使辨认和修复变得十分艰难的是,它能够在任何时候从代码中的任何地位产生。因而,仅仅查看一些日志来确定触发它的代码行通常是不够的。一些最常见的起因是: 应用程序须要比操作系统提供更多的内存。有时这能够通过简略地增加更多的RAM来解决。然而,在一台机器上能够增加多少RAM是有限度的,所以应该尽可能防止内存的适度应用。Java应用程序应用默认的JVM内存限度,这通常相当激进。随着应用程序规模的增长和更多个性的增加,它最终会超过这个限度,并将被JVM杀死。内存透露对于长时间运行的过程来说,最终将导致没有足够的资源。有各种各样的开源和开源工具,用于查看过程的内存应用状况以及它是如何演变的。咱们将在前面的局部探讨这些工具。 2. 理解内存透露首先让咱们理解什么是内存透露。内存透露是一种资源透露类型,当程序开释抛弃的内存时产生故障,导致性能受损或失败。当一个对象存储在内存中,但运行的代码无法访问时,也可能产生这种状况。 这听起来很形象,但在现实生活中,内存透露到底是什么样子呢?让咱们看一个用垃圾回收(GC)语言编写的应用程序内存透露的典型示例。 该图显示了旧的Gen内存(老年代对象)的内存模式。绿线显示调配的内存,紫色线显示GC对Old Gen memory执行扫描阶段后的理论内存使用量,垂直红线显示GC步骤前后内存使用量的差别。 正如您在本例中所看到的,每个垃圾收集步骤都会稍微缩小内存使用量,但总体而言,调配的空间会随着工夫的推移而增长。此模式示意并非所有调配的内存都能够开释。 3. 内存透露的次要起因内存透露有多种起因。咱们将在这里探讨最常见的。第一个也是最容易被忽视的起因是 动态变量的滥用 。在Java应用程序中,只有所有者类加载到Java虚拟机(JVM)中,动态字段就存在于内存中。如果类自身是动态的,那么将在整个程序执行过程中加载该类,因而类和动态字段都不会被垃圾回收。 这个问题的理论解决办法出乎意料地简略。咱们抉择将默认线程池从200个线程笼罩到16个线程。 未敞开的流和连贯是内存透露的另一个起因。一般来说,操作系统只容许无限数量的关上的文件流,因而,如果应用程序遗记敞开这些文件流,在一段时间后,最终将无奈关上新文件。 同样,容许的凋谢连贯的数量也受到限制。如果一个人连贯到一个数据库但没有敞开它,在关上肯定数量的这样的连贯之后,它将达到全局限度。在此之后,应用程序将无奈再与数据库通信,因为它无奈关上新的连贯。 最初,内存透露的最初一个次要起因是未开释的本机对象。如果本机库自身有破绽,那么应用本机库 JNI 的Java应用程序很容易遇到内存透露。这些类型的透露通常是最难调试的,因为大多数时候,您不肯定领有本机库的代码,并且通常将其用作黑盒。 对于本机库内存透露的另一个方面是,JVM垃圾收集器甚至不晓得本机库调配的堆内存。因而,人们只能应用其余工具来解决此类透露问题。 好吧,实践够了。让咱们看一个实在的场景: 4. 案例钻研-修复图像处理服务中的OOM问题现状 如简介局部所述,咱们始终致力于图像处理服务。以下是开发初始阶段内存应用模式的外观: 在这张图中,Y轴上的数字示意内存的GiBs。红线示意JVM可用于堆内存(1GiB)的相对最大值。深灰色线示意一段时间内的均匀理论堆使用量,灰色虚线示意随时间推移的最小和最大理论堆使用量。结尾和结尾处的峰值示意应用程序被重新部署的工夫,因而在本例中能够疏忽它们。 该图显示了一个显著的趋势,因为它从大概须要的300MB堆开始,而后在短短几天内增长到超过800MiB,而在Docker容器中运行的应用程序将因为OOM而被杀死。 为了更好地阐明这种状况,让咱们也看看在同一时间段内应用程序的其余指标。 看看这个图,内存透露的惟一迹象是堆使用率和GC旧gen大小随着工夫的推移而增长。当堆空间使用量达到1GiB时,运行Docker容器的Kubernetes pod就要被杀死了。每一个其余指标看起来都很稳固:线程数始终放弃在略低于40的程度,加载的类的数量也很稳固,非堆的应用也很稳固。 这些图表中惟一短少的变量是垃圾收集工夫。它与堆上调配的内存成比例减少。这意味着响应工夫越来越慢,利用程序运行的工夫越长。 5. 故障排除咱们试图解决这个问题的第一步是确保所有的流和连贯都敞开了。有些角落的案子咱们一开始没有波及。然而,所有都没有扭转。咱们察看到的行为和以前齐全一样。这意味着咱们必须更深刻地开掘。 下一步是查看本机内存的应用状况,并确保最终开释所有调配的内存。咱们用来为服务做重载的 OpenCV 库不是java库,而是本地C++库。它提供了一个能够在应用程序中应用的Java本机接口。 因为咱们晓得OpenCV有可能透露Java本机内存,所以咱们确保所有OpenCV Mat对象都被开释,并在返回响应之前显式地调用GC。依然没有明确的透露指示器,内存应用模式也没有任何变动。 到目前为止还没有明确的批示,是时候用专用工具进一步剖析内存应用状况了。首先,咱们钻研了内存分析器工具中的内存转储。 第一个转储是在应用程序启动后生成的,只有几个申请。第二个转储是在应用程序达到1GiB堆使用率之前生成的。咱们剖析了在这两种状况下调配的内容和可能引起问题的内容。乍一看没有什么不寻常的事。 而后咱们决定比拟堆上最须要的内存。令咱们诧异的是,堆上存储了相当多的申请和响应对象。这是“bingo”时刻。 深入研究这个内存转储,咱们发现堆上存储了44个响应对象,比初始转储中的响应对象要高得多。这44个响应对象实际上都存储了本人的 launchDurlClassLoader ,因为它位于一个独自的线程中。每个对象的保留内存大小都超过 3MiB 。 咱们容许应用程序为咱们的用例应用很多的线程。默认状况下,springboot应用程序应用大小为200的线程池来解决web申请。这对于咱们的服务来说太大了,因为每个申请/响应都须要几MB的内存来保留原始/调整大小的图像。因为线程只是按需创立的,所以应用程序开始时的堆使用量很小,但随着每个新申请的减少,使用量越来越高。 这个问题的理论解决办法出乎意料地简略。咱们抉择将默认线程池从200个线程缩小到16个线程。这就彻底解决了咱们的内存问题。当初堆终于稳固了,因而GC也更快了。 6. 辨认内存问题的工具在考察和排除此问题的过程中,咱们应用了几个被证实是必不可少的工具: Datadog咱们手头上的第一个工具是针对JVM度量的DataDog APM仪表板,它非常容易应用,容许咱们取得下面的图形和仪表板。 ...

December 10, 2020 · 1 min · jiezi