关于垃圾回收机制:Datenlord-垃圾回收机制与无锁化编程二

上一篇文章介绍了无锁化编程场景下的一种垃圾回收机制,Epoch-based Memory Reclaimation(EB)。 本篇介绍另一种无锁化编程场景下的垃圾回收机制,Hazard Pointer(HP)。HP也是一种确定型GC。 HP的内存回收办法比较简单: 对无锁化编程场景下的每个线程,须要显式标注出该线程要竞争拜访的共享对象,即线程把要竞争拜访的对象的指针标注为危险指针(Hazard Pointer), 拜访完结后或勾销标注该危险指针、或标注该危险指针指向的共享对象为待回收。 在回收内存时,HP判断每个待回收的共享对象是否能够平安回收,只须要查看该对象的指针是否正被某个线程标注为危险指针,如果没有就能回收,否则不能回收。 HP跟EB一样也是采纳空间换工夫的策略,并不是马上回收每个能够被回收的共享对象,而是批量回收,以缩小内存回收对程序性能的影响。 确定型GC算法Hazard Pointer(HP)持续沿用无锁化堆栈作为例子来展现HP的用法,而后再介绍HP的细节。 Hazard Pointer(HP)的用法采纳HP作为GC的无锁化堆栈的入栈和出栈操作实现如下所示。 struct Node { void* data; std::atomic< Node * > next;};std::atomic<Node *> top; // 栈顶top.store( nullptr ); // 初始化栈顶为空指针bool push( Node* new_node ) { // 线程本地的危险指针队列里新增一个危险指针 HP * hazard_cur_top = LocalHP::new_hp(); while ( true ) { Node * cur_top = top.load(); // 标注以后栈顶指针cur_top为危险指针 hazard_cur_top.set(cur_top); new_node->next.store( cur_top ); // CAS调用批改栈顶 if ( top.compare_exchange_weak( cur_top, new_node )) { break; // 批改栈顶胜利 } } // 入栈操作胜利,勾销标注cur_top为危险指针 hazard_cur_top.set(nullptr); return true;}Node * pop() { // 线程本地的危险指针队列里新增两个危险指针 HP * hazard_cur_top = LocalHP::new_hp(); HP * hazard_next = LocalHP::new_hp(); while ( true ) { Node * cur_top = top.load(); if ( cur_top == nullptr ) { break; // 堆栈为空 } // 标注以后栈顶指针cur_top为危险指针 hazard_cur_top.set(cur_top); Node * next = cur_top->next.load(); // 标注以后栈顶的下一节点指针next为危险指针 hazard_next.set(cur_top); // CAS调用批改栈顶 if ( top.compare_exchange_weak( cur_top, next )) { break; // 批改栈顶胜利 } } if ( cur_top != nullptr ) { // 出栈操作胜利,标注cur_top为待回收对象 hazard_cur_top.maybe_reclaim(); } else { // 栈为空,勾销标注cur_top为危险指针 hazard_cur_top.set(nullptr); } // 出栈操作完结,勾销标注next为危险指针 hazard_next.set(nullptr); return cur_top;}下面的实现能够看出,入栈操作和出栈操作有不同的危险指针。对于入栈操作: ...

July 21, 2022 · 1 min · jiezi

关于垃圾回收机制:JavaScript垃圾回收机制

