关于内存泄漏: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

Android-Native-内存泄漏系统化解决方案

导读:C++内存泄漏问题的分析、定位一直是Android平台上困扰开发人员的难题。因为地图渲染、导航等核心功能对性能要求很高,高德地图APP中存在大量的C++代码。解决这个问题对于产品质量尤为重要和关键,高德技术团队在实践中形成了一套自己的解决方案。 分析和定位内存泄漏问题的核心在于分配函数的统计和栈回溯。如果只知道内存分配点不知道调用栈会使问题变得格外复杂,增加解决成本,因此两者缺一不可。 Android中Bionic的malloc_debug模块对内存分配函数的监控及统计是比较完善的,但是栈回溯在Android体系下缺乏高效的方式。随着Android的发展,Google也提供了栈回溯的一些分析方法,但是这些方案存在下面几个问题: 1.栈回溯的环节都使用的libunwind,这种获取方式消耗较大,在Native代码较多的情况下,频繁调用会导致应用很卡,而监控所有内存操作函数的调用栈正需要高频的调用libunwind的相关功能。 2.有ROM要求限制,给日常开发测试带来不便。 3.用命令行或者DDMS进行操作,每排查一次需准备一次环境,手动操作,最终结果也不够直观,同时缺少对比分析。 因此,如何进行高效的栈回溯、搭建系统化的Android Native内存分析体系显得格外重要。 高德地图基于这两点做了一些改进和扩展,经过这些改进,通过自动化测试可及时发现并解决这些问题,大幅提升开发效率,降低问题排查成本。 一、栈回溯加速Android平台上主要采用libunwind来进行栈回溯,可以满足绝大多数情况。但是libunwind实现中的全局锁及unwind table解析,会有性能损耗,在多线程频繁调用情况下会导致应用变卡,无法使用。 加速原理 编译器的-finstrument-functions编译选项支持编译期在函数开始和结尾插入自定义函数,在每个函数开始插入对__cyg_profile_func_enter的调用,在结尾插入对__cyg_profile_func_exit的调用。这两个函数中可以获取到调用点地址,通过对这些地址的记录就可以随时获取函数调用栈了。 插桩后效果示例: 这里需要格外注意,某些不需要插桩的函数可以使用__attribute__((no_instrument_function))来向编译器声明。 如何记录这些调用信息?我们想要实现这些信息在不同的线程之间读取,而且不受影响。一种办法是采用线程的同步机制,比如在这个变量的读写之处加临界区或者互斥量,但是这样又会影响效率了。 能不能不加锁?这时就想到了线程本地存储,简称TLS。TLS是一个专用存储区域,只能由自己线程访问,同时不存在线程安全问题,符合这里的场景。 于是采用编译器插桩记录调用栈,并将其存储在线程局部存储中的方案来实现栈回溯加速。具体实现如下: 1.利用编译器的-finstrument-functions编译选项在编译阶段插入相关代码。 2.TLS中对调用地址的记录采用数组+游标的形式,实现最快速度的插入、删除及获取。 定义数组+游标的数据结构: typedef struct { void* stack[MAX_TRACE_DEEP]; int current;} thread_stack_t;初始化TLS中thread_stack_t的存储key: static pthread_once_t sBackTraceOnce = PTHREAD_ONCE_INIT;static void __attribute__((no_instrument_function))destructor(void* ptr) { if (ptr) { free(ptr); }}static void __attribute__((no_instrument_function))init_once(void) { pthread_key_create(&sBackTraceKey, destructor);}初始化thread_stack_t放入TLS中: get_backtrace_info() { thread_stack_t* ptr = (thread_stack_t*) pthread_getspecific(sBackTraceKey); if (ptr) return ptr; ptr = (thread_stack_t*)malloc(sizeof(thread_stack_t)); ptr->current = MAX_TRACE_DEEP - 1; pthread_setspecific(sBackTraceKey, ptr); return ptr;}3.实现__cyg_profile_func_enter和__cyg_profile_func_exit,记录调用地址到TLS中。 ...

July 15, 2019 · 1 min · jiezi

12内存溢出与内存泄漏

内存溢出与内存泄露一、内存溢出一种程序运行出现的错误。当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误。 二、内存泄漏占用的内存没有及时释放。内存泄露积累多了就容易导致内存溢出。常见的内存泄露: 意外的全局变量。没有及时清理的计时器或回调函数。闭包// 1. 内存溢出var obj = {}for (var i = 0; i < 10000; i++) { obj[i] = new Array(10000000) console.log('-----')}// 2. 内存泄露// 意外的全局变量,没有加varfunction fn() { a = new Array(10000000) console.log(a)}fn()// 3. 没有及时清理的计时器或回调函数var intervalId = setInterval(function () { //启动循环定时器后不清理 console.log('----')}, 1000)// clearInterval(intervalId)// 4. 闭包function fn1() { var a = 4 function fn2() { console.log(++a) } return fn2}var f = fn1()f()// f = null

June 27, 2019 · 1 min · jiezi

一份还热乎的蚂蚁金服面经已拿Offer附答案

本文来自我的知识星球的球友投稿,他在最近的校招中拿到了蚂蚁金服的实习生Offer,整体思路和面试题目由作者——泽林提供,部分答案由Hollis整理自知识星球《Hollis和他的朋友们》中「直面Java」板块。 经历了漫长一个月的等待,终于在前几天通过面试官获悉已被蚂蚁金服录取,这期间的焦虑、痛苦自不必说,知道被录取的那一刻,一整年的阴霾都一扫而空了。 笔者面的是阿里的Java研发工程师岗,面试流程是3轮技术面+1轮hr面。 意外的一面一面的时候大概是3月12号,面完等了差不多半个月才突然接到二面面试官的电话。一面可能是简历面,所以问题比较简单。 ArrayList和LinkedList区别 ArrayList 是一个可改变大小的数组.当更多的元素加入到ArrayList中时,其大小将会动态地增长.内部的元素可以直接通过get与set方法进行访问,因为ArrayList本质上就是一个数组. LinkedList 是一个双链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于ArrayList. 当然,这些对比都是指数据量很大或者操作很频繁的情况下的对比,如果数据和运算量很小,那么对比将失去意义. 什么情况会造成内存泄漏 在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点: 首先,这些对象是可达的,即在有向图中,存在通路可以与其相连; 其次,这些对象是无用的,即程序以后不会再使用这些对象。 如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。 什么是线程死锁,如何解决 产生死锁的条件有四个: 1.互斥条件:所谓互斥就是进程在某一时间内独占资源。 2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 3.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。 4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 线程死锁是因为多线程访问共享资源,由于访问的顺序不当所造成的,通常是一个线程锁定了一个资源A,而又想去锁定资源B;在另一个线程中,锁定了资源B,而又想去锁定资源A以完成自身的操作,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,而无法执行的情况。 要解决死锁,可以从死锁的四个条件出发,只要破坏了一个必要条件,那么我们的死锁就解决了。在java中使用多线程的时候一定要考虑是否有死锁的问题哦。 红黑树是什么?怎么实现?时间复杂度 红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树。 红黑树是特殊的二叉查找树,意味着它满足二叉查找树的特征:任意一个节点所包含的键值,大于等于左孩子的键值,小于等于右孩子的键值。 除了具备该特性之外,红黑树还包括许多额外的信息。 红黑树的每个节点上都有存储位表示节点的颜色,颜色是红(Red)或黑(Black)。 红黑树的特性: (1) 每个节点或者是黑色,或者是红色。 (2) 根节点是黑色。 (3) 每个叶子节点是黑色。 (4) 如果一个节点是红色的,则它的子节点必须是黑色的。 (5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。 关于它的特性,需要注意的是: 第一,特性(3)中的叶子节点,是只为空(NIL或null)的节点。 第二,特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。 具体实现代码这里不贴了,要实现起来,需要包含的基本操作是添加、删除和旋转。在对红黑树进行添加或删除后,会用到旋转方法。旋转的目的是让树保持红黑树的特性。旋转包括两种:左旋 和 右旋。 红黑树的应用比较广泛,主要是用它来存储有序的数据,它的查找、插入和删除操作的时间复杂度是O(lgn)。 TCP三次握手 三次握手(three times handshake;three-way handshake)所谓的“三次握手”即对每次发送的数据量是怎样跟踪进行协商使数据段的发送和接收同步,根据所接收到的数据量而确定的数据确认数及数据发送、接收完毕后何时撤消联系,并建立虚连接。 为了提供可靠的传送,TCP在发送新的数据之前,以特定的顺序将数据包的序号,并需要这些包传送给目标机之后的确认消息。TCP总是用来发送大批量的数据。当应用程序在收到数据后要做出确认时也要用到TCP。 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态; 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。 突如其来的二面一面的时候大概是3月12号,面完等了差不多半个月才突然接到二面面试官的电话。 介绍项目 Storm怎么保证一致性 Storm是一个分布式的流处理系统,利用anchor和ack机制保证所有tuple都被成功处理。如果tuple出错,则可以被重传,但是如何保证出错的tuple只被处理一次呢?Storm提供了一套事务性组件Transaction Topology,用来解决这个问题。 Transactional Topology目前已经不再维护,由Trident来实现事务性topology,但是原理相同。 参考:https://cloud.tencent.com/info/5721fb4532f6a72ed2e563f9449fd025.html 说一下hashmap以及它是否线程安全 ...

April 26, 2019 · 2 min · jiezi

Nodejs-应用故障排查手册-雪崩型内存泄漏问题

摘要: 还有一些问题场景下下应用的内存泄漏非常严重和迅速,甚至于在我们的告警系统感知之前就已经造成应用的 OOM 了,这时我们来不及或者说根本没办法获取到堆快照,因此就没有办法借助于之前的办法来分析为什么进程会内存泄漏到溢出进而 Crash 的原因了。楔子实践篇一中我们也看到了一个比较典型的由于开发者不当使用第三方库,而且在配置信息中携带了三方库本身使用不到的信息,导致了内存泄漏的案例,实际上类似这种相对缓慢的 Node.js 应用内存泄漏问题我们总是可以在合适的机会抓取堆快照进行分析,而且堆快照一般来说确实是分析内存泄漏问题的最佳手段。 但是还有一些问题场景下下应用的内存泄漏非常严重和迅速,甚至于在我们的告警系统感知之前就已经造成应用的 OOM 了,这时我们来不及或者说根本没办法获取到堆快照,因此就没有办法借助于之前的办法来分析为什么进程会内存泄漏到溢出进而 Crash 的原因了。这种问题场景实际上属于线上 Node.js 应用内存问题的一个极端状况,本节将同样从源自真实生产的一个案例来来给大家讲解下如何处理这类极端内存异常。 本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。 最小化复现代码同样我们因为例子的特殊性,我们需要首先给出到大家生产案例的最小化复现代码,建议读者自行运行一番此代码,这样结合起来看下面的排查分析过程会更有收获。最小复现代码还是基于 Egg.js,如下所示: 'use strict';const Controller = require('egg').Controller;const fs = require('fs');const path = require('path');const util = require('util');const readFile = util.promisify(fs.readFile);class DatabaseError extends Error { constructor(message, stack, sql) { super(); this.name = 'SequelizeDatabaseError'; this.message = message; this.stack = stack; this.sql = sql; }}class MemoryController extends Controller { async oom() { const { ctx } = this; let bigErrorMessage = await readFile(path.join(__dirname, 'resource/error.txt')); bigErrorMessage = bigErrorMessage.toString(); const error = new DatabaseError(bigErrorMessage, bigErrorMessage, bigErrorMessage); ctx.logger.error(error); ctx.body = { ok: false }; }}module.exports = MemoryController;这里我们还需要在 app/controller/ 目录下创建一个 resource 文件夹,并且在这个文件夹中添加一个 error.txt,这个 TXT 内容随意,只要是一个能超过 100M 的很大的字符串即可。  ...

April 23, 2019 · 2 min · jiezi

Node.js 应用故障排查手册 —— 正确打开 Chrome devtools

楔子前面的预备章节中我们大致了解了如何在服务器上的 Node.js 应用出现问题时,从常规的错误日志、系统/进程指标以及兜底的核心转储这些角度来排查问题。这样就引出了下一个问题:我们知道进程的 CPU/Memory 高,或者拿到了进程 Crash 后的核心转储,要如何去进行分析定位到具体的 JavaScript 代码段。其实 Chrome 自带的 Devtools,对于 JavaScript 代码的上述 CPU/Memory 问题有着很好的原生解析展示,本节会给大家做一些实用功能和指标的介绍(基于 Chrome v72,不同的版本间使用方式存在差异)。本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。CPU 飙高问题I. 导出 JS 代码运行状态当我们通过第一节中提到的系统/进程指标排查发现当前的 Node.js 应用的 CPU 特别高时,首先我们需要去通过一些方式将当前 Node.js 应用一段时间内的 JavaScript 代码运行状况 Dump 出来,这样子才能分析知道 CPU 高的原因。幸运的是,V8 引擎内部实现了一个 CPU Profiler 能够帮助我们完成一段时间内 JS 代码运行状态的导出,目前也有不少成熟的模块或者工具来帮我们完成这样的操作。v8-profiler 是一个老牌的 Node.js 应用性能分析工具,它可以很方便地帮助开发者导出 JS 代码地运行状态,我们可以在项目目录执行如下命令安装此模块:npm install v8-profiler –save接着可以在代码中按照如下方式获取到 5s 内地 JS 代码运行状态:‘use strict’;const v8Profiler = require(‘v8-profiler’);const title = ’test’;v8Profiler.startProfiling(title, true);setTimeout(() => { const profiler = v8Profiler.stopProfiling(title); profiler.delete(); console.log(profiler);}, 5000);那么我们可以看到,v8-profiler 模块帮我导出的代码运行状态实际上是一个很大的 JSON 对象,我们可以将这个 JSON 对象序列化为字符串后存储到文件:test.cpuprofile 。注意这里的文件名后缀必须为 .cpuprofile ,否则 Chrome devtools 是不识别的。注意:v8-profiler 目前也处于年久失修的状态了,在 Node.js 8 和 Node.js 10 上已经无法正确编译安装了,如果你在 8 或者 10 的项目中想进行使用,可以试试看 v8-profiler-next。II. 分析 CPU Profile 文件借助于 v8-profiler 拿到我们的 Node.js 应用一段时间内的 JS 代码运行状态后,我们可以将其导入 Chrome devtools 中进行分析展示。在 Chrome 72 中,分析我们 Dump 出来的 CPU Profile 的方法已经和之前有所不同了,默认工具栏中也不会展示 CPU Profile 的分析页面,我们需要通过点击工具栏右侧的 更多 按钮,然后选择 More tools -> JavaScript Profiler 来进入到 CPU 的分析页面,如下图所示:选中 JavaScript Profiler 后,在出现的页面上点击 Load 按钮,然后将刚才保存得到的 test.cpuprofile 文件加载进来,就可以看到 Chrome devtools 的解析结果了:这里默认的视图是 Heavy 视图,在这个视图下,Devtools 会按照对你的应用的影响程度从高到低,将这些函数列举出来,点击展开能够看到这些列举出来的函数的全路径,方便你去代码中对应的位置进行排查。这里解释两个比较重要的指标,以便让大家能更有针对性地进行排查:Self Time: 此函数本身代码段执行地时间(不包含任何调用)Total Time: 此函数包含了其调用地其它函数总共的执行时间像在上述地截图例子中,ejs 模块在线上都应该开启了缓存,所以 ejs 模块的 compile 方法不应该出现在列表中,这显然是一个非常可疑的性能损耗点,需要我们去展开找到原因。除了 Heavy 视图,Devtools 实际上还给我们提供了火焰图来进行更多维度的展示,点击左上角可以切换:火焰图按照我们的 CPU 采样时间轴进行展示,那么在这里我们更容易看到我们的 Node.js 应用在采样期间 JS 代码的执行行为,新增的两个指标这边也给大家解释一下其含义:Aggregated self time: 在 CPU 采样期间聚合后的此函数本身代码段的执行总时间(不包含其他调用)Aggregated total time: 在 CPU 采样期间聚合后的此函数包含了其调用地其它函数总共的执行总时间综上,借助于 Chrome devtools 和能够导出当前 Node.js 应用 Javascript 代码运行状态的模块,我们已经可以比较完备地对应用服务异常时,排查定位到相应的 Node.js 进程 CPU 很高的情况进行排查和定位分析了。在生产实践中,这部分的 JS 代码的性能的分析往往也会用到新项目上线前的性能压测中,有兴趣的同学可以更深入地研究下。内存泄漏问题I. 判断是否内存泄漏在笔者的经历中,内存泄漏问题是 Node.js 在线上运行时出现的问题种类中的重灾区。尤其是三方库自身的 Bug 或者开发者使用不当引起的内存泄漏,会让很多的 Node.js 开发者感到束手无策。本节首先向读者介绍下,什么情况下我们的应用算是有很大的可能在发生内存泄漏呢?实际上判断我们的线上 Node.js 应用是否有内存泄漏也非常简单:借助于大家各自公司的一些系统和进程监控工具,如果我们发现 Node.js 应用的总内存占用曲线 处于长时间的只增不降 ,并且堆内存按照趋势突破了 堆限制的 70% 了,那么基本上应用 很大可能 产生了泄漏。当然事无绝对,如果确实应用的访问量(QPS)也在一直增长中,那么内存曲线只增不减也属于正常情况,如果确实因为 QPS 的不断增长导致堆内存超过堆限制的 70% 甚至 90%,此时我们需要考虑的扩容服务器来缓解内存问题。II. 导出 JS 堆内存快照如果确认了 Node.js 应用出现了内存泄漏的问题,那么和上面 CPU 的问题一样,我们需要通过一些办法导出 JS 内存快照(堆快照)来进行分析。V8 引擎同样在内部提供了接口可以直接将分配在 V8 堆上的 JS 对象导出来供开发者进行分析,这里我们采用 heapdump 这个模块,首先依旧是执行如下命令进行安装:npm install heapdump –save接着可以在代码中按照如下方法使用此模块:‘use sytrict’;const heapdump = require(‘heapdump’);heapdump.writeSnapshot(’./test’ + ‘.heapsnapshot’);这样我们就在当前目录下得到了一个堆快照文件:test.heapsnapshot ,用文本编辑工具打开这个文件,可以看到其依旧是一个很大的 JSON 结构,同样这里的堆快照文件后缀必须为 .heapsnapshot ,否则 Chrome devtools 是不识别的。III. 分析堆快照在 Chrome devtools 的工具栏中选择 Memory 即可进入到分析页面,如下图所示:然后点击页面上的 Load 按钮,选择我们刚才生成 test.heapsnapshot 文件,就可以看到分析结果,如下图所示:默认的视图其实是一个 Summary 视图,这里的 Constructor 和我们编写 JS 代码时的构造函数并无不同,都是指代此构造函数创建的对象,新版本的 Chrome devtools 中还在构造函数后面增加 * number 的信息,它代表这个构造函数创建的实例的个数。实际上在堆快照的分析视图中,有两个非常重要的概念需要大家去理解,否则很可能拿到堆快照看着分析结果也无所适从,它们是 Shallow Size 和 Retained Size ,要更好地去理解这两个概念,我们需要先了解 支配树。首先我们看如下简化后的堆快照描述的内存关系图:这里的 1 为根节点,即 GC 根,那么对于对象 5 来说,如果我们想要让对象 5 回收(即从 GC 根不可达),仅仅去掉对象 4 或者对象 3 对于对象 5 的引用是不够的,因为显然从根节点 1 可以分别从对象 3 或者对象 4 遍历到对象 5。因此我们只有去掉对象 2 才能将对象 5 回收,所以在上面这个图中,对象 5 的直接支配者是对象 2。照着这个思路,我们可以通过一定的算法将上述简化后的堆内存关系图转化为支配树:对象 1 到对象 8 间的支配关系描述如下:对象 1 支配对象 2对象 2 支配对象 3 、4 和 5对象 4 支配对象 6对象 5 支配对象 7对象 6 支配对象 8好了,到这里我们可以开始解释什么是 Shallow Size 和 Retained Size 了,实际上对象的 Shallow Size 就是对象自身被创建时,在 V8 堆上分配的大小,结合上面的例子,即对象 1 到 8 自身的大小。对象的 Retained Size 则是把此对象从堆上拿掉,则 Full GC 后 V8 堆能够释放出的空间大小。同样结合上面的例子,支配树的叶子节点对象 3、对象 7 和对象 8 因为没有任何直接支配对象,因此其 Retained Size 等于其 Shallow Size。将剩下的非叶子节点可以一一展开,为了篇幅描述方便,SZ_5表示对象 5 的 Shallow Size,RZ_5 表示对象 5 的 Retained Size,那么可以得到如下结果:对象 3 的 Retained Size:RZ_3 = SZ_3对象 7 的 Retained Size:RZ_7 = SZ_7对象 8 的 Retained Size:RZ_8 = SZ_8对象 6 的 Retained Size:RZ_6 = SZ_6 + RZ_8 = SZ_6 + SZ_8对象 5 的 Retained Size:RZ_5 = SZ_5 + RZ_7 = SZ_5 + SZ_7对象 4 的 Retained Size:RZ_4 = SZ_4 + RZ_6 = SZ_4 + SZ_6 + SZ_8对象 2 的 Retained Size:RZ_2 = SZ_2 + RZ_3 + RZ_4 + RZ_5 = SZ_2 + SZ_3 + SZ_4 + SZ_5 + SZ_6 + SZ_7 + SZ_8GC 根 1 的 Retained Size:RZ_1 = SZ_1 + RZ_2 = SZ_1 + SZ_2 + RZ_3 + RZ_4 + RZ_5 = SZ_2 + SZ_3 + SZ_4 + SZ_5 + SZ_6 + SZ_7 + SZ_8这里可以发现,GC 根的 Retained Size 等于堆上所有从此根出发可达对象的 Shallow Size 之和,这和我们的理解预期是相符合的,毕竟将 GC 根从堆上拿掉的话,原本就应当将从此根出发的所有对象都清理掉。理解了这一点,回到我们最开始看到的默认总览视图中,正常来说,可能的泄漏对象往往其 Retained Size 特别大,我们可以在窗口中依据 Retained Size 进行排序来对那些占据了堆空间绝大部分的对象进行排查:假如确认了可疑对象,Chrome devtools 中也会给你自动展开方便你去定位到代码段,下面以 NativeModule 这个构造器生成的对象 vm 为例:这里上半部分是顺序的引用关系,比如 NativeModule 实例 @45655 的 exports 属性指向了对象 @45589,filename 属性则指向一个字符串 “vm.js”;下半部分则是反向的引用关系:NativeModule 实例 @13021 的 _cache 属性指向了 Object 实例 @41103,而 Object 实例 @41103 的 vm 属性指向了 NativeModule 实例 @45655。如果对这部分展示图表比较晕的可以仔细看下上面的例子,因为找到可疑的泄漏对象,结合上图能看到此对象下的属性和值及其父引用关系链,绝大部分情况下我们就可以定位到生成可疑对象的 JS 代码段了。实际上除了默认的 Summary 视图,Chrome devtools 还提供了 Containment 和 Statistics 视图,这里再看下 Containment 视图,选择堆快照解析页面的左上角可以进行切换,如下图所示:这个视图实际上是堆快照解析出来的内存关系图的直接展示,因此相比 Summary 视图,从这个视图中直接查找可疑的泄漏对象相对比较困难。结尾Chrome devtools 实际上是非常强大的一个工具,本节也只是仅仅介绍了对 CPU Profile 和堆快照解析能力的介绍和常用视图的使用指南,如果你仔细阅读了本节内容,面对服务器上定位到的 Node.js 应用 CPU 飙高或者内存泄漏这样的问题,想必就可以做到心中有数不慌乱了。本文作者:奕钧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 4, 2019 · 3 min · jiezi

Node.js 应用故障排查手册 —— 冗余配置传递引发的内存溢出

楔子前面一小节我们以一个真实的压测案例来给大家讲解如何利用 Node.js 性能平台 生成的 CPU Profile 分析来进行压测时的性能调优。那么与 CPU 相关的问题相比,Node.js 应用中由于不当使用产生的内存问题是一个重灾区,而且这些问题往往都是出现在生产环境下,本地压测都难以复现,实际上这部分内存问题也成为了很多的 Node.js 开发者不敢去将 Node.js 这门技术栈深入运用到后端的一大阻碍。本节将以一个开发者容易忽略的生产内存溢出案例,来展示如何借助于性能平台实现对线上应用 Node.js 应用出现内存泄漏时的发现、分析、定位问题代码以及修复的过程,希望能对大家有所启发。本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。最小化复现代码因为内存问题相对 CPU 高的问题来说比较特殊,我们直接从问题排查的描述可能不如结合问题代码来看比较直观,因此在这里我们首先给出了最小化的复现代码,大家运行后结合下面的分析过程应该能更有收获,样例基于 Egg.js:如下所示:‘use strict’;const Controller = require(’egg’).Controller;const DEFAULT_OPTIONS = { logger: console };class SomeClient { constructor(options) { this.options = options; } async fetchSomething() { return this.options.key; }}const clients = {};function getClient(options) { if (!clients[options.key]) { clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options)); } return clients[options.key];}class MemoryController extends Controller { async index() { const { ctx } = this; const options = { ctx, key: Math.random().toString(16).slice(2) }; const data = await getClient(options).fetchSomething(); ctx.body = data; }}module.exports = MemoryController;然后在 app/router.js 中增加一个 Post 请求路由:router.post(’/memory’, controller.memory.index);造成问题的 Post 请求 Demo 这里也给出来,如下所示:‘use strict’;const fs = require(‘fs’);const http = require(‘http’);const postData = JSON.stringify({ // 这里的 body.txt 可以放一个比较大 2M 左右的字符串 data: fs.readFileSync(’./body.txt’).toString()});function post() { const req = http.request({ method: ‘POST’, host: ’localhost’, port: ‘7001’, path: ‘/memory’, headers: { ‘Content-Type’: ‘application/json’, ‘Content-Length’: Buffer.byteLength(postData) } }); req.write(postData); req.end(); req.on(’error’, function (err) { console.log(12333, err); });}setInterval(post, 1000);最后我们在启动完成最小化复现的 Demo 服务器后,再运行这个 Post 请求的客户端,1s 发起一个 Post 请求,在平台控制台可以看到堆内存在一直增加,如果我们按照本书工具篇中的 Node.js 性能平台使用指南 - 配置合适的告警 一节中配置了 Node.js 进程堆内存告警的话,过一会就会收到平台的 短信/邮件 提醒。问题排查过程收到性能平台的进程内存告警后,我们登录到控制台并且进入应用首页,找到告警对应实例上的问题进程,然后参照工具篇中的 Node.js 性能平台使用指南 - 内存泄漏 中的方法抓取堆快照,并且点击 分析 按钮查看 AliNode 定制后的分解结果展示:这里默认的报表页面顶部的信息含义已经提到过了,这里不再重复,我们重点来看下这里的可疑点信息:提示有 18 个对象占据了 96.38% 的堆空间,显然这里就是我们需要进一步查看的点。我们可以点击 对象名称 来看到这18 个 system/Context 对象的详细内容:这里进入的是分别以这 18 个 system/Context 为根节点起始的支配树视图,因此展开后可以看到各个对象的实际内存占用情况,上图中显然问题集中在第一个对象上,我们继续展开查看:很显然,这里真正吃掉堆空间的是 451 个 SomeClient 实例,面对这样的问题我们需要从两个方面来判断这是否真的是内存异常的问题:当前的 Node.js 应用在正常的逻辑下,是否单个进程需要 451 个 SomeClient 实例如果确实需要这么多 SomeClient 实例,那么每个实例占据 1.98MB 的空间是否合理对于第一个判断,在对应的实际生产面临的问题中,经过代码逻辑的重新确认,我们的应用确实需要这么多的 Client 实例,显然此时排查重点集中在每个实例的 1.98MB 的空间占用是否合理上,假如进一步判断还是合理的,这意味着 Node.js 默认单进程 1.4G 的堆上限在这个场景下是不适用的,需要我们来通过启动 Flag 调大堆上限。正是基于以上的判断需求,我们继续点开这些 SomeClient 实例进行查看:这里可以很清晰的看到,这个 SomeClient 本身只有 1.97MB 的大小,但是下面的 options 属性对应的 Object@428973 对象一个就占掉了 1.98M,进一步展开这个可疑的 Object@428973 对象可以看到,其 ctx 属性对应的 Object@428919 对象正是 SomeClient 实例占据掉如此大的对空间的根本原因所在!我们可以点击其它的 SomeClient 实例,可以看到每一个实例均是如此,此时我们需要结合代码,判断这里的 options.ctx 属性挂载到 SomeClient 实例上是否也是合理的,点击此问题 Object 的地址:进入到这个 Object 的关系图中:Search 展示的视图不同于 Dom 结果图,它实际上展示的是从堆快中解析出来的原始对象关系图,所以边信息是一定会存在的,靠边名称和对象名称,我们比较容易判断对象在代码中的位置。但是在这个例子中,仅仅依靠以 Object@428973 为起始点的内存原始关系图,看不到很明确的代码位置,毕竟不管是 Object.ctx 还是 Object.key 都是相当常见的 JavaScript 代码关系,因此我们继续点击 Retainer 视图:得到如下信息:这里的 Retainer 信息和 Chrome Devtools 中的 Retainer 含义是一样的,它代表了节点在堆内存中的原始父引用关系,正如本文的内存问题案例中,仅靠可疑点本身以及其展开无法可靠地定位到问题代码的情况下,那么展开此对象的 Retainer 视图,可以看到它的父节点链路可以比较方便的定位到问题代码。这里我们显然可以通过在 Retainer 视图下的问题对象父引用链路,很方便地找到代码中创建此对象的代码:function getClient(options) { if (!clients[options.key]) { clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options)); } return clients[options.key];}结合看 SomeClient 的使用,看到用于初始化的 options 参数中实际上只是用到了其 key 属性,其余的属于冗余的配置信息,无需传入。代码修复与确认知道了原因后修改起来就比较简单了,单独生成一个 SomeClient 使用的 options 参数,并且仅将需要的数据从传入的 options 参数上取过来以保证没有冗余信息即可:function getClient(options) { const someClientOptions = Object.assign({ key: options.key }, DEFAULT_OPTIONS); if (!clients[options.key]) { clients[options.key] = new SomeClient(someClientOptions); } return clients[options.key];}重新发布后运行,可以到堆内存下降至只有几十兆,至此 Node.js 应用的内存异常的问题完美解决。结尾本节中也比较全面地给大家展示了如何使用 Node.js 性能平台 来排查定位线上应用内存泄漏问题,其实严格来说本次问题并不是真正意义上的内存泄漏,像这种配置传递时开发者图省事直接全量 Assign 的场景我们在写代码时或多或少时都会遇到,这个问题带给我们的启示还是:当我们去编写一个公共组件模块时,永远不要去相信使用者的传入参数,任何时候都应当只保留我们需要使用到的参数继续往下传递,这样可以避免掉很多问题。本文作者:奕钧 阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 1, 2019 · 2 min · jiezi

深入分析 ThreadLocal 内存泄漏问题

前言ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。但是如果滥用 ThreadLocal,就可能会导致内存泄漏。下面,我们将围绕三个方面来分析 ThreadLocal 内存泄漏的问题ThreadLocal 实现原理ThreadLocal为什么会内存泄漏ThreadLocal 最佳实践ThreadLocal 实现原理ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。ThreadLocal为什么会内存泄漏ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些被动的预防措施并不能保证不会内存泄漏:使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。为什么使用弱引用从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?我们先来看看官方文档的说法:To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.为了应对非常大和长时间的用途,哈希表使用弱引用的 key。下面我们分两种情况讨论:key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。ThreadLocal 最佳实践综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?每次使用完ThreadLocal,都调用它的remove()方法,清除数据。在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。本文作者:肖汉松阅读原文本文为云栖社区原创内容,未经允许不得转载。

February 12, 2019 · 1 min · jiezi

Android内存泄漏定位、分析、解决全方案

原文链接更多教程为什么会发生内存泄漏内存空间使用完毕之后未回收, 会导致内存泄漏。有人会问:Java不是有垃圾自动回收机制么?不幸的是,在Java中仍存在很多容易导致内存泄漏的逻辑(logical leak)。虽然垃圾回收器会帮我们干掉大部分无用的内存空间,但是对于还保持着引用,但逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。例如忘记释放分配的内存的。(Cursor忘记关闭等)。应用不再需要这个对象,未释放该对象的所有引用。强引用持有的对象,垃圾回收器是无法在内存中回收这个对象。持有对象生命周期过长,导致无法回收。Java判断无效对象的原理Android内存回收管理策略图:图中的每个圆节点代表对象的内存资源,箭头代表可达路径。当圆节点与 GC Roots 存在可达路径时,表示当前资源正被引用,虚拟机是无法对其进行回收的(如图中的黄色节点)。反过来,如果圆节点与 GC Roots 不存在可达路径,则意味着这块对象的内存资源不再被程序引用,系统虚拟机可以在 GC 过程中将其回收掉。从定义上讲,Android(Java)平台的内存泄漏是指没有用的对象资源任与GC-Root保持可达路径,导致系统无法进行回收。内存泄漏带来的危害用户对单次的内存泄漏并没有什么感知,但当泄漏积累到内存都被消耗完,就会导致卡顿,崩溃。内存泄露是内存溢出OOM的重要原因之一,会导致CrashAndroid中常见的可能发生内存泄漏的地方1.在Android开发中,最容易引发的内存泄漏问题的是Context。比如Activity的Context,就包含大量的内存引用,一旦泄漏了Context,也意味泄漏它指向的所有对象。造成Activity泄漏的常见原因:Static Activities在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量。如果这个静态变量在Activity生命周期结束后没有清空,就导致内存泄漏。因为static变量是贯穿这个应用的生命周期的,所以被泄漏的Activity就会一直存在于应用的进程中,不会被垃圾回收器回收。static Activity activity; //这种代码要避免单例中保存Activity在单例模式中,如果Activity经常被用到,那么在内存中保存一个Activity实例是很实用的。但是由于单例的生命周期是应用程序的生命周期,这样会强制延长Activity的生命周期,这是相当危险而且不必要的,无论如何都不能在单例子中保存类似Activity的对象。举例:public class Singleton { private static Singleton instance; private Context mContext; private Singleton(Context context){ this.mContext = context; } public static Singleton getInstance(Context context){ if (instance == null){ synchronized (Singleton.class){ if (instance == null){ instance = new Singleton(context); } } } return instance; }}在调用Singleton的getInstance()方法时传入了Activity。那么当instance没有释放时,这个Activity会一直存在。因此造成内存泄露。解决方法:可以将new Singleton(context)改为new Singleton(context.getApplicationContext())即可,这样便和传入的Activity没关系了。Static Views同理,静态的View也是不建议的Inner Classes内部类的优势可以提高可读性和封装性,而且可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。 例如在内部类中持有Activity对象解决方法:1.将内部类变成静态内部类;2.如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用;3.在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;例如:发生内存泄漏的代码:public class LeakAct extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_leak); test(); } //这儿发生泄漏 public void test() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }解决方法:public class LeakAct extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_leak); test(); } //加上static,变成静态匿名内部类 public static void test() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }Anonymous Classes匿名类也维护了外部类的引用。当你在匿名类中执行耗时任务,如果用户退出,会导致匿名类持有的Activity实例就不会被垃圾回收器回收,直到异步任务结束。原文链接更多教程Handlerhandler中,Runnable内部类会持有外部类的隐式引用,被传递到Handler的消息队列MessageQueue中,在Message消息没有被处理之前,Activity实例不会被销毁了,于是导致内存泄漏。解决方法:1.可以把Handler类放在单独的类文件中,或者使用静态内部类便可以避免泄露;2.如果想在Handler内部去调用所在的Activity,那么可以在handler内部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的.3.在界面销毁是,释放handler资源@Override protected void doOnDestroy() { super.doOnDestroy(); if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); } mHandler = null; mRenderCallback = null; }同样还有其他匿名类实例,如TimerTask、Threads等,执行耗时任务持有Activity的引用,都可能导致内存泄漏。线程产生内存泄露的主要原因在于线程生命周期的不可控。如果我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。要解决Activity的长期持有造成的内存泄漏,可以通过以下方法:传入Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,主动结束执行的任务,并释放Activity资源。将线程的内部类,改为静态内部类。因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会hold其主体(引用)。也就使主体不能被释放,亦即内存泄露。静态类编译后和非内部类是一样的,有自己独立的类名。不会悄悄引用所属类的实例,所以就不容易泄露。如果需要引用Acitivity,使用弱引用。谨慎对context使用static关键字。2.Bitmap没调用recycle()Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后才设置为null.3.集合中对象没清理造成的内存泄露我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。解决方案:在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。4.注册没取消造成的内存泄露这种Android的内存泄露比纯Java的内存泄漏还要严重,因为其他一些Android程序可能引用系统的Android程序的对象(比如注册机制)。即使Android程序已经结束了,但是别的应用程序仍然还有对Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。解决方案:1.使用ApplicationContext代替ActivityContext;2.在Activity执行onDestory时,调用反注册;5.资源对象没关闭造成的内存泄露资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。而不是等待GC来处理。6.占用内存较多的对象(图片过大)造成内存溢出因为Bitmap占用的内存实在是太多了,特别是分辨率大的图片,如果要显示多张那问题就更显著了。Android分配给Bitmap的大小只有8M.解决方法:1.等比例缩小图片BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一 2.对图片采用软引用,及时地进行recycle()操作//软引用 SoftReference<Bitmap> bitmap = new SoftReference<Bitmap>(pBitmap); //回收操作 if(bitmap != null) { if(bitmap.get() != null && !bitmap.get().isRecycled()){ bitmap.get().recycle(); bitmap = null; } } 7.WebView内存泄露(影响较大)解决方案:用新的进程起含有WebView的Activity,并且在该Activity 的onDestory() 最后加上 System.exit(0); 杀死当前进程。检测内存泄漏的方法1.使用 静态代码分析工具-Lint 检查内存泄漏Lint 是 Android Studio 自带的工具,使用姿势很简单 Analyze -> Inspect Code 然后选择想要扫面的区域即可对可能引起泄漏的编码,Lint 都会进行温馨提示:2.LeakCanary 工具Square 公司出品的内存分析工具,官方地址如下:https://github.com/square/lea…LeakCanary 需要在项目代码中集成,不过代码也非常简单,如下的官方示例:在你的 build.gradle:dependencies { debugImplementation ‘com.squareup.leakcanary:leakcanary-android:1.6.3’ releaseImplementation ‘com.squareup.leakcanary:leakcanary-android-no-op:1.6.3’ // Optional, if you use support library fragments: debugImplementation ‘com.squareup.leakcanary:leakcanary-support-fragment:1.6.3’}在 Application 类:public class ExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); if (LeakCanary.isInAnalyzerProcess(this)) { // This process is dedicated to LeakCanary for heap analysis. // You should not init your app in this process. return; } LeakCanary.install(this); // Normal app init code… }}当内存泄漏发生时,LeakCanary 会弹窗提示并生成对应的堆存储信息记录-3.Android Monitor开Android Studio,编译代码,在模拟器或者真机上运行App,然后点击,在Android Monitor下点击Monitor对应的Tab,进入如下界面在Memory一栏中,可以观察不同时间App内存的动态使用情况,点击可以手动触发GC,点击可以进入HPROF Viewer界面,查看Java的Heap,如下图Reference Tree代表指向该实例的引用,可以从这里面查看内存泄漏的原因,Shallow Size指的是该对象本身占用内存的大小,Retained Size代表该对象被释放后,垃圾回收器能回收的内存总和。扩展知识四种引用类型的介绍强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;软引用(SoftReference):只有在内存空间不足时,才会被回的对象;弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。原文链接更多教程 ...

February 11, 2019 · 2 min · jiezi

Spring Boot引起的“堆外内存泄漏”排查及经验总结

背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于Spring Boot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=128m -XX:InitialCodeCacheSize=128m, -Xss512k -Xmx4g -Xms4g,-XX:+UseG1GC -XX:G1HeapRegionSize=4M”,实际使用的物理内存如下图所示:排查过程1. 使用Java层面的工具定位内存区域(堆内内存、Code区域或者使用unsafe.allocateMemory和DirectByteBuffer申请的堆外内存)笔者在项目中添加-XX:NativeMemoryTracking=detailJVM参数重启项目,使用命令jcmd pid VM.native_memory detail查看到的内存分布如下:发现命令显示的committed的内存小于物理内存,因为jcmd命令显示的内存包含堆内内存、Code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他Native Code(C代码)申请的堆外内存。所以猜测是使用Native Code申请内存所导致的问题。为了防止误判,笔者使用了pmap查看内存分布,发现大量的64M的地址;而这些地址空间不在jcmd命令所给出的地址空间里面,基本上就断定就是这些64M的内存所导致。2. 使用系统层面的工具定位堆外内存因为笔者已经基本上确定是Native Code所引起,而Java层面的工具不便于排查此类问题,只能使用系统层面的工具去定位问题。首先,使用了gperftools去定位问题gperftools的使用方法可以参考gperftools,gperftools的监控如下:从上图可以看出:使用malloc申请的的内存最高到3G之后就释放了,之后始终维持在700M-800M。笔者第一反应是:难道Native Code中没有使用malloc申请,直接使用mmap/brk申请的?(gperftools原理就使用动态链接的方式替换了操作系统默认的内存分配器(glibc)。)然后,使用strace去追踪系统调用因为使用gperftools没有追踪到这些内存,于是直接使用命令“strace -f -e"brk,mmap,munmap" -p pid”追踪向OS申请内存请求,但是并没有发现有可疑内存申请。strace监控如下图所示:接着,使用GDB去dump可疑内存因为使用strace没有追踪到可疑内存申请;于是想着看看内存中的情况。就是直接使用命令gdp -pid pid进入GDB之后,然后使用命令dump memory mem.bin startAddress endAddressdump内存,其中startAddress和endAddress可以从/proc/pid/smaps中查找。然后使用strings mem.bin查看dump的内容,如下:从内容上来看,像是解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,那么在项目启动之后使用strace作用就不是很大了。所以应该在项目启动的时候使用strace,而不是启动完成之后。再次,项目启动时使用strace去追踪系统调用项目启动使用strace追踪系统调用,发现确实申请了很多64M的内存空间,截图如下:使用该mmap申请的地址空间在pmap对应如下:最后,使用jstack去查看对应的线程因为strace命令中已经显示申请内存的线程ID。直接使用命令jstack pid去查看线程栈,找到对应的线程栈(注意10进制和16进制转换)如下:这里基本上就可以看出问题来了:MCC(美团统一配置中心)使用了Reflections进行扫包,底层使用了Spring Boot去加载JAR。因为解压JAR使用Inflater类,需要用到堆外内存,然后使用Btrace去追踪这个类,栈如下:然后查看使用MCC的地方,发现没有配置扫包路径,默认是扫描所有的包。于是修改代码,配置扫包路径,发布上线后内存问题解决。3. 为什么堆外内存没有释放掉呢?虽然问题已经解决了,但是有几个疑问:为什么使用旧的框架没有问题?为什么堆外内存没有释放?为什么内存大小都是64M,JAR大小不可能这么大,而且都是一样大?为什么gperftools最终显示使用的的内存大小是700M左右,解压包真的没有使用malloc申请内存吗?带着疑问,笔者直接看了一下Spring Boot Loader那一块的源码。发现Spring Boot对Java JDK的InflaterInputStream进行了包装并且使用了Inflater,而Inflater本身用于解压JAR包的需要用到堆外内存。而包装之后的类ZipInflaterInputStream没有释放Inflater持有的堆外内存。于是笔者以为找到了原因,立马向Spring Boot社区反馈了这个bug。但是反馈之后,笔者就发现Inflater这个对象本身实现了finalize方法,在这个方法中有调用释放堆外内存的逻辑。也就是说Spring Boot依赖于GC释放堆外内存。笔者使用jmap查看堆内对象时,发现已经基本上没有Inflater这个对象了。于是就怀疑GC的时候,没有调用finalize。带着这样的怀疑,笔者把Inflater进行包装在Spring Boot Loader里面替换成自己包装的Inflater,在finalize进行打点监控,结果finalize方法确实被调用了。于是笔者又去看了Inflater对应的C代码,发现初始化的使用了malloc申请内存,end的时候也调用了free去释放内存。此刻,笔者只能怀疑free的时候没有真正释放内存,便把Spring Boot包装的InflaterInputStream替换成Java JDK自带的,发现替换之后,内存问题也得以解决了。这时,再返过来看gperftools的内存分布情况,发现使用Spring Boot时,内存使用一直在增加,突然某个点内存使用下降了好多(使用量直接由3G降为700M左右)。这个点应该就是GC引起的,内存应该释放了,但是在操作系统层面并没有看到内存变化,那是不是没有释放到操作系统,被内存分配器持有了呢?继续探究,发现系统默认的内存分配器(glibc 2.12版本)和使用gperftools内存地址分布差别很明显,2.5G地址使用smaps发现它是属于Native Stack。内存地址分布如下:到此,基本上可以确定是内存分配器在捣鬼;搜索了一下glibc 64M,发现glibc从2.11开始对每个线程引入内存池(64位机器大小就是64M内存),原文如下:按照文中所说去修改MALLOC_ARENA_MAX环境变量,发现没什么效果。查看tcmalloc(gperftools使用的内存分配器)也使用了内存池方式。为了验证是内存池搞的鬼,笔者就简单写个不带内存池的内存分配器。使用命令gcc zjbmalloc.c -fPIC -shared -o zjbmalloc.so生成动态库,然后使用export LD_PRELOAD=zjbmalloc.so替换掉glibc的内存分配器。其中代码Demo如下:#include<sys/mman.h>#include<stdlib.h>#include<string.h>#include<stdio.h>//作者使用的64位机器,sizeof(size_t)也就是sizeof(long) void* malloc ( size_t size ){ long* ptr = mmap( 0, size + sizeof(long), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0 ); if (ptr == MAP_FAILED) { return NULL; } ptr = size; // First 8 bytes contain length. return (void)(&ptr[1]); // Memory that is after length variable}void calloc(size_t n, size_t size) { void ptr = malloc(n * size); if (ptr == NULL) { return NULL; } memset(ptr, 0, n * size); return ptr;}void *realloc(void ptr, size_t size){ if (size == 0) { free(ptr); return NULL; } if (ptr == NULL) { return malloc(size); } long plen = (long)ptr; plen–; // Reach top of memory long len = plen; if (size <= len) { return ptr; } void rptr = malloc(size); if (rptr == NULL) { free(ptr); return NULL; } rptr = memcpy(rptr, ptr, len); free(ptr); return rptr;}void free (void ptr ){ if (ptr == NULL) { return; } long plen = (long)ptr; plen–; // Reach top of memory long len = plen; // Read length munmap((void)plen, len + sizeof(long));}通过在自定义分配器当中埋点可以发现其实程序启动之后应用实际申请的堆外内存始终在700M-800M之间,gperftools监控显示内存使用量也是在700M-800M左右。但是从操作系统角度来看进程占用的内存差别很大(这里只是监控堆外内存)。笔者做了一下测试,使用不同分配器进行不同程度的扫包,占用的内存如下:为什么自定义的malloc申请800M,最终占用的物理内存在1.7G呢?因为自定义内存分配器采用的是mmap分配内存,mmap分配内存按需向上取整到整数个页,所以存在着巨大的空间浪费。通过监控发现最终申请的页面数目在536k个左右,那实际上向系统申请的内存等于512k * 4k(pagesize) = 2G。为什么这个数据大于1.7G呢?因为操作系统采取的是延迟分配的方式,通过mmap向系统申请内存的时候,系统仅仅返回内存地址并没有分配真实的物理内存。只有在真正使用的时候,系统产生一个缺页中断,然后再分配实际的物理Page。总结整个内存分配的流程如上图所示。MCC扫包的默认配置是扫描所有的JAR包。在扫描包的时候,Spring Boot不会主动去释放堆外内存,导致在扫描阶段,堆外内存占用量一直持续飙升。当发生GC的时候,Spring Boot依赖于finalize机制去释放了堆外内存;但是glibc为了性能考虑,并没有真正把内存归返到操作系统,而是留下来放入内存池了,导致应用层以为发生了“内存泄漏”。所以修改MCC的配置路径为特定的JAR包,问题解决。笔者在发表这篇文章时,发现Spring Boot的最新版本(2.0.5.RELEASE)已经做了修改,在ZipInflaterInputStream主动释放了堆外内存不再依赖GC;所以Spring Boot升级到最新版本,这个问题也可以得到解决。参考资料GNU C Library (glibc)Native Memory TrackingSpring BootgperftoolsBtrace作者简介纪兵,2015年加入美团,目前主要从事酒店C端相关的工作。 ...

January 4, 2019 · 2 min · jiezi