关于内存管理:一文揭秘高效稳定的-Apache-Doris-内存管理机制

作者:SelectDB 高级研发工程师、Apache Doris Committer 邹新一背景Apache Doris 作为基于 MPP 架构的 OLAP 数据库,数据从磁盘加载到内存后,会在算子间流式传递并计算,在内存中存储计算的两头后果,这种形式缩小了频繁的磁盘 I/O 操作,充分利用多机多核的并行计算能力,可在性能上出现微小劣势。 在面临内存资源耗费微小的简单计算和大规模作业时,无效的内存调配 、统计、 管控对于零碎的稳定性起着非常要害的作用——更快的内存调配速度将无效晋升查问性能,通过对内存的调配、跟踪与限度能够保障不存在内存热点,及时精确地响应内存不足并尽可能躲避 OOM 和查问失败,这一系列机制都将显著进步零碎稳定性;更准确的内存统计,也是大查问落盘的根底。 问题和思考在内存短缺时内存治理通常对用户是无感的,但实在场景中往往面临着各式各样的极其 Case ,这些都将为内存性能和稳定性带来挑战,在过来版本中,用户在应用 Apache Doris 时在内存治理方面遭逢了以下挑战:OOM 导致 BE 过程解体。内存不足时,用户能够承受执行性能稍慢一些,或是让后到的工作排队,或是让大量工作失败,总之心愿无效限度过程的内存应用而不是宕机;BE 过程占用内存较高。用户反馈 BE 过程占用了较多内存,但很难找到对应内存耗费大的查问或导入工作,也无奈无效限度单个查问的内存应用;用户很难正当的设置每个query的内存大小,所以经常出现内存还很短缺,然而query 被cancel了;高并发性能降落重大,也无奈疾速定位到内存热点;构建 HashTable 的两头数据不反对落盘,两个大表的 Join 因为内存超限无奈实现。针对开发者而言又存在另外一些问题,比方内存数据结构性能重叠且应用凌乱,MemTracker 的构造难以了解且手动统计易出错等。 针对以上问题,咱们经验了过来多个版本的迭代与优化。从 Apache Doris 1.1.0 版本开始,咱们逐步对立内存数据结构、重构 MemTracker、开始反对查问内存软限,并引入过程内存超限后的 GC 机制,同时优化了高并发的查问性能等。在 Apache Doris 1.2.4 版本中,Apache Doris 内存管理机制已趋于欠缺,在 Benchmark、压力测试和实在用户业务场景的反馈中,根本打消了内存热点以及 OOM 导致 BE 宕机的问题,同时可定位内存 Top 的查问、反对查问内存灵便限度。而在最新的 Doris 2.0 alpha 版本中,咱们实现了查问的异样平安,并将逐渐配合 Pipeline 执行引擎和两头数据落盘 , 让用户不再受内存不足困扰。 在此咱们将零碎介绍 Apache Doris 在内存治理局部的实现与优化。 ...

May 25, 2023 · 4 min · jiezi

关于内存管理:记一次-rr-和硬件断点解决内存踩踏问题

在日常的调试过程中,咱们总会遇到一些乏味的 bug,在本文我就遇到了一个有意思的查问后果不统一问题。 故事的开始咱们在测试 NebulaGraph 的 MATCH 语句的时候发现一个很神奇的事件: (root@nebula) [gdlancer]> match (v1)-[e*1..1]->(v2) where id(v1) in [1, 2, 3, 4] and (v2)-[e*1..1]->(v1) return e;+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| e |+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+| [[:Rel_5 2->2 @0 {Rel_5_0_Bool: true, Rel_5_1_Bool: true, Rel_5_2_Double: 0.533698, Rel_5_3_String: "Stephen Curry", Rel_5_4_Double: 0.162998}]] || [[:Rel_1 2->2 @0 {Rel_1_0_Int: 3, Rel_1_1_Int: 5, Rel_1_2_Int: 81, Rel_1_3_Double: 0.975062, Rel_1_4_Bool: true, Rel_1_5_Int: 59}]] || [[:Rel_0 2->2 @0 {Rel_0_0_Bool: true, Rel_0_1_String: "Kevin Durant", Rel_0_2_String: "Joel Embiid", Rel_0_3_Int: 96, Rel_0_4_Double: 0.468568, Rel_0_5_Int: 98, Rel_0_6_Int: 77}]] || [[:Rel_2 2->2 @0 {Rel_2_0_Int: 38, Rel_2_1_Double: 0.120953, Rel_2_2_String: "Null1", Rel_2_3_Bool: false, Rel_2_4_Bool: true, Rel_2_5_Int: 6, Rel_2_6_String: "Tracy McGrady"}]] || [[:Rel_3 2->2 @0 {Rel_3_0_String: "Aron Baynes", Rel_3_1_String: "LeBron James", Rel_3_2_Double: 0.831096, Rel_3_3_Int: 11}]] || [[:Rel_4 2->2 @0 {Rel_4_0_Bool: true, Rel_4_1_String: "Kevin Durant", Rel_4_2_Double: 0.71757, Rel_4_3_String: "Marc Gasol", Rel_4_4_Double: 0.285247, Rel_4_5_String: "Cory Joseph"}]] |+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+Got 6 rows (time spent 146.7ms/168.31625ms)Tue, 03 Jan 2023 14:10:03 CST(root@nebula) [gdlancer]> match (v1)-[e*1..1]->(v2) where id(v1) in [1, 2, 3, 4] and (v2)-[e*1..1]->(v1) return e;+---+| e |+---++---+Empty set (time spent 30.67ms/58.220042ms)Tue, 03 Jan 2023 14:10:05 CST同样的语句,两次查问的后果集竟然不一样! ...

March 22, 2023 · 33 min · jiezi

关于内存管理:golang的内存管理