前言咱们晓得,JavaScript中的变量次要分为两种类型:根本类型和援用类型。根本类型的值存储在栈(stack)内存中,而援用类型值的存储须要用到栈内存和堆(heap)内存,栈内存保留着变量的堆内存地址,地址指向的堆内存空间保留着具体的值。栈中变量的值在应用完后会被立刻回收,而堆中变量的值不会立刻回收,须要手动回收或应用某种策略进行回收。 JavaScript具备主动垃圾回收机制,不须要像C/C++语言那样须要开发者手动跟踪内存的应用状况。原理很简略:找出那些不再持续应用的变量,而后开释其占用的内存。为此,垃圾收集器会依照固定的工夫距离(或代码执行中预约的收集工夫),周期性地执行这一操作。 那么,怎么判断哪些变量有用哪些变量没用呢?以函数来说,函数中的局部变量只在函数执行过程中存在,在这个过程中,会为局部变量在栈或堆内存上调配相应的空间,以便存储它们的值。而后在函数中应用这些变量,直到函数执行完结。这时,局部变量就没有存在的必要了,因而能够开释它们的内存以供未来应用。这种状况下很容易判断变量是否有还有存在的必要;但并非所有状况下(如闭包)都这么容易就能得出结论。垃圾收集器必须跟踪有用或无用的变量,对不再有用的变量打上标记,以备未来发出其占用的内存。用于标识无用变量的策略可能会因实现而不同,但具体到浏览器中但实现,通常有两个策略:“标记革除”和“援用计数” 注:以上内容摘自《JavaScript高级程序设计(第3版)》 标记革除“标记革除”算法是目前宽泛应用最宽泛的垃圾收集算法,它的策略是,用某种标记办法将有用变量和无用变量进行辨别,做好标记后,下一次垃圾收集器工作的时候,就将标记为“垃圾”的变量进行回收,开释其所占内存。 垃圾收集器如何给变量打上标记呢?要了解它的工作形式,咱们要了解一个JavaScript内存治理的重要的概念:可达性。 可达性所谓“可达性”,是指变量能够由“根”登程,通过一层或多层能够被拜访到。如果一个变量从根登程能够被拜访到,那么它就是“可达”的。垃圾回收器将可达的变量视为有用的变量,将那些不可达的变量视为无用的变量,并给无用的变量打上“垃圾”的标记,便于之后的回收操作。 在JavaScript中,有一组根本的固有可达值,因为不言而喻的起因无奈删除。例如: 本地函数的局部变量和参数以后嵌套调用链上的其余函数的变量和参数全局变量还有一些其余外部的值咱们以一段代码为例: function marry(man,woman){ man.wife = woman woman.husban = man}var man = { name:'Tom'}var woman = { name:'Mary'}var family = marry(man,woman)这段代码在浏览器中运行时,内存示意如下:   能够看到,man、woman、family这三个全局变量都挂在到了window对象上,依据咱们对“可达性”的定义,这些变量的值都能够从window登程被拜访到,它们都是“可达”的,垃圾回收器不会对它们进行回收。 当初,咱们尝试让垃圾回收器回收man指向的对象 man = null这时候的内存图变为 能够看到,此时之前创立的man对象的值仍然能够通过window.woman.husband或window.family.father拜访到,因而这个对象仍被视为有用变量,不会被回收。接下来咱们“切断”所有拜访门路: delete woman.husband delete family.father 此时内存图变为: 此时,因为man对象曾经无奈从“根”登程拜访到了,因而它未来要被垃圾回收器当成“垃圾”回收。 为了加深了解,咱们换一种操作,间接从根开始切断它们的拜访门路: man = null woman = null family = null此时很容易得出内存图示: 如图所见,尽管之前定义的man、woman、family相互之间还有关联(援用),但它们曾经无奈从根登程拜访到了,成为了一座“孤岛”。垃圾回收器在运行的时候会将它们标记为“不可达”变量或“垃圾”,在回收的时候将它们“捡起来”。 这便是“可达性”。 说完了“可达性”,咱们来说说垃圾回收器是如何进行标记的。如上文所述,标记只是一种策略,如何标记要看各浏览器具体的实现。 上面咱们以《JavaScript高级程序设计》中的一段话为例: 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,能够应用任何标记形式)。而后,它会去掉环境中的变量以及被环境中的变量援用的变量的标记。而在此之后再被加上标记的变量将被视为筹备删除的变量,起因是环境中的变量曾经无法访问到这些变量来。最初,垃圾收集器实现内存革除工作,销毁那些带标记的值并回收它们所占用的内存空间。 标记革除算法分为两个阶段:标记阶段和革除阶段。 上文援用这段话咱们能够这么了解:在标记阶段,垃圾收集器先给内存中的所有变量都打上一个的标记,接着,从所有的“根”登程,沿着援用链路把沿途的所有变量的标记勾销;而那些无奈被拜访到的变量身上仍带有标记,这些变量就是之后要回收的“垃圾”,咱们将它们标记为“垃圾”;在革除阶段,垃圾回收器将那些被标记为“垃圾”的变量收集起来,在适当的机会将它们销毁,并回收它们占用的内存空间。 以上文彻底删除man为例形容这一过程。 首先,给内存中所有变量打上标记(途中黄色局部): 接着,沿着根登程,将沿途可拜访到的变量的标记去掉: 这时候能够很容易辨认到,之前man指向的对象是“垃圾”,咱们给他打个更显眼的标记: 这样垃圾回收器就很容易地找到这个“垃圾”将它回收了。 ...

February 11, 2021 · 1 min · jiezi

关于垃圾回收机制:我是这样跟面试官讲垃圾回收的

垃圾回收机制是什么?咱们为什么要学习垃圾回收机制?明天咱们就带着这两个问题一起来看看。在咱们日常的开发过程中,并不会过多的关注对象的回收和开释,JVM就能够帮忙咱们来实现垃圾,缩小了咱们很多的工作量,好像垃圾回收离咱们很远,其实垃圾回收机制是咱们从高级到中高级开发必须把握的。把回收对象的工作齐全交给JVM,看似解放了,其实也减少了不确定性,事件并不是什么时候都是完满的,在现如今各种简单业务场景下,不适合的垃圾回收算法及策略,往往是导致咱们零碎性能瓶颈的次要起因。 垃圾回收也不能一概而论,不同的业务场景采取不同的措施,如果业务场景对内存的要求比拟高,就须要进步对象的回收效率,如果是CPU使用率高,这个时候就要升高垃圾回收频率。 咱们都晓得,JVM的内存中有多个区域,垃圾回收次要是看堆和办法区的内存,因为其余区域如程序计数器、虚拟机栈和本地办法栈等区域的内存具备确定性,所以咱们要把眼光次要放在堆中的对象回收和办法区的废除常量的回收。 JVM如何判断一个对象能够回收的?最开始接触垃圾回收的时候,应该都听过,对象没有被援用的时候就能够被回收,然而怎么判断对象是否被援用,次要有两种形式:援用计数算法和可达性剖析算法。 援用计数算法:所谓的援用计数算法,就是通过一个对象的援用计数器来判断该对象是否被援用,对象被援用的时候,计数器就加1,援用生效计数器就减1。计数器的值为0 的时候就阐明这个对象没有被援用了,能够被JVM回收了。须要留神的是,援用计数算法尽管实现形式简略,然而会呈现循环援用的问题。 可达性剖析算法:可达性剖析算法的根底是GC Roots,是所有对象的跟对象,在JVM加载时,会创立一些对象援用失常对象,这些对象作为这些失常对象的起始点,在垃圾回收时,JVM会从GC Roots开始向下搜寻,如果一个对象到GC  Roots没有任何援用链相连时,就证实这个对象能够回收了。 垃圾回收线程是如何回收对象的?JVM去回收对象次要听从两个个性:自动性、不可预期性。 自动性:JVM会创立一个零碎级的线程来跟踪每一块被调配进来的内存,在JVM闲暇时,就会主动的查看每一块调配进来的内存空间,而后主动回收每一块内存。 不可预期性:不可预期性次要是一个对象没有被援用的时候,是立马就被回收的吗,这个答案是未知的,有可能立马就被回收,有可能隔了很久仍然在内存中。 GC算法JVM给咱们提供了多种回收算法来实现回收机制,一般来说,市面上常见的垃圾收集器的回收算法次要分为四类: 标记-革除算法(Mark-Sweep)长处:不须要挪动对象,简略高效 确定:标记-革除的过程效率低,会产生内存碎片。 复制算法(Copying)长处:简略高效,不会产生内存碎片 毛病:内存使用率低,还有可能产生频繁复制的问题。 标记-整顿算法(Mark-Compact)长处:不须要挪动对象,效率高,不产生内存碎片 毛病:须要挪动部分对象 分代收集算法(Gennerational Collection)长处:分区回收 毛病:对于长期存活对象的回收成果不太好。 理解了四种垃圾收集器的回收算法之后,咱们再来看看基于这些算法实现的回收器,简略介绍几种常见的: 掂量GC性能的规范?垃圾收集器各种各样的,不同的场景实用不同的回收器,如何筛选适合的垃圾收集器,次要取决于垃圾收集器的三个指标:吞吐量、卡顿工夫、垃圾回收频率。 吞吐量:指零碎应用程序破费的工夫和零碎运行总时长的比值,GC 的吞吐量=GC耗时/零碎总运行工夫。GC的吞吐量个别不低于95%。 卡顿工夫:卡顿工夫是垃圾收集器在工作的时候,应用程序暂停的工夫。个别串行收集器的卡顿工夫较长,并发收集器的卡顿工夫因为收集器和应用程序交替运行,所以卡顿工夫会比拟短,然而效率不如串行的,零碎吞吐量会有所降落。 垃圾回收频率:垃圾回收频率工夫和卡顿工夫是相互影响的,咱们能够通过增大内存的形式来升高垃圾回收产生的频率,然而内存增大后,沉积的对象就更多,当垃圾回收时,卡顿的工夫就会减少。所以咱们要把握减少内存的这个度,来保障失常的垃圾回收频率即可。 如何查看并剖析GC日志?前边废话这么多,预计很多大兄弟都看烦了,接下来咱们来看看如何收集GC日志,并剖析GC日志,咱们须要JVM参数来设置GC日志,须要关注以下几个参数: -XX:+PrintGC  #输入GC日志-XX:+PrintGCDetails #输入GC的具体日志-XX:+PrintGCTimeStamps #输入GC的工夫戳(以基准工夫的模式)-XX:+PrintGCDateStamps #输入GC的工夫戳(以日期的模式,如 2020-12-08T23:59:59.234+0800)-XX:+PrintHeapAtGC #在进行GC的前后打印出堆的信息-Xloggc:../logs/gc.log #日志文件的输入门路咱们按需配置参数即可,打印后的日志,例如下图:很短时间的GC日志咱们能够用记事本关上去查看,如果是剖析长时间的GC日志,再用记事本关上去看就有点艰难,咱们就须要借助工具来剖析,个别省事的能够用GCViewer来关上日志文件,就能够图形化的查看GC性能。通过工具咱们能够看到吞吐量、卡顿工夫、GC频率,很直观的查看GC的性能状况。 GCeasy也是一个更好用的GC日志剖析工具,只须要把日志文件压缩一下,上传官网就能够在线剖析,下边是我应用一个本地的GC日志剖析的后果: GC调优上边通过剖析GC日志,找出影响性能的问题,接下来就该有针对性的调优了,简略介绍几种罕用的调优策略,次要是升高Minor GC和Full GCd 频率。 升高Minor GC频率咱们首先来看,Minor GC次要是针对Eden区的对象回收,因为新生代空间个别比拟小,Eden区很块就会满,就会导致Minor GC的频率比拟高,咱们的解决办法通常是增大新生代空间来升高Minor GC的频率。在前边讲掂量GC性能指标的时候,咱们提到增大内存会减少回收时候的卡顿工夫。Minor GC也会导致应用程序的卡顿,只是工夫十分短暂,那么扩充Eden区会不会导致Minor GC的工夫增长,还得深刻看一下一次Minor GC产生了什么。 每次Minor GC次要做了两件事,扫描新生代(A)和复制存活对象(B)。其中复制对象的耗时是远高于扫描对象的。咱们举个例子,如果一个对象在Eden区域存活500ms,Minor GC的频率是300ms一次,失常状况下,在一次Minor GC中用时就说A+B的工夫,这个时候咱们通过gc日志剖析,把Eden扩容,变成了600ms才进行一次Minor GC,此时这个对象在Eden区中曾经被回收,就不必复制对象了,就省去了复制存活对象的工夫,在这一次Minor GC中只是减少了扫描新生代的工夫。 总结:单次 Minor GC 工夫更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。如果堆内存中存活工夫比拟长的对象多,减少年老代的空间,单次Minor GC的工夫反而会减少,如果是堆内存中短期对象多,那么扩容后,单词Minor GC的工夫不会显著的减少,还升高了Minor GC频率。升高Full GC频率Full GC的触发通常是因为堆内存空间有余或者老年代对象太多造成的,Full GC又会带来上下文切换,前边的文章咱们曾经专门介绍过上下文切换,都晓得上下文切换会升高零碎的性能。咱们能够通过下边几个方向来升高Full GC的频率。 ...

December 11, 2020 · 1 min · jiezi

关于垃圾回收机制:深入理解Chrome-V8垃圾回收机制

最近,我的项目进入保护期,根本没有什么需要,比拟闲,这让我莫名的有了危机感,每天像是在混日子,感觉这像是在温水煮青蛙,曾经毕业3年了,很怕本人到了5年教训的时候,能力却和3年教训的时候一样,没什么出息。于是开始整顿本人的技术点,刚好查漏补缺,在收藏夹在翻出了一篇文章一名【合格】前端工程师的自检清单,看到了外面的两个问题: JavaScript中的变量在内存中的具体存储模式是什么?浏览器的垃圾回收机制,如何防止内存透露?而后各种查资料,就整顿了这篇文章。 浏览本文之后,你能够理解到: JavaScript的内存是怎么治理的?Chrome是如何进行垃圾回收的?Chrome对垃圾回收进行了哪些优化?原文地址 欢送star JavaScript的内存治理不论什么程序语言,内存生命周期根本是统一的: 调配你所须要的内存应用调配到的内存(读、写)不须要时将其开释偿还与其余须要手动治理内存的语言不通,在JavaScript中,当咱们创立变量(对象,字符串等)的时候,零碎会主动给对象调配对应的内存。 var n = 123; // 给数值变量分配内存var s = "azerty"; // 给字符串分配内存var o = { a: 1, b: null}; // 给对象及其蕴含的值分配内存// 给数组及其蕴含的值分配内存(就像对象一样)var a = [1, null, "abra"]; function f(a){ return a + 2;} // 给函数(可调用的对象)分配内存// 函数表达式也能调配一个对象someElement.addEventListener('click', function(){ someElement.style.backgroundColor = 'blue';}, false);当零碎发现这些变量不再被应用的时候,会主动开释(垃圾回收)这些变量的内存,开发者不必过多的关怀内存问题。 尽管这样,咱们开发过程中也须要理解JavaScript的内存管理机制,这样能力防止一些不必要的问题,比方上面代码: {}=={} // false[]==[] // false''=='' // true在JavaScript中,数据类型分为两类,简略类型和援用类型,对于简略类型,内存是保留在栈(stack)空间中,简单数据类型,内存是保留在堆(heap)空间中。 根本类型:这些类型在内存中别离占有固定大小的空间,他们的值保留在栈空间,咱们通过按值来拜访的援用类型:援用类型,值大小不固定,栈内存中寄存地址指向堆内存中的对象。是按援用拜访的。而对于栈的内存空间,只保留简略数据类型的内存,由操作系统主动调配和主动开释。而堆空间中的内存,因为大小不固定,零碎无奈无奈进行主动开释,这个时候就须要JS引擎来手动的开释这些内存。 为什么须要垃圾回收在Chrome中,v8被限度了内存的应用(64位约1.4G/1464MB , 32位约0.7G/732MB),为什么要限度呢? 表层起因是,V8最后为浏览器而设计,不太可能遇到用大量内存的场景深层起因是,V8的垃圾回收机制的限度(如果清理大量的内存垃圾是很耗时间,这样回引起JavaScript线程暂停执行的工夫,那么性能和利用直线降落)后面说到栈内的内存,操作系统会主动进行内存调配和内存开释,而堆中的内存,由JS引擎(如Chrome的V8)手动进行开释,当咱们代码的依照正确的写法时,会使得JS引擎的垃圾回收机制无奈正确的对内存进行开释(内存泄露),从而使得浏览器占用的内存一直减少,进而导致JavaScript和利用、操作系统性能降落。 Chrome 垃圾回收算法在JavaScript中,其实绝大多数的对象存活周期都很短,大部分在通过一次的垃圾回收之后,内存就会被开释掉,而少部分的对象存活周期将会很长,始终是沉闷的对象,不须要被回收。为了进步回收效率,V8 将堆分为两类新生代和老生代,新生代中寄存的是生存工夫短的对象,老生代中寄存的生存工夫久的对象。 新生区通常只反对 1~8M 的容量,而老生区反对的容量就大很多了。对于这两块区域,V8 别离应用两个不同的垃圾回收器,以便更高效地施行垃圾回收。 ...

September 26, 2020 · 1 min · jiezi