0.1、索引https://blog.waterflow.link/articles/1663406367769 1、内存治理内存治理是治理计算机内存的过程,在主存和磁盘之间挪动过程以进步零碎的整体性能。内存治理的根本要求是提供办法来依据程序的申请动静的将局部内存调配给程序,并在不须要时开释它以供重用。 程序通过将他们的内存划分为执行特定工作的不同局部来治理他们。栈和堆就是这部分中的俩个,他们管理程序的未应用的内存并将其调配给不同类型的数据。当程序不再须要这些内存的时候就会开释他们,供后续应用。 2、经典的内存模型 正在运行的程序将其数据保留在这些逻辑区域之中。操作系统在将逻辑加载到内存时为全局变量和动态变量分配内存,并且在程序完结之前不会开释它。这些值不会被批改。另外俩个区域,堆和栈,更多的是动态分配的变量。在这些区域中,程序依据须要去调配和开释内存。这两个区域之间的区别上面会说到。 文本段文本段是指标文件或内存中程序的一部分,其中蕴含可执行指令。文本段个别放在堆或栈的下方,以避免堆栈溢出被笼罩。 初始化数据和未初始化数据段数据段是程序虚拟地址空间的一部分,其中蕴含由程序员初始化的全局变量和动态变量。 栈栈用于动态内存调配,就像数据结构中的栈,遵循后进先出。通常函数和局部变量会在栈上调配,当数据被调配到栈上或是从栈上弹出时,实际上没有任何物理挪动,只有保留在栈中的值会被批改。这使得从栈中存储和查问数据的过程十分快,因为不须要查找。 咱们能够从它最下面的块中存储和查问数据。 存储在栈上的任何数据都必须是无限且动态的。 这意味着数据的大小在编译时是已知的。 堆的内存治理简单明了,由操作系统实现。 因为栈的大小是无限的,咱们可能会在这里遇到堆栈溢出谬误。 堆这里的堆和数据结构中的堆是没有关系的。堆用于动态内存调配。栈只容许在顶部进行调配和开释,而程序能够在堆中的任何地位调配或开释内存。程序必须以与其调配相同的程序将内存返回到栈。然而程序能够以任何程序将内存返回到堆中。这意味着堆比栈更灵便。指针、数组和大数据结构通常存储在堆中。 存储在堆上的数据必须造成一个足够大的间断块,以应用单个内存块满足申请。此属性减少了堆的复杂性。首先,执行调配操作的代码必须扫描堆,直到找到足够大的间断内存块来满足申请。其次,当内存返回堆时,必须合并相邻的已开释块,以更好地适应将来对大块内存的申请。这种减少的复杂性意味着应用堆治理内存比应用堆栈慢。 堆内存调配计划不提供主动解除调配。咱们须要应用垃圾回收器来删除未应用的对象,以便无效地应用内存。 栈和堆的区别与栈相比,堆更慢,因为查找数据的过程波及更多。堆比栈能够存储更多的数据。堆以动静大小存储数据;栈以动态大小存储数据。堆在应用程序的线程之间共享。堆因为其动静个性而更难治理。当咱们议论内存治理时,咱们次要是在议论治理堆内存。堆内存调配不是线程平安的,因为存储在此空间中的数据对所有线程都是可拜访或可见的。内存调配的重要性内存是无限的。如果一个程序持续耗费内存而不开释它,它将耗尽内存并自行解体。因而,软件程序不能得心应手地持续应用内存,它会导致其余程序和过程耗尽内存。因为这一点的重要性,大多数编程语言(包含 Go)都提供了主动内存治理的办法。 3、go的内存模型Go 反对主动内存治理,例如主动内存调配和主动垃圾回收,防止了很多暗藏的 bug。 在 Go 中,每个线程都有本人的堆栈。当咱们启动一个线程时,咱们调配一块内存用作该线程的堆栈。当这块内存不够用时,问题就来了。为了克服这个问题,咱们能够减少堆栈的默认大小,但减少堆栈的大小意味着每个线程的大小都会减少。如果咱们想应用大量线程,这将十分低效。 另一种抉择是独自决定每个线程的堆栈大小。同样,在咱们有很多线程的设置中,这将是低效的,因为咱们须要弄清楚咱们应该为每个堆栈调配多少内存。 Go 的创建者想出的不是给每个 goroutine 一个固定数量的堆栈内存,而是 Go 运行时尝试依据须要为 goroutine 提供所需的堆栈空间。这样咱们在创立线程时就不须要思考堆栈大小了。 goroutine 以2 kb的堆栈大小开始,能够依据须要增长和放大。Go 查看它行将执行的函数所需的堆栈数量是否可用,如果不够用,则调用morestack调配一个新帧,而后才执行该函数。当该函数退出时,它的返回参数被复制回原始堆栈帧,并且任何不须要的堆栈空间都被开释。 堆栈也有下限。如果达到此限度,咱们的应用程序将panic并停止。 Go 在两个中央分配内存:一个用于动态分配的全局堆和一个用于每个 goroutine 的本地堆栈。Go 与许多垃圾收集语言相比的一个次要区别是,许多对象间接调配在程序堆上。Go 更喜爱在栈上调配。栈调配代价更低,因为它只须要两条 CPU 指令:一条推入栈进行调配,另一条从栈中开释。 可怜的是,并非所有数据都能够应用栈上调配的内存。栈调配要求能够在编译时确定变量的生命周期和内存占用。如果无奈确定,则在运行时动态分配到堆上。 Go 编译器应用一个称为逃逸剖析的过程来查找其生命周期在编译时已知的对象,并将它们调配到栈上而不是在垃圾回收的堆内存中。根本思维是在编译时做垃圾回收的工作。编译器跨代码区域跟踪变量的范畴。它应用这些数据来确定哪些变量持有一组查看,以证实它们的生命周期在运行时是齐全可知的。如果变量通过了这些查看,则能够在栈上调配值。如果不是,就代表逃逸,并且必须进行堆调配。 内存是在栈上调配还是逃到堆上齐全取决于你如何应用内存,而不是你如何申明变量。 能够通过上面的命令查看是否有内存逃逸,go build -gcflags '-m'。 4、垃圾回收垃圾回收是主动内存治理的一种模式。垃圾回收器尝试回收由程序调配但不再被援用的内存。 Go 的垃圾回收器是一个非分代并发、三色标记和清理垃圾回收器。 分代垃圾回收器专一于最近调配的对象,因为它假如像长期变量这样的短期对象最常被回收。 Go 编译器更喜爱在栈上调配对象,短期对象通常调配在栈上而不是堆上;这意味着不须要分代GC。 Go 的垃圾回收分为两个阶段,标记阶段和革除阶段。GC 应用三色算法来剖析内存块的应用状况。该算法首先将仍被援用的对象标记为“沉闷”,并在下一阶段(扫描)开释不沉闷对象的内存。 不必回收垃圾,然而能够缩小垃圾导致垃圾回收代价高次要因素之一是堆上的对象数量。通过优化咱们的代码以缩小堆上长寿命对象的数量,咱们能够最大限度地缩小破费在 GC 上的资源,并进步咱们的零碎性能。 重构构造 在读取数据时,古代计算机 CPU 的外部数据寄存器能够保留和解决 64 位。这称为字长。它通常是 32 位或 64 位的。 ...

September 17, 2022 · 2 min · jiezi

关于内存管理:Enhanced-SWAP内存管理-OpenHarmony构建新的内存管理优化方案ESWAP

OpenHarmony是面向全场景泛终端设备的操作系统,终端设备内存性能的强弱会间接影响用户的体验。终端设备的内存差别很大,对于内存比拟小的终端设备,内存优化计划无疑是加强内存性能、晋升用户体验的要害。针对传统内存计划及管理机制的有余,OpenHarmony构建了一套欠缺的内存解决方案——ESWAP。 1. 传统内存计划及管理机制在传统的Linux内存优化计划中,终端设备通常采纳SWAP及ZRAM内存计划。 1.1 SWAP SWAP即内存替换技术或虚拟内存技术,如图1所示,在零碎的物理内存不足时,把内存中的一部分不罕用的内存空间释放出来,以增大零碎可用内存供以后运行的程序应用。这些被开释的数据被长期保留到SWAP分区中,等到须要应用时,再从SWAP分区中复原到内存中。 图1 SWAP虚拟内存技术 从图1中不难看出,SWAP内存替换技术增大了设施内用内存,然而,SWAP内存换入/换出时会遭逢IO性能瓶颈,重大时甚至会影响用户的应用体验,并且flash存储器件的频繁读写也会缩减其寿命。 1.2 ZRAM ZRAM即内存压缩技术,如图2所示,在零碎的物理内存不足时,将零碎物理内存的一部分划分进去作为ZRAM分区,而后把不罕用的匿名页压缩后放到ZRAM分区里,相当于就义了一些CPU效率,以增大零碎可用内存供以后运行的程序应用。等到须要应用时,再从ZRAM分区中将数据解压进去。 图2 ZRAM内存压缩技术 尽管ZRAM在肯定水平上增大了设施内用内存,然而如果没有适合的形式来对内存进行治理,负面影响也会非常明显,将会造成内存页频繁的压缩/解压缩,从而抢占失常业务的CPU工夫,减少零碎的功耗。并且,如果压缩/解压速度不够快的话,会间接影响用户的应用体验。 1.3 内存管理机制 除了内存计划有余,传统的内存调配及治理形式,无奈感知业务个性及数据的重要性。如果终端设备多个过程或业务共用一块内存,当内存负载越来越重,进行内存数据回收时,会频繁呈现数据搬移,以及内存震荡的景象。这些景象会减轻内核治理内存的开销,并导致系统CPU负载长期处于高负载的状态,从而减少零碎功耗。 2. OpenHarmony内存解决方案针对原有内存计划的有余,OpenHarmony构建了一套欠缺的内存解决方案ESWAP,买通了下层零碎到内核的调用栈,让内核能在下层配置的领导下,对每一块内存数据进行正当的治理。 上面咱们将为大家介绍ESWAP解决方案以及其关键技术的解析。 2.1 ESWAP计划介绍 ESWAP(Enhanced SWAP)是OpenHarmony针对内存优化问题提供的一套欠缺的内存解决方案,联合内存压缩和内存替换技术,定制了一套正当高效的调度管理策略,使压缩和替换两者的工作可能高效且均衡。ESWAP基于关联性的数据聚合技术及下层领导策略,将内存划分为不同的分组进行治理,通过回收优先级来辨别不同分组下内存的沉闷水平,优先压缩、换出较不沉闷的内存数据,以晋升数据交换性能,缩小寿命冲击。 ESWAP解决方案的整体框架如图3所示: 图3 ESWAP解决方案 ESWAP解决方案在全局资源调度子系统中减少了一个系统资源调度模块,通过向账户子系统订阅本地账户的变动来感知以后的账户状态和内存状态,而后依据账户状态给各个账户设置不同的回收优先级、设置指标可用内存量、设置压缩和换出的比例等参数,并将这些参数下发给ZSWAPD。ZSWAPD会根据回收优先级判断回收的先后顺序;根据指标可用内存量和以后可用内存量的差值决定回收的量;根据压缩和换出的比例来决定压缩和换出的量,从而实现在达成内存扩大成果前提下的性能和功耗均衡。 2.2 关键技术解析 ESWAP内存解决方案都用到了哪些关键技术呢?上面为你一一道来。 2.2.1 定制的ZRAM和替换分区 ESWAP联合内存压缩和内存替换技术,提供了自定义新增存储分区作为内存替换分区的能力,并在内核中创立了一个常驻过程ZSWAPD,用于将ZRAM压缩后的匿名页加密换出到ESWAP存储分区中,从而能齐全地空出一块可用内存,以此来达到维持Memavailable水线的指标,如图4所示。 图4 ESWAP技术 同时,ESWAP模块还能够记录每个匿名页的冷热特色信息,并将这些数据通过关联性、冷热程序进行相应的寄存,使ESWAP替换区中间断寄存的匿名页具备工夫和空间局部性。因而在匿名页换入时,能够将替换区中的相邻匿名页一并读入ZRAM,以此来保证数据的存取速度,晋升IO性能。 2.2.2 动静的内存回收机制 OpenHarmony提供了一种额定的内存回收机制ZSWAPD,并创立了“buffer”来作为掂量以后零碎内存能力的指标。buffer指的是以后零碎能提供的最大可用内存。ZSWAPD会依据buffer量以及上文所述的各种策略,来对匿名页进行压缩换出以回收。同时,ZSWAPD还能依据内存冷热拆散的合理性以及内存回收状态, 动静地管制ZRAM和ESWAP之间的均衡,从而取得更高的能效比。 2.2.3 灵便的内存回收策略 OpenHarmony基于Memcg分组进行了回收策略的加强,应用回收优先级来领导ZSWAPD回收的先后顺序。回收策略将既定的buffer相干配置下发给ZSWAPD,来领导其回收适当数量的内存。此外,因为匿名页可能存储在RAM、ZRAM、ESWAP三个模块中, 下层能够依据须要,通过灵便地配置替换策略,管制这三个模块中存储的比例,防止频繁换入换出带来的负面影响。 至此,ESWAP的三项关键技术就介绍完了,咱们来总结一下: ● 定制的ZRAM和替换分区:将数据通过关联性、冷热程序进行寄存,保障了数据的存取速度,晋升了IO性能。 ● 动静的内存回收机制:从回收优先级、可用内存量、压缩和替换比例三个维度动静地控制数据回收,从而取得更高的能效比。 ● 灵便的内存回收策略:灵便地管制RAM、ZRAM、ESWAP三个模块数据存储的比例,保障了各个模块的均衡。 2.3 ESWAP相干接口 ESWAP解决方案支持系统开发者定制本人的回收策略,并在/dev/memcg下提供了仅对下层回收策略可见的接口。零碎开发者能够通过这些接口来定制本人的下层策略,具体接口如下所示: 以上就是本文全部内容,ESWAP解决方案仍在一直建设中,期待宽广开发者退出咱们,独特见证全场景智能时代的有限可能! 感兴趣的小伙伴能够通过上面链接获取ESWAP源码进行深刻理解: https://gitee.com/openharmony...

April 27, 2022 · 1 min · jiezi

关于内存管理:Linux内核内存管理总结

微信公众号:奔跑吧linux社区 加奔跑吧微信群请先加微信:runninglinuxkernel欢送订阅奔跑吧配套旗舰篇视频节目:https://weidian.com/?userid=1... 这篇对《奔跑吧Linux内核》第一版的内存治理 做了很棒的笔记和总结。《奔跑吧Linux内核》第一版各大书店有售。这篇文章作者是:Arnold Lu@南京原文:https://www.cnblogs.com/arnol... Linux的内存治理波及到的内容十分庞杂,而且与内核的方方面面耦合在一起,想要了解透彻十分艰难。在开始学习之前进行了一些筹备工作《如何开展Linux Memory Management学习?》, 参考资料遂决定以如下材料作为参考,进行Linux内存治理的钻研:《奔跑吧 Linux内核》:以第2章为底本开展,这是目前能获取的紧跟以后内核倒退(Linux 4.0),并且讲的比拟全面的一本材料。 《Understanding the Linux Virtual Memory Manager》:简略说就是虽老但经典,基于(Linux 2.4/2.6)。作者是目前依然沉闷在Linux社区MM专家。 《wowotech Memory Management》:没有其余系列经典,也没有条理系列的介绍MM,然而依然值得按考。 《tolimit Linux内存源码剖析》:绝对零散的介绍了内存相干剖析文档 《Linux Kernel v4.0》:当然必不可少的,是源码了。 当逐步深刻看到MMU相干代码时,读一下ARM架构对于MMU的规格书有助于了解。 不然对于虚拟地址到物理地址的映射就会很虚无,这些材料包含《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition》的《Virtual Memory System Architecture》,以及相干MMU TRM。 Linux Memory Management框架图整个内存治理从宏观上能够分为三大部分:用户空间、内核空间和相干硬件。用户空间次要是libc对相干零碎调用进行封装,对用户程序提供API,罕用的有malloc、mmap、munmap、remap、madvise、mempolicy等等。 相干硬件包含MMU/TLB、L1/L2 Cache以及DDR RAM,具体到ARM架构须要对照MMU/L2 Cache以及RAM规格书。 内核空间就简单多了,首先介绍初始化及初始化后的布局。 2.1 物理内存初始化从获取内存大小、初始化页表,再进行zone初始化,而后在zone中应用搭档零碎进行物理内存初始化; 2.2 页表的映射过程讲述了ARM32和ARM64两种架构下的页表映射,如何从虚拟地址由MMU转化成物理页面地址的; 2.3 内核内存的布局图在内存被初始化之后,内核的内存布局基本上就确定了,ARM32和ARM64下布局有很大区别。在malloc一节brk中介绍了用户空间的布局。2.1~2.3是内存的一个动态状态,在有了这些根底之后,2.4~2.9依照从低层到下层的一一介绍了。2.4 调配物理页面介绍了基于搭档零碎的页调配和开释;2.5 slab分配器基于搭档零碎,slab调配更小内存块;以及基于slab的kmalloc;2.6 vmalloc和kmalloc区别在于v,即在VMALLOC区域调配;2.7 VMA即Virtual Memory Area,是过程内存治理的外围;2.8 malloc和2.9 mmap都基于VMA,malloc/free用于调配/开释一块内存;mmap/munmap用于匿名/文件映射到用户空间。以及mmap(补充)。因为malloc/mmap分配内存并不是立刻调配,只是在用到的时候才会触发2.10 缺页中断解决。在缺页但页有余的状况下,就须要进行一些操作调整内存,这些操作的根底是2.11 page援用计数,还有页面的2.12 反向映射RMAP技术。在内存不足状况下触发kswapd2.13 回收页面,其中匿名页面有着非凡的2.14 匿名页面生命周期。在kswapd回收仍然无奈满足内存调配,就须要对内存进行2.16 内存规整,它依赖的技术是2.15 页面迁徙。因为内存中存在一些内容齐全一样的页面,应用2.17 KSM技术进行合并,同时利用COW技术,在须要时重新分配。还介绍了2.18 Dirty COW内存破绽,而后对内存治理数据结构和API进行了总结2.19 总结内存治理数据结构和API。最初2.20 最新更新和瞻望对新技术进行了介绍。除了以上技术,还有如下内存技术: ...

April 3, 2022 · 2 min · jiezi

关于内存管理:如何定位内存泄露

残缺高频题库仓库地址:https://github.com/hzfe/awesome-interview 残缺高频题库浏览地址:https://febook.hzfe.org/ 相干问题垃圾回收机制答复关键点垃圾回收 DevTools 内存透露是指不再应用的内存,没有被垃圾回收机制回收。当内存透露很大或足够频繁时,用户会有所感知:轻则影响利用性能,体现为缓慢卡顿;重则导致利用奔溃,体现为无奈失常应用。为了防止内存透露带来的不良影响,须要对垃圾回收机制进行理解,把握内存透露分析方法,欠缺线上相干监控措施。 内存透露定位和剖析个别须要辅助工具,比方 Chrome DevTools。开发者能够通过 DevTools 记录页面流动详情,生成可视化剖析后果,从时间轴中直观理解内存透露状况;利用 DevTools 获取若干次内存快照,查看内存堆栈变动;以及应用 Chrome 工作管理器,实时监控内存的应用状况。 知识点深刻1. 排查内存透露常见问题在 JavaScript 中,当一些不再须要的数据依然可达时,V8 会认为这些数据仍在被应用,不会开释内存。为了调试内存透露,咱们须要找到被谬误保留的数据,并确保 V8 可能将其清理掉。 代码量较小时,开发者通常能够基于以下根本准则进行疾速自查: 是否滥用全局变量,没有手动回收。是否没有正确销毁定时器、闭包。是否没有正确监听事件和销毁事件。除此之外,开发者能够借助内部工具进行内存透露排查。 2. 应用 Chrome DevTools 定位内存透露Performance 关上筹备剖析的页面和 DevTools 的 Performance 面板,勾选 Memory 并开始录制,在模仿用户操作一段时间后完结录制,DevTools 会将这段时间内的页面行为流动进行记录和剖析。 通过生成的后果能够直观查看到内存工夫线,理解内存随工夫的占用变动,如果内存占用曲线成阶梯状始终回升,则可能存在内存透露。按需选取工夫线中的区域片段,查看对应时间段内的流动类型和工夫占用,作为排查和定位内存透露的辅助方法。 Memory 关上筹备剖析的页面和 DevTools 的 Memory 面板,按需生成快照。每个快照的内容是快照时刻,进行一次垃圾回收后,利用中所有可达的对象。 当开发者明确晓得与内存透露关联的用户交互步骤时,能够生成屡次内存快照进行比照,排查出透露的对象:在做用户交互操作之前,进行一次失常内存堆栈信息的快照;在做用户交互操作中或操作完结时,进行内存快照。应用 Comparison 视图或应用 filter 按需查看快照之间的差别。 下面的图中应用 filter 查看快照 2 和快照 3 的差别,通过后果可知在两个快照之间继续被调配 clickCallback 闭包。通过点击文件门路能够定位到内存透露的代码。 3. Node.js 中的内存透露定位如果须要定位 Node.js 中的内存透露,启动 Node.js 时带上 --inspect 参数,以便利用 Chrome DevTools 工具生成 Memory 快照数据。如图所示,启动 Node.js 服务后,关上 Chrome DevTools,会有 Node 标识,点击能够关上 Node 专用 DevTools。 ...

October 30, 2021 · 1 min · jiezi

关于内存管理:虚拟内存精粹

博客原文虚拟内存精粹 导言虚拟内存是当今计算机系统中最重要的抽象概念之一,它的提出是为了更加无效地治理内存并且升高内存出错的概率。虚拟内存影响着计算机的方方面面,包含硬件设计、文件系统、共享对象和过程/线程调度等等,每一个致力于编写高效且出错概率低的程序的程序员都应该深刻学习虚拟内存。 本文全面而深刻地分析了虚拟内存的工作原理,帮忙读者疾速而粗浅地了解这个重要的概念。 计算机存储器存储器是计算机的核心部件之一,在齐全现实的状态下,存储器应该要同时具备以下三种个性: 速度足够快:存储器的存取速度该当快于 CPU 执行一条指令,这样 CPU 的效率才不会受限于存储器容量足够大:容量可能存储计算机所需的全副数据价格足够便宜:价格低廉,所有类型的计算机都能装备然而事实往往是残暴的,咱们目前的计算机技术无奈同时满足上述的三个条件,于是古代计算机的存储器设计采纳了一种分档次的构造: 从顶至底,古代计算机里的存储器类型别离有:寄存器、高速缓存、主存和磁盘,这些存储器的速度逐级递加而容量逐级递增。存取速度最快的是寄存器,因为寄存器的制作资料和 CPU 是雷同的,所以速度和 CPU 一样快,CPU 拜访寄存器是没有时延的,然而因为价格昂贵,因而容量也极小,个别 32 位的 CPU 装备的寄存器容量是 32✖️32 Bit,64 位的 CPU 则是 64✖️64 Bit,不论是 32 位还是 64 位,寄存器容量都小于 1 KB,且寄存器也必须通过软件自行治理。 第二层是高速缓存,也即咱们平时理解的 CPU 高速缓存 L1、L2、L3,个别 L1 是每个 CPU 独享,L3 是全副 CPU 共享,而 L2 则依据不同的架构设计会被设计成独享或者共享两种模式之一,比方 Intel 的多核芯片采纳的是共享 L2 模式而 AMD 的多核芯片则采纳的是独享 L2 模式。 第三层则是主存,也即主内存,通常称作随机拜访存储器(Random Access Memory, RAM)。是与 CPU 间接替换数据的外部存储器。它能够随时读写(刷新时除外),而且速度很快,通常作为操作系统或其余正在运行中的程序的长期材料存储介质。 最初则是磁盘,磁盘和主存相比,每个二进制位的成本低了两个数量级,因而容量比之会大得多,动辄上 GB、TB,而毛病则是访问速度则比主存慢了大略三个数量级。机械硬盘速度慢次要是因为机械臂须要一直在金属盘片之间挪动,期待磁盘扇区旋转至磁头之下,而后能力进行读写操作,因而效率很低。 主存物理内存咱们平时始终提及的物理内存就是上文中对应的第三种计算机存储器,RAM 主存,它在计算机中以内存条的模式存在,嵌在主板的内存槽上,用来加载各式各样的程序与数据以供 CPU 间接运行和应用。 虚拟内存在计算机领域有一句如同摩西十诫般神圣的哲言:"计算机科学畛域的任何问题都能够通过减少一个间接的中间层来解决",从内存治理、网络模型、并发调度甚至是硬件架构,都能看到这句哲言在闪烁着光辉,而虚拟内存则是这一哲言的完满实际之一。 虚拟内存是古代计算机中的一个十分重要的存储器形象,次要是用来解决应用程序日益增长的内存应用需要:古代物理内存的容量增长曾经十分疾速了,然而还是跟不上应用程序对主存需要的增长速度,对于应用程序来说内存还是可能会不够用,因而便须要一种办法来解决这两者之间的容量差矛盾。为了更高效地治理内存并尽可能打消程序谬误,古代计算机系统对物理主存 RAM 进行形象,实现了虚拟内存 (Virtual Memory, VM) 技术。 ...

April 19, 2021 · 3 min · jiezi

关于内存管理:简单聊聊内存逃逸-|-剑指-offer-golang

问题简略讲讲golang的内存逃逸吗? 解析什么是内存逃逸在程序中,每个函数块都会有本人的内存区域用来存本人的局部变量(内存占用少)、返回地址、返回值之类的数据,这一块内存区域有特定的构造和寻址形式,寻址起来十分迅速,开销很少。这一块内存地址称为栈。栈是线程级别的,大小在创立的时候曾经确定,当变量太大的时候,会"逃逸"到堆上,这种景象称为内存逃逸。简略来说,局部变量通过堆调配和回收,就叫内存逃逸。 内存逃逸的危害堆是一块没有特定构造,也没有固定大小的内存区域,能够依据须要进行调整。全局变量,内存占用较大的局部变量,函数调用完结后不能立即回收的局部变量都会存在堆外面。变量在堆上的调配和回收都比在栈上开销大的多。对于 go 这种带 GC 的语言来说,会减少 gc 压力,同时也容易造成内存碎片。 如何分析程序是否产生内存逃逸build时增加-gcflags=-m 选项可剖析内存逃逸状况,比方输入./main.go:3:6: moved to heap: x 示意局部变量x逃逸到了堆上。 内存逃逸产生机会向 channel 发送指针数据。因为在编译时,不晓得channel中的数据会被哪个 goroutine 接管,因而编译器没法晓得变量什么时候才会被开释,因而只能放入堆中。package mainfunc main() { ch := make(chan int, 1) x := 5 ch <- x // x不产生逃逸,因为只是复制的值 ch1 := make(chan *int, 1) y := 5 py := &y ch1 <- py // y逃逸,因为y地址传入了chan中,编译时无奈确定什么时候会被接管,所以也无奈在函数返回后回收y}局部变量在函数调用完结后还被其余中央应用,比方函数返回局部变量指针或闭包中援用包外的值。因为变量的生命周期可能会超过函数周期,因而只能放入堆中。package mainfunc Foo () func (){ x := 5 // x产生逃逸,因为在Foo调用实现后,被闭包函数用到,还不能回收,只能放到堆上寄存 return func () { x += 1 }}func main() { inner := Foo() inner()}在 slice 或 map 中存储指针。比方 []*string,其前面的数组可能是在栈上调配的,但其援用的值还是在堆上。package mainfunc main() { var x int x = 10 var ls []*int ls = append(ls, &x) // x产生逃逸,ls存储的是指针,所以ls底层的数组尽管在栈存储,但x自身却是逃逸到堆上}切片扩容后长度太大,导致栈空间有余,逃逸到堆上。package mainfunc main() { s := make([]int, 10000, 10000) for index, _ := range s { s[index] = index }}在 interface 类型上调用办法。 在 interface 类型上调用办法时会把interface变量应用堆调配, 因为办法的真正实现只能在运行时晓得。package maintype foo interface { fooFunc()}type foo1 struct{}func (f1 foo1) fooFunc() {}func main() { var f foo f = foo1{} f.fooFunc() // 调用办法时,f产生逃逸,因为办法是动态分配的}防止内存逃逸的方法对于小型的数据,应用传值而不是传指针,防止内存逃逸。防止应用长度不固定的slice切片,在编译期无奈确定切片长度,只能将切片应用堆调配。interface调用办法会产生内存逃逸,在热点代码片段,审慎应用。写在最初喜爱本文的敌人,欢送关注公众号「会玩 code」,专一大白话分享实用技术。 ...

April 18, 2021 · 1 min · jiezi

关于v8:V8-堆栈空间和垃圾回收机制

微信公众号:[前端一锅煮]一点技术、一点思考。栈空间堆空间新生代内存回收老生代内存回收标记革除、标记整顿、增量标记JavaScript 引擎的内存空间次要分为栈和堆。 V8 的垃圾回收策略次要基于分代式垃圾回收机制。依照对象的存活工夫将内存的垃圾回收进行不同分代,而后别离对不同分代的内存应用最适宜的算法。次要分为新生代和老生代,有标记革除、标记整顿、增量标记等办法。 栈空间栈是长期存储空间,次要存储局部变量和函数调用。 根本类型赋值(Number, Boolean, String, Null, Undefined, Symbol, BigInt),零碎会为新的变量在栈内存中调配一个新值。 援用类型赋值,零碎会为新的变量在栈内存中调配一个值,这个值仅仅是指向同一个对象的援用,和原对象指向的都是堆内存中的同一个对象。 对于函数,解释器创立了”调用栈“来记录函数的调用过程。每调用一个函数,解释器就把该函数增加进调用栈,解释器会为被增加进来的函数创立一个栈帧(用来保留函数的局部变量以及执行语句)并立刻执行。如果正在执行的函数还调用了其余函数,新函数会持续被增加进入调用栈。函数执行实现,对应的栈帧立刻被销毁。 两种查看调用栈的办法应用 console.trace() 向 web 控制台输入一个堆栈跟踪。 浏览器开发者工具进行断点调试。 栈溢出栈尽管很轻量,在应用时创立,应用完结后销毁,然而不是能够有限增长的,被调配的调用栈空间被占满时,就会引起”栈溢出“的谬误。 (function foo() { foo()})()Maximum call stack size exceeded. 为什么根本数据类型存储在栈中,援用数据类型存储在堆中?JavaScript 引擎须要用栈来维护程序执行期间的上下文的状态,如果栈空间大了的话,所有数据都寄存在栈空间外面,会影响到上下文切换的效率,进而影响整个程序的执行效率。 堆空间堆空间存储的数据比较复杂,大抵能够划分为 5 个区域: 新生代内存区(new space):新生代内存区会被划分为两块,别离是 from space 和 to space(具体有什么用下文会说),64位零碎下默认 32MB,32位零碎下默认 16MB,通常新创建的对象会先放入 from 中。老生代内存区(old space):较为长久的保留对象,分为两个区域 old pointer space 和 old data space 别离用来寄存 GC 后还存活的指针信息和数据信息,64位零碎下能应用约 1.4GB,32位零碎下能应用约 0.7GB。大对象区(large object space):这里寄存体积超过其余区大小的对象,次要为了防止大对象的拷贝,应用该空间专门存储大对象。单元区、属性单元区、Map区(Cell space、property cell space、map space):Map 空间寄存对象的 Map 信息也就是暗藏类(Hiden Class)最大限度为 8MB;每个 Map 对象固定大小,为了疾速定位,所以将该空间独自进去。代码区 (code Space):次要寄存代码对象,最大限度为 512MB,也是惟一领有执行权限的内存。新生代内存是长期调配的内存,存活工夫短,老生代内存是常驻内存,存活工夫长。 ...

April 11, 2021 · 1 min · jiezi

关于内存管理:Go看源码必会知识之unsafe包

原文链接:戳这里 前言有看源码的敌人应该会发现,Go规范库中大量应用了unsafe.pointer,要想更好的了解源码实现,就要晓得unsafe.pointer到底是什么?所以明天就与大家来聊一聊unsafe包。什么是unsafe家喻户晓,Go语言被设计成一门强类型的动态语言,那么他的类型就不能扭转了,动态也是意味着类型查看在运行前就做了。所以在Go语言中是不容许两个指针类型进行转换的,应用过C语言的敌人应该晓得这在C语言中是能够实现的,Go中不容许这么应用是处于平安思考,毕竟强制转型会引起各种各样的麻烦,有时这些麻烦很容易被觉察,有时他们却又暗藏极深,难以觉察。大多数读者可能不明确为什么类型转换是不平安的,这里用C语言举一个简略的例子: int main(){ double pi = 3.1415926; double *pv = &pi; void *temp = pd; int *p = temp;}在规范C语言中,任何非void类型的指针都能够和void类型的指针互相指派,也能够通过void类型指针作为中介,实现不同类型的指针间接互相转换。下面示例中,指针pv指向的空间本是一个双精度数据,占8个字节,然而通过转换后,p指向的是一个4字节的int类型。这种产生内存截断的设计缺点会在转换后进行内存拜访是存在安全隐患。我想这就是Go语言被设计成强类型语言的起因之一吧。 尽管类型转换是不平安的,然而在一些非凡场景下,应用了它,能够突破Go的类型和内存平安机制,能够绕过类型零碎低效,进步运行效率。所以Go规范库中提供了一个unsafe包,之所以叫这个名字,就是不举荐大家应用,然而不是不能用,如果你把握的特地好,还是能够实际的。 unsafe 实现原理在应用之前咱们先来看一下unsafe的源码局部,规范库unsafe包中只提供了3种办法,别离是: func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptrSizeof(x ArbitrayType)办法次要作用是用返回类型x所占据的字节数,但并不蕴含x所指向的内容的大小,与C语言规范库中的Sizeof()办法性能一样,比方在32位机器上,一个指针返回大小就是4字节。Offsetof(x ArbitraryType)办法次要作用是返回构造体成员在内存中的地位离构造体起始处(构造体的第一个字段的偏移量都是0)的字节数,即偏移量,咱们在正文中看一看到其入参必须是一个构造体,其返回值是一个常量。Alignof(x ArbitratyType)的次要作用是返回一个类型的对齐值,也能够叫做对齐系数或者对齐倍数。对齐值是一个和内存对齐无关的值,正当的内存对齐能够进步内存读写的性能。个别对齐值是2^n,最大不会超过8(受内存对齐影响).获取对齐值还能够应用反射包的函数,也就是说:unsafe.Alignof(x)等价于reflect.TypeOf(x).Align()。对于任意类型的变量x,unsafe.Alignof(x)至多为1。对于struct构造体类型的变量x,计算x每一个字段f的unsafe.Alignof(x,f),unsafe.Alignof(x)等于其中的最大值。对于array数组类型的变量x,unsafe.Alignof(x)等于形成数组的元素类型的对齐倍数。没有任何字段的空struct{}和没有任何元素的array占据的内存空间大小为0,不同大小为0的变量可能指向同一块地址。仔细的敌人会发发现这三个办法返回的都是uintptr类型,这个目标就是能够和unsafe.poniter类型互相转换,因为*T是不能计算偏移量的,也不能进行计算,然而uintptr是能够的,所以能够应用uintptr类型进行计算,这样就能够能够拜访特定的内存了,达到对不同的内存读写的目标。三个办法的入参都是ArbitraryType类型,代表着任意类型的意思,同时还提供了一个Pointer指针类型,即像void *一样的通用型指针。 type ArbitraryType inttype Pointer *ArbitraryType// uintptr 是一个整数类型,它足够大,能够存储type uintptr uintptr下面说了这么多,可能会有点懵,在这里对三种指针类型做一个总结: *T:一般类型指针类型,用于传递对象地址,不能进行指针运算。unsafe.poniter:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(需转换到某一类型的一般指针)uintptr:用于指针运算,GC不把uintptr当指针,uintptr无奈持有对象。uintptr类型的指标会被回收。三者关系就是:unsafe.Pointer是桥梁,能够让任意类型的指针实现互相转换,也能够将任意类型的指针转换为uintptr进行指针运算,也就说uintptr是用来与unsafe.Pointer打配合,用于指针运算。画个图示意一下: 基本原理就说到这里啦,接下来咱们一起来看看如何应用~ unsafe.Pointer根本应用咱们在上一篇剖析atomic.Value源码时,看到atomic/value.go中定义了一个ifaceWords构造,其中typ和data字段类型就是unsafe.Poniter,这里应用unsafe.Poniter类型的起因是传入的值就是interface{}类型,应用unsafe.Pointer强转成ifaceWords类型,这样能够把类型和值都保留了下来,不便前面的写入类型查看。截取局部代码如下: // ifaceWords is interface{} internal representation.type ifaceWords struct { typ unsafe.Pointer data unsafe.Pointer}// Load returns the value set by the most recent Store.// It returns nil if there has been no call to Store for this Value.func (v *Value) Load() (x interface{}) { vp := (*ifaceWords)(unsafe.Pointer(v)) for { typ := LoadPointer(&vp.typ) // 读取曾经存在值的类型 /** ..... 两头省略 **/ // First store completed. Check type and overwrite data. if typ != xp.typ { //以后类型与要存入的类型做比照 panic("sync/atomic: store of inconsistently typed value into Value") }}下面就是源码中应用unsafe.Pointer的一个例子,有一天当你筹备读源码时,unsafe.pointer的应用到处可见。好啦,接下来咱们写一个简略的例子,看看unsafe.Pointer是如何应用的。 ...

February 1, 2021 · 3 min · jiezi

关于内存管理:前端面试每日-31-第652天

明天的知识点 (2021.01.27) —— 第652天 (我也要出题)[html] 如何在页面关上PDF文件?[css] 你有应用过css的clamp函数吗?说说它有什么用处?[js] 增加原生事件如果不移除为什么会内存泄露?[软技能] 说说你对大数据的了解《论语》,曾子曰:“吾日三省吾身”(我每天屡次检查本人)。前端面试每日3+1题,以面试题来驱动学习,每天提高一点!让致力成为一种习惯,让奋斗成为一种享受!置信 保持 的力量!!!欢送在 Issues 和敌人们一起探讨学习! 我的项目地址:前端面试每日3+1【举荐】欢送跟 jsliang 一起折腾前端,零碎整顿前端常识,目前正在折腾 LeetCode,打算买通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个Star, 同时欢送微信扫码关注 前端剑解 公众号,并退出 “前端学习每日3+1” 微信群互相交换(点击公众号的菜单:交换)。 学习不打烊,充电加油只为遇到更好的本人,365天无节假日,每天早上5点纯手工公布面试题(死磕本人,愉悦大家)。心愿大家在这虚夸的前端圈里,放弃沉着,保持每天花20分钟来学习与思考。在这变幻无穷,类库层出不穷的前端,倡议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢送大家到Issues交换,激励PR,感激Star,大家有啥好的倡议能够加我微信一起交换探讨!心愿大家每日去学习与思考,这才达到来这里的目标!!!(不要为了谁而来,要为本人而来!)交换探讨欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个[Star]

January 27, 2021 · 1 min · jiezi

ObjectiveC的内存管理1内存管理概述

概述应用程序开发中,内存管理是个重要的话题。简单而言,语言层面的内存管理基本有三类: 1. 纯粹的手动管理如C和曾经的C++。 char *some_string = malloc(BUFFER_SIZE);// do somethingfree(some_string);这个简单的例子里用完就释放还好,但是有时候这个some_string被传来传去不知道飞哪儿去了,就比较尴尬。纯手动管理的代价是程序员的心智负担比较重。即使后来C++程序员们抽象出RAII这样的实践规范,一定程度上降低了管理的复杂度,但是相对来说成本还是略高。随着语言的发展,已经很少有语言只依赖手动管理内存了。 2. 基于某些机制实现半自动管理这里的某些机制其实通常就是引用计数。毕竟这是最简单的内存管理辅助手段。 引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。引用计数大家都了解,不多说,单纯的自动使用引用计数问题在于无法解决循环引用的问题。很多语言选择让程序员付出一点劳动来解决这个问题。早年,Objc选择的是退一步,完全让程序员来管理引用计数的加减,称为MRC,显然管理成本偏高。后来推出了ARC,提供了更健全的机制,程序员只要标识出对象间的引用关系是强引用还是弱引用就可以了,大大降低了程序员的负担。虽然走这个路子的语言不算多,但除了Objc之外还是有好几个的。C++的智能指针跟Objc的ARC就比较相似。而Rust的所有权模型本质上也是类似的。 3. 自动垃圾回收通过GC自动管理内存大概是现在的主流了。对程序员来讲实在是太舒适了,开发时几乎不用考虑内存管理的问题。Java、JavaScript、Python、go等一大票语言都是走的这条路。GC是基于可达性分析算法的,即,从根节点(全局变量、局部变量等等)出发,遍历引用到的对象,所有没遍历到的对象就可以释放了。当然从原理到实际应用中间差了十万八千里。朴素的GC会经常造成Stop The World。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。于是很多GC算法被发明出来用于优化、减少。常见的CMS、G1回收算法都极大地减少了STW的时间,但仍然不能完全避免。R大在Java 大内存应用(10G 以上)会不会出现严重的停顿?中提到Zing JVM采用的C4算法是可以完全避免STW的,不过看起来为了避免STW,C4算法会吃掉更多的内存,程序吞吐量会受到影响。 小结总结一下吧,纯手动管理基本上已被淘汰,ARC(暂且把方法二这类都称为ARC吧)和GC对比之下,对开发者,ARC需要程序员付出一定的代价进行管理,GC则基本上完全解放双手;性能上,GC通常会造成STW现象,对响应时间比较敏感的程序,比如高频交易系统,是很难接受的,而ARC不会对性能造成明显影响。 几点有趣的事情1. 总体性能总体性能上,只要不是内存跑得特别满,ARC的总体代价是高于GC的。其实想想就知道了,GC只关注两次回收间的变化,而ARC要对每一次引用的改变进行计数,总体性能上比GC差是很正常的,但由于ARC的耗时是均匀分布在运行时间里的,通常我们不用很关注。关于这个问题可以参考这篇论文。 2. cpython的方案另外比较特别的是,Python的默认解释器CPython中应用了引用计数与垃圾回收相结合的手法,没有循环引用的对象会被引用计数回收,剩下的交给GC处理,大大降低了GC的压力。感觉很有意思。 3. ARC的性能在类ARC方案上,C++提供的能力是比较全面的。这可能跟c++常用于一些性能比较苛刻的场景有关。出于性能原因,使用c++智能指针时有如下指导思想: 对象的所有权不重要时 ,用裸指针对象的所有权唯一时,用unique_ptr,能用unique_ptr就不要用shared_ptr。要处理复杂情况时,可以使用shared_ptr,但需要注意不要滥用。当引用关系不影响所有权时,用weak_ptr。Rust也有类似的能力。而python和Objective-C就没有这么多讲究,所有的引用计数其实都是shared_ptr。Objc在iOS上这么多年,而后来的swift也传承了ARC,基本上可以认为,移动端应用从小到大都不差这么一丢丢性能。 以此推论,绝大部分c++应用也完全没必要关注这几个指针间的差异,操起shared_ptr就是干。

May 25, 2019 · 1 min · jiezi

Node-内存管理和垃圾回收

前言从前端思维转变到后端, 有一个很重要的点就是内存管理。以前写前端因为只是在浏览器上运行, 所以对于内存管理一般不怎么需要上心, 但是在服务器端, 则需要斤斤计较内存。 V8的内存限制和垃圾回收机制内存限制内存限制一般的后端语言开发中, 在基本的内存使用是没有限制的。 但由于Node是基于V8构建的, 而V8对于内存的使用有一定的限制。 在默认情况下, 64位的机器大概可以使用1.4G, 而32则为0.7G的大小。关于为什么要限制内存大小, 有两个方面。一个是V8一开始是为浏览器服务的, 而在浏览器端这样的内存大小是绰绰有余的。另一个则是待会提到的垃圾回收机制, 垃圾回收会暂停Js的运行, 如果内存过大, 就会导致垃圾回收的时间变长, 从而导致Js暂停的时间过长。 当然, 我们可以在启动Node服务的时候, 手动设置内存的大小 如下: node --max-old-space-size=768 // 设置老生代, 单位为MB node --max-semi-space-size=64 // 设置新生代, 单位为MB查看内存 在Node环境中, 可以通过process.memoryUsage()来查看内存分配 rss(resident set size):所有内存占用,包括指令区和堆栈heapTotal:V8引擎可以分配的最大堆内存,包含下面的 heapUsedheapUsed:V8引擎已经分配使用的堆内存external: V8管理C++对象绑定到JavaScript对象上的内存事实上, 对于大文件的操作通常会使用Buffer, 究其原因就是因为Node中内存小的原因, 而使用Buffer是不受这个限制, 它是堆外内存, 也就是上面提到的external。 v8的内存分代目前没有一种垃圾自动回收算法适用于所有场景, 所以v8的内部采用的其实是两种垃圾回收算法。他们回收的对象分别是生存周期较短和生存周期较长的两种对象。关于具体的算法, 参考下文。 这里先介绍v8是怎么做内存分代的。 新生代 v8中的新生代主要存放的是生存周期较短的对象, 它具有两个空间semispace, 分别为From和To, 在分配内存的时候将内存分配给From空间, 当垃圾回收的时候, 会检查From空间存活的对象(广度优先算法)并复制到To空间, 然后清空From空间, 再互相交换From和To空间的位置, 使得To空间变为From空间。 该算法缺陷很明显就是有一半的空间一直闲置着并且需要复制对象, 但是由于新生代本身具有的内存比较小加上其分配的对象都是生存周期比较短的对象, 所以浪费的空间以及复制使用的开销会比较小。 在64位系统中一个semisapce为16MB, 而32位则为8MB, 所以新生代内存大小分别为32MB和16MB。 老生代 老生代主要存放的是生存周期比较长的对象。内存按照 1MB 分页,并且都按照 1MB 对齐。新生代的内存页是连续的,而老生代的内存页是分散的,以链表的形式串联起来。 它的内部有4种类型。 ...

May 5, 2019 · 3 min · jiezi

php中的内存管理

一、php内存管理概述——Zend引擎由于计算机的内存由操作系统进行管理,所以普通应用程序是无法直接对内存进行访问的。应用程序只能向操作系统申请内存,通常的应用也是这么做的,在需要的时候通过类似malloc之类的库函数 向操作系统申请内存。在一些对性能要求较高的应用场景下是需要频繁的使用和释放内存的, 比如Web服务器,编程语言等,由于向操作系统申请内存空间会引发系统调用, 系统调用和普通的应用层函数调用性能差别非常大,因为系统调用会将CPU从用户态切换到内核, 因为涉及到物理内存的操作,只有操作系统才能进行,而这种切换的成本是非常大的, 如果频繁的在内核态和用户态之间切换会产生性能问题。鉴于系统调用的开销,一些对性能有要求的应用通常会自己在用户态进行内存管理, 例如第一次申请稍大的内存留着备用,而使用完释放的内存并不是马上归还给操作系统, 可以将内存进行复用,这样可以避免多次的内存申请和释放所带来的性能消耗。PHP不需要显式的对内存进行管理,这些工作都由Zend引擎进行管理了。PHP内部有一个内存管理体系, 它会自动将不再使用的内存垃圾进行释放。二、php中查看与设置内存的相关参数与函数配置内存大小:(1)php.ini中可以更改配置memory_limit = 32M(2)若环境中没有禁用ini_set()函数,可以通过此函数设置:ini_set(“memory_limit”, “128M”);查看内存情况:(1)memory_get_usage(),这个函数的作用是获取 目前PHP脚本所用的内存大小。(2)memory_get_peak_usage(),这个函数的作用返回 当前脚本到目前位置所占用的内存峰值,这样就可能获取到目前的脚本的内存需求情况。

March 26, 2019 · 1 min · jiezi

C 语言之柔性数组

一 历史在c99标准出来之前。如果要在某个结构体中使用字符串变时,为了使字符串变量存储地址能与结构体整体连在一起,需要这样实现#include <stdio.h>#include <malloc.h>#include <string.h>typedef struct pen{ int len; char data;//字符串变量}pen;int main(int argc, char argv){ char str[] = “this is a string”;//需要填入的字符串 / 动态申请一个pen类型结构体变量, 它的大小为,pen类型的本身长度, 再加上str(需要填入字符串的长度),再加1, / struct pen p = (struct pen)malloc(sizeof(pen) + strlen(str) + 1); p->data= NULL; //设置p的长度为目标字符串的长度 p->len = strlen(str); / 将目标字符串拷贝到结构体对应的位置 此处为什么p+1之后指向的是pen结构体存储空间后的位置,而不是只加一呢? 因为此处的p+1偏移的是p指向类型大小的偏移量,什么意思呢?p指向的类型为pen类型的结构体, 而pen类型的结构体大小为 len(4字节)加上 data(8个字节),由于此处有内存对齐的情况, 所以实际上pen大小为 4 + 8 + 4(这个4为内存对齐的多余空间,如果再增加一个int类型的变量, pen的大小还是为16)=16字节 所以此处p+1向后偏移了16字节,通过下方地址打印可以详细看出 / strcpy((char)(p + 1), str); //int所占字节数,不同机器不同。一般64位为4字节 printf(“sizeof(int): %ld\n”, sizeof(int)); //上文已说明,16字节 printf(“sizeof(pen): %ld\n”, sizeof(pen)); //起始地址 printf(“start: %p\n\n”, (char)p); //上文已说明,偏移后的地址 printf("(p+1) : %p\n", (char)(p+1)); //偏移后,对应的字符串 printf("(char*)(p+1): %s\n\n", (char*)(p+1)); //结构体变量data的地址 printf("&(p->data): %p\n", &(p->data)); //数据,null,此处为空,故此变量已经被浪费。访问对应字符串数据需要(char *)(p+1) printf(“p->data: %s\n\n”, p->data);}二 柔性数组通过上文我们可以看到,data字段是一个被浪费的指针(8个字节)。并且我们想取到结构体下的字符串变量时需要(char *)(p+1)写这么一串东西,既不好看,也容易出错,那有没有可以直接用p->data取到字符串并且内存是连续的,而且又不浪费data字段呢, 柔性数组 就是用来干这个的。#include <stdio.h>#include <malloc.h>#include <string.h>typedef struct pen{ int len; char data[];//柔性数组}pen;int main(int argc, char **argv){ char str[] = “this is a new string”;//需要填入的字符串 struct pen p = (struct pen)malloc(sizeof(pen) + strlen(str) + 1); p->data= NULL; p->len = strlen(str); strcpy((char *)(p+1), str); printf(“pen->data: %s\n”, p->data);}C99使用不完整类型实现柔性数组成员,在C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组(flexible array)成员(也叫伸缩性数组成员),但结构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。柔性数组成员只作为一个符号地址存在,而且必须是结构体的最后一个成员,sizeof 返回的这种结构大小不包括柔性数组的内存 (此段话摘自:https://blog.csdn.net/ce123_z…) ...

February 19, 2019 · 1 min · jiezi

【C++】一种典型隐秘的多次delete的情况

本文介绍分析一种多次delete动态内存的情况。说是典型,是因为这个问题已经在我两个同事身上发生过;说是隐秘,是因为一旦发生问题,靠肉眼很难确定原因。预备知识不同于C语言通过malloc和free等函数实现动态内存的分配和释放,C++引入了new和delete运算符实现。基本的用法如下:int* p = new int;delete p;如果上述代码的p多次释放会出现什么情况呢?int* p = new int;delete p;delete p;很明显会引起程序崩溃,这是我本地执行的错误信息,错误提示也给出了double free的字样,告诉我们这可能是两次释放导致的问题。double free or corruption (fasttop): 0x0000000001029c20 ======= Backtrace: =========/lib/x86_64-linux-gnu/libc.so.6(+0x70bfb)[0x7f6469dbcbfb]/lib/x86_64-linux-gnu/libc.so.6(+0x76fc6)[0x7f6469dc2fc6]/lib/x86_64-linux-gnu/libc.so.6(+0x7780e)[0x7f6469dc380e]…这种情况有没有简单的规避方式吗?我看到好多人这么写:int p = new int;delete p;p = nullptr; //或 p = NULL;delete p;由于C++中delete空指针是不出错的,所以执行不会出错。基于工程上的考虑,delete一个指针后,要把指针赋值为空指针,借此提高代码健壮性。但这往往会掩盖问题,使得问题查找更难,比如上述问题,我们应该去分析为什么两次delete,而不是通过p = nullptr; 暂时掩盖问题。上面情况更好的解决方式是使用智能指针或RAII,尽量在高层代码里不混杂底层逻辑。在此不絮叨了,进入下一部分。问题将问题场景简化为以下的例子:class List {public: List() { count = 0; elements = nullptr; } //空列表 List(int num) : count(num), elements(new int[num]) {} //num个元素的列表 ~List() { count = 0; delete [] elements; }private: int *elements;//元素的首地址 int count; //元素的个数};void processList(List ls) {}int main() { List list(5); processList(list);}问题的根源出在C++默认的拷贝是“浅拷贝”,即只拷贝当前变量类型所占用的内存。下面按代码执行步骤分析:(1)List list(5);建立一个对象,该对象{count=5, elements=x}(2)传入processList由于是浅拷贝,此时ls变量的值为{count=5, elements=x},即与main中的list共用elements。(3)processList函数体结束由于ls对象即将超出作用域,编译器会调用ls的析构函数,此时ls变量的值为{count=0, elements=无效地址},由于list与ls共享elements,所以main中的elements变量的值为{count=5, elements=无效地址y}。(4)main函数体结束list也即将超出作用域,编译器调用list的析构函数,由于其elements已经被delete了,再释放一次会出现内存错误,导致程序终止。那怎么修正这个问题呢?只要将processList的ls改为引用传参即可,引用传参仅将使用权传给函数,所有权还属于原对象,所以不会执行析构函数。void processList(List& ls) {}当然,还有一种方式是改写重载赋值和拷贝构造函数,实现“深拷贝”,这样也能解决问题。标准库中的string和vector是较常用的类,所以本身实现了深拷贝,所以不会出现以上问题。在包含的元素较多时,需要考虑性能问题。C++的零成本抽象,带来性能优势的同时,一定也要细细考虑其带来的复杂成本。比如,GC 用爽的人很可能即会犯以上的错误,往往不是因为不知道,而是因为相关意识不强烈。所以,一定要小心,小心,再小心。请关注我的公众号哦。 ...

January 1, 2019 · 1 min · jiezi

一眼看穿????JS变量,作用域和内存问题

这篇文章将梳理下环境,作用域链,变量对象和活动对象,以及内存管理问题。基本类型和引用类型的值我们都知道JS中的数据类型有两大类,基本数据类型和引用数据类型,下面从三个方面来解剖他们①保存方式基本类型的值是指简单的数据段,引用类型的值是指那些可能由多个值构成的对象。基本类型按值访问可以直接操作保存在变量中实际的值引用类型按引用地址访问保存在内存中的对象,而JS不能不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,所以说在实际操作过程中操作的是对象的引用,而不是实际的对象。②复制变量值基本类型在复制变量值的时候,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。也就是说基础类型的值复制给新变量后,会在栈内存中开辟一个新的地址空间去存储值,原值和复制值参与任何操作都互不影响引用类型在复制变量值的时候,同样会在栈内存中开辟一个新的地址空间去存储值,只不过,引用类型复制的是指针,原值和复制值的指针指向同一堆内存中存储的值,也就是说着两个变量实际上将引用同一对象,因此改变其中一个变量,就会影响到另一个变量。③传递参数先了解一个基本原则,ECMAScript中所有函数的参数都是按值传递的,千万不能觉得在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的。根据这个原则,如果参数值是基本类型的,在函数内部修改值,并不会影响到函数外部的值,但如果是引用类型的,参数依旧是值传递,只不过传递的是栈内存的地址值,因此函数内部的修改会影响到函数外部的值。下面看一个????let obj_value = { a: 1, b: 2}function func(val) { val.a = 3 val.c = 6 console.log(val) // {a: 3, b: 2, c: 6}}console.log(obj_value) // {a: 1, b: 2}func(obj_value)console.log(obj_value) // {a: 3, b: 2, c: 6}下面????能证明引用类型的参数也是按值传递的function func(obj) { obj.a = 1 obj = {} obj.a = 2}let test = {}func(test)console.log(test.a) // 1上面的????,按照我们理解应该打印出a=2,但事与愿违,首先,test在函数func中新增了一个a属性并赋值为1,此时,obj中传递的是引用类型在栈内存中存储的地址值,也就是说函数内的obj复制的是test地址,他们两个共同指向一个对象,因此通过obj新增,修改删除操作都会反映到函数外部,接下来再看函数内的第二条语句,obj={},这就不得了了,这是重写,也就是说它会抹去obj原本存储的地址值,这就切断了test和obj共同指向一个对象这个联系,因此第三条语句,obj.a=2就是函数内部的事情了。所以总结一句话,引用类型的增删改操作与其关联所有对象都会受到波及和影响,重新就会切断自身与其余对象的联系检测类型typeof()(只适用于基本类型,不适用于对象)typeof函数可用于检测string,number,boolean,undefined,function还是symbol,但如果变量的值是引用类型或null,则typeof会返回object。ECMA-262规定任何在内部实现[[call]]方法的对象都应该在应用typeof操作符时返回"function"对于正则表达式类型的typeof检测,在IE和Firefox中会返回object,其余的返回function。let func = function() {}console.log(typeof (’’)) // stringconsole.log(typeof (1)) // numberconsole.log(typeof (true)) // booleanconsole.log(typeof (undefined)) // undefinedconsole.log(typeof ({})) // objectconsole.log(typeof (null)) // objectconsole.log(typeof (Symbol(’’))) //symbolconsole.log(typeof (func)) // functioninstanceof(只适用于对象,不适用于基本类型)instanceof操作符用于判断是什么类型的对象如果变量是给定引用类型的实例,那么instanceof操作符就会返回true根据规定,所有的引用类型的值都是Object的实例,因此在检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回true,instanceof操作符检测基本类型的值,该操作符始终会返回false,因为基本类型不是对象。执行环境及作用域执行环境也称为环境,它定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样,在web浏览器中,全局执行环境被认为是window对象,因此,在浏览器中,创建的所有全局变量和函数都是作为window对象的属性和方法。ECMAScript程序中执行流的控制机制每个函数都有各自的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。作用域链代码在环境中执行,就会创建变量对象的作用域链,作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在的环境的变量对象,如果这个环境是函数,则将其活动对象作为变量对象。什么是活动对象呢?活动对象实际就是变量对象在真正执行时的另一种形式。活动对象一开始只包含一个变量,即arguments对象。作用域中的下一个变量对象来自外部环境,再下一个变量对象来自下一个环境,层层嵌套,一直延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象环境的访问是沿着作用域链进行的,作用域链是单向的,即由里到外,内部环境可以访问外部环境,反之不行。变量对象和活动对象的概念变量对象(VO)变量对象是与执行上下文对应的概念,定义执行上下文中的所有变量,函数以及当前执行上下文函数的参数列表,也就是说变量对象定义着一个函数内定义的参数列表、内部变量和内部函数变量对象的内部顺序是参数列表->内部函数->内部变量变量对象的创建过程检查当前执行环境的参数列表,建立Arguments对象。检查当前执行环境上的function函数声明,每检查到一个函数声明,就在变量对象中以函数名建立一个属性,属性值则指向函数所在的内存地址。检查当前执行环境上的所有var变量声明,每检查到一个var声明,如果VO(变量对象)中已存在function属性名,则调过,不存在就在变量对象中以变量名建立一个属性,属性值为undefined。变量对象是在函数被调用,但是函数尚未执行的时刻被创建的,这个创建变量对象的过程实际就是函数内数据(函数参数,内部变量,内部函数)初始化的过程。活动对象(AO)未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。全局变量对象我们上面说的都是函数上下文中的变量对象,是根据执行上下文中的数据(参数、变量、函数)确定其内容的,全局上下文中的变量对象则有所不同。以浏览器为例,全局变量对象是window对象,全局上下文在执行前的初始化阶段,全局变量、函数都是被挂载倒window上的。执行上下文的生命周期延长作用域链执行环境的类型就两种——全局和局部(函数)延长作用域链的意思是在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。延长方法(以下两个语句都会在作用域链的前端添加一个变量对象):try-catch语句的catch块:会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。with语句:会指定的对象添加到作用域链中通过with语句延长作用域链function addLink() { let name = ‘george’ with(local) { var url = href + name // 此时通过with语句将local对象添加到addLink环境的头部,因此在addLink中就有权可以访问local对象的属性和方法 } return url }没有块级作用域JS只有函数作用域和全局作用域,没有块级作用域。声明变量var声明的变量会自动被添加到最接近的环境中在函数内部,最接近的环境就是函数的局部环境,在with语句中,最接近的环境是函数环境,初始化变量若没有通过var声明,该变量会自动被添加到全局环境。查询表示符当某个环境中为了读取和写入一个标识符时,必须通过搜索来确定标识符实际代表什么。搜索过程从作用域链的前端开始,沿着作用域链向上查找,一直追溯到全局环境变量对象,找到标识符,搜索过程停止,反之,返回undefined。垃圾收集JS具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存收集原理找出那些不再继续使用的变量,然后释放其占用的内存,为此垃圾收集器会按照固定的时间间隔(或代码中预定的收集时间),周期性地执行这一操作。收集方法标记清除引用计数标记清除(最常用的垃圾收集方式)原理:垃圾收集器在运行的时候回给存储在内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记,最后删除被标记的变量。标记清除算法将“不再使用的对象”定义为“无法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。引用计数原理:通过名字很好理解,引用计数,就是跟踪记录每个值被引用的次数,当引用次数为0时,将其删除。计数方法:当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1,如果同一个值被赋给另一个变量,则该值的引用次数加1,相反,包含这个值引用的变量又取的另一个值,则这个值的引用次数减1。引用计数的严重问题——循环引用循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。当出现循环引用的时候,引用次数永远不可能为0,这会导致内存得不到回收。解决方法:手动断开不需要的引用,即,将引用对象置为null立即执行垃圾回收函数IE中:window.CollectGarbage()Opera7或更高版本:window.opera.collect()管理内存分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少对于浏览器而言,确保占用最少的内存可以让页面获得更好的性能。优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据,一旦数据不再有用,最好通过将其值设置为null来释放其引用,这个做法叫做解除引用。这一做法适用于大多数全局变量和全局对象的属性,局部变量会在它们离开执行环境时自动被解除引用。 ...

December 9, 2018 · 1 min · jiezi