发布网友 发布时间:2022-04-23 05:53
共2个回答
懂视网 时间:2022-05-15 04:26
这次给大家带来怎样使用垃圾回收器,使用垃圾回收器的注意事项有哪些,下面就是实战案例,一起来看一下。垃圾回收器是一把十足的双刃剑。其好处是可以大幅简化程序的内存管理代码,因为内存管理无需程序员来操作,由此也减少了(但没有根除)长时间运转的程序的内存泄漏。对于某些程序员来说,它甚至能够提升代码的性能。
另一方面,选择垃圾回收器也就意味着程序当中无法完全掌控内存,而这正是移动终端开发的症结。对于JavaScript,程序中没有任何内存管理的可能——ECMAScript标准中没有暴露任何垃圾回收器的接口。网页应用既没有办法管理内存,也没办法给垃圾回收器进行提示。
严格来讲,使用垃圾回收器的语言在性能上并不一定比不使用垃圾回收器的语言好或者差。在C语言中,分配和释放内存有可能是非常昂贵的操作,为了使分配的内存能够在将来释放,堆的管理会趋于复杂。而在托管内存的语言中,分配内存往往只是增加一个指针。但随后我们就会看到,当内存耗尽时,垃圾回收器介入回收所产生的巨大代价。一个未经琢磨的垃圾回收器,会致使程序在运行中出现长时间、无法预期的停顿,这直接影响到交互系统(特别是带有动画效果的)在使用上的体验。引用计数系统时常被吹捧为垃圾回收机制的替代品,但当大型子图中的最后一个对象的引用解除后,同样也会有无法预期的停顿。而且引用计数系统在频繁执行读取、改写、存储操作时,也会有可观的性能负担。
或好或坏,JavaScript需要一个垃圾回收器。V8的垃圾回收器实现现在已经成熟,其性能优异,停顿短暂,性能负担也非常可控。
基本概念
垃圾回收器要解决的最基本问题就是,辨别需要回收的内存。一旦辨别完毕,这些内存区域即可在未来的分配中重用,或者是返还给操作系统。一个对象当它不是处于活跃状态的时候它就死了(废话)。一个对象处于活跃状态,当且仅当它被一个根对象或另一个活跃对象指向。根对象被定义为处于活跃状态,是浏览器或V8所引用的对象。比如说,被局部变量所指向的对象属于根对象,因为它们的栈被视为根对象;全局对象属于根对象,因为它们始终可被访问;浏览器对象,如DOM元素,也属于根对象,尽管在某些场合下它们只是弱引用。
从侧面来说,上面的定义非常宽松。实际上我们可以说,当一个对象可被程序引用时,它就是活跃的。比如:
function f() { var obj = {x: 12}; g(); // 可能包含一个死循环 return obj.x; }
def scavenge(): swap(fromSpace, toSpace) allocationPtr = toSpace.bottom scanPtr = toSpace.bottom for i = 0..len(roots): root = roots[i] if inFromSpace(root): rootCopy = copyObject(&allocationPtr, root) setForwardingAddress(root, rootCopy) roots[i] = rootCopy while scanPtr < allocationPtr: obj = object at scanPtr scanPtr += size(obj) n = sizeInWords(obj) for i = 0..n: if isPointer(obj[i]) and not inOldSpace(obj[i]): fromNeighbor = obj[i] if hasForwardingAddress(fromNeighbor): toNeighbor = getForwardingAddress(fromNeighbor) else: toNeighbor = copyObject(&allocationPtr, fromNeighbor) setForwardingAddress(fromNeighbor, toNeighbor) obj[i] = toNeighbor def copyObject(*allocationPtr, object): copy = *allocationPtr *allocationPtr += size(object) memcpy(copy, object, size(object)) return copy
在这个算法的执行过程中,我们始终维护两个出区中的指针:allocationPtr指向我们即将为新对象分配内存的地方,scanPtr指向我们即将进行活跃检查的下一个对象。scanPtr所指向地址之前的对象是处理过的对象,它们及其邻接都在出区,其指针都是更新过的,位于scanPtr和allocationPtr之间的对象,会被复制至出区,但这些对象内部所包含的指针如果指向入区中的对象,则这些入区中的对象不会被复制。逻辑上,你可以将scanPtr和allocationPtr之间的对象想象为一个广度优先搜索用到的对象队列。
译注:广度优先搜索中,通常会将节点从队列头部取出并展开,将展开得到的子节点存入队列末端,周而复始进行。这一过程与更新两个指针间对象的过程相似。
我们在算法的初始时,复制新区所有可从根对象达到的对象,之后进入一个大的循环。在循环的每一轮,我们都会从队列中删除一个对象,也就是对scanPtr增量,然后跟踪访问对象内部的指针。如果指针并不指向入区,则不管它,因为它必然指向老生区,而这就不是我们的目标了。而如果指针指向入区中某个对象,但我们还没有复制(未设置转发地址),则将这个对象复制至出区,即增加到我们队列的末端,同时也就是对allocationPtr增量。这时我们还会将一个转发地址存至出区对象的首字,替换掉Map指针。这个转发地址就是对象复制后所存放的地址。垃圾回收器可以轻易将转发地址与Map指针分清,因为Map指针经过了标记,而这个地址则未标记。如果我们发现一个指针,而其指向的对象已经复制过了(设置过转发地址),我们就把这个指针更新为转发地址,然后打上标记。
算法在所有对象都处理完毕时终止(即scanPtr和allocationPtr相遇)。这时入区的内容都可视为垃圾,可能会在未来释放或重用。
秘密武器:写屏障
上面有一个细节被忽略了:如果新生区中某个对象,只有一个指向它的指针,而这个指针恰好是在老生区的对象当中,我们如何才能知道新生区中那个对象是活跃的呢?显然我们并不希望将老生区再遍历一次,因为老生区中的对象很多,这样做一次消耗太大。
为了解决这个问题,实际上在写缓冲区中有一个列表,列表中记录了所有老生区对象指向新生区的情况。新对象诞生的时候,并不会有指向它的指针,而当有老生区中的对象出现指向新生区对象的指针时,我们便记录下来这样的跨区指向。由于这种记录行为总是发生在写操作时,它被称为写屏障——因为每个写操作都要经历这样一关。
你可能好奇,如果每次进行写操作都要经过写屏障,岂不是会多出大量的代码么?没错,这就是我们这种垃圾回收机制的代价之一。但情况没你想象的那么严重,写操作毕竟比读操作要少。某些垃圾回收算法(不是V8的)会采用读屏障,而这需要硬件来辅助才能保证一个较低的消耗。V8也有一些优化来降低写屏障带来的消耗:
大多数的脚本执行时间都是发生在Crankshaft当中的,而Crankshaft常常能静态地判断出某个对象是否处于新生区。对于指向这些对象的写操作,可以无需写屏障。
Crankshaft中新出现了一种优化,即在对象不存在指向它的非局部引用时,该对象会被分配在栈上。而一个栈上对象的相关写操作显然无需写屏障。(译注:新生区和老生区在堆上。)
“老→新”这样的情况相对较为少见,因此通过将“新→新”和“老→老”两种常见情况的代码做优化,可以相对提升多数情形下的性能。每个页都以1MB对齐,因此给定一个对象的内存地址,通过将低20bit滤除来快速定位其所在的页;而页头有相关的标识来表明其属于新生区还是老生区,因此通过判断两个对象所属的区域,也可以快速确定是否是“老→新”。
一旦我们找到“老→新”的指针,我们就可以将其记录在写缓冲区的末端。经过一定的时间(写缓冲区满的时候),我们将其排序,合并相同的项目,然后再除去已经不符合“老→新”这一情形的指针。(译注:这样指针的数目就会减少,写屏障的时间相应也会缩短)
“标记-清除”算法与“标记-紧缩”算法
Scavenge算法对于快速回收、紧缩小片内存效果很好,但对于大片内存则消耗过大。因为Scavenge算法需要出区和入区两个区域,这对于小片内存尚可,而对于超过数MB的内存就开始变得不切实际了。老生区包含有上百MB的数据,对于这么大的区域,我们采取另外两种相互较为接近的算法:“标记-清除”算法与“标记-紧缩”算法。
这两种算法都包括两个阶段:标记阶段,清除或紧缩阶段。
在标记阶段,所有堆上的活跃对象都会被标记。每个页都会包含一个用来标记的位图,位图中的每一位对应页中的一字(译注:一个指针就是一字大小)。这个标记非常有必要,因为指针可能会在任何字对齐的地方出现。显然,这样的位图要占据一定的空间(32位系统上占据3.1%,64位系统上占据1.6%),但所有的内存管理机制都需要这样占用,因此这种做法并不过分。除此之外,另有2位来表示标记对象的状态。由于对象至少有2字长,因此这些位不会重叠。状态一共有三种:如果一个对象的状态为白,那么它尚未被垃圾回收器发现;如果一个对象的状态为灰,那么它已被垃圾回收器发现,但它的邻接对象仍未全部处理完毕;如果一个对象的状态为黑,则它不仅被垃圾回收器发现,而且其所有邻接对象也都处理完毕。
如果将堆中的对象看作由指针相互联系的有向图,标记算法的核心实际是深度优先搜索。在标记的初期,位图是空的,所有对象也都是白的。从根可达的对象会被染色为灰色,并被放入标记用的一个单独分配的双端队列。标记阶段的每次循环,GC会将一个对象从双端队列中取出,染色为黑,然后将它的邻接对象染色为灰,并把邻接对象放入双端队列。这一过程在双端队列为空且所有对象都变黑时结束。特别大的对象,如长数组,可能会在处理时分片,以防溢出双端队列。如果双端队列溢出了,则对象仍然会被染为灰色,但不会再被放入队列(这样他们的邻接对象就没有机会再染色了)。因此当双端队列为空时,GC仍然需要扫描一次,确保所有的灰对象都成为了黑对象。对于未被染黑的灰对象,GC会将其再次放入队列,再度处理。
以下是标记算法的伪码:
markingDeque = [] overflow = false def markHeap(): for root in roots: mark(root) do: if overflow: overflow = false refillMarkingDeque() while !markingDeque.isEmpty(): obj = markingDeque.pop() setMarkBits(obj, BLACK) for neighbor in neighbors(obj): mark(neighbor) while overflow def mark(obj): if markBits(obj) == WHITE: setMarkBits(obj, GREY) if markingDeque.isFull(): overflow = true else: markingDeque.push(obj) def refillMarkingDeque(): for each obj on heap: if markBits(obj) == GREY: markingDeque.push(obj) if markingDeque.isFull(): overflow = true return
标记算法结束时,所有的活跃对象都被染为了黑色,而所有的死对象则仍是白的。这一结果正是清理和紧缩两个阶段所期望的。
标记算法执行完毕后,我们可以选择清理或是紧缩,这两个算法都可以收回内存,而且两者都作用于页级(注意,V8的内存页是1MB的连续内存块,与虚拟内存页不同)。
清理算法扫描连续存放的死对象,将其变为空闲空间,并将其添加到空闲内存链表中。每一页都包含数个空闲内存链表,其分别代表小内存区(<256字)、中内存区(<2048字)、大内存区(<16384字)和超大内存区(其它更大的内存)。清理算法非常简单,只需遍历页的位图,搜索连续的白对象。空闲内存链表大量被scavenge算法用于分配存活下来的活跃对象,但也被紧缩算法用于移动对象。有些类型的对象只能被分配在老生区,因此空闲内存链表也被它们使用。
紧缩算法会尝试将对象从碎片页(包含大量小空闲内存的页)中迁移整合在一起,来释放内存。这些对象会被迁移到另外的页上,因此也可能会新分配一些页。而迁出后的碎片页就可以返还给操作系统了。迁移整合的过程非常复杂,因此我只提及一些细节而不全面讲解。大概过程是这样的。对目标碎片页中的每个活跃对象,在空闲内存链表中分配一块其它页的区域,将该对象复制至新页,并在碎片页中的该对象上写上转发地址。迁出过程中,对象中的旧地址会被记录下来,这样在迁出结束后V8会遍历它所记录的地址,将其更新为新的地址。由于标记过程中也记录了不同页之间的指针,此时也会更新这些指针的指向。注意,如果一个页非常“活跃”,比如其中有过多需要记录的指针,则地址记录会跳过它,等到下一轮垃圾回收再进行处理。
增量标记与惰性清理
你应该想到了,当一个堆很大而且有很多活跃对象时,标记-清除和标记-紧缩算法会执行的很慢。起初我研究V8时,垃圾回收所引发的500-1000毫秒的停顿并不少见。这种情况显然很难接受,即使是对于移动设备。
2012年年中,Google引入了两项改进来减少垃圾回收所引起的停顿,并且效果显著:增量标记和惰性清理。
增量标记允许堆的标记发生在几次5-10毫秒(移动设备)的小停顿中。增量标记在堆的大小达到一定的阈值时启用,启用之后每当一定量的内存分配后,脚本的执行就会停顿并进行一次增量标记。就像普通的标记一样,增量标记也是一个深度优先搜索,并同样采用白灰黑机制来分类对象。
但增量标记和普通标记不同的是,对象的图谱关系可能发生变化!我们需要特别注意的是,那些从黑对象指向白对象的新指针。回忆一下,黑对象表示其已完全被垃圾回收器扫描,并不会再进行二次扫描。因此如果有“黑→白”这样的指针出现,我们就有可能将那个白对象漏掉,错当死对象处理掉。(译注:标记过程结束后剩余的白对象都被认为是死对象。)于是我们不得不再度启用写屏障。现在写屏障不仅记录“老→新”指针,同时还要记录“黑→白”指针。一旦发现这样的指针,黑对象会被重新染色为灰对象,重新放回到双端队列中。当算法将该对象取出时,其包含的指针会被重新扫描,这样活跃的白对象就不会漏掉。
增量标记完成后,惰性清理就开始了。所有的对象已被处理,因此非死即活,堆上多少空间可以变为空闲已经成为定局。此时我们可以不急着释放那些空间,而将清理的过程延迟一下也并无大碍。因此无需一次清理所有的页,垃圾回收器会视需要逐一进行清理,直到所有的页都清理完毕。这时增量标记又蓄势待发了。
Google近期还新增了并行清理支持。由于脚本的执行线程不会再触及死对象,页的清理任务可以放在另一个单独的线程中进行并只需极少的同步工作。同样的支持工作也正在并行标记上开展着,但目前还处于早期试验阶段。
总结
垃圾回收真的很复杂。我在文章中已经略过了大量的细节,而文章仍然变得很长。我一个同事说他觉得研究垃圾回收器比寄存器分配还要可怕,我表示确实如此。也就是说,我宁可将这些繁琐的细节交给运行时来处理,也不想将其交给所有的应用开发者来做。尽管垃圾回收存在一些性能问题而且偶尔会出现灵异现象,它还是将我们从大量的细节中解放了出来,以便让我们集中精力于更重要的事情上。
相信看了本文案例你已经掌握了方法,更多精彩请关注Gxl网其它相关文章!
推荐阅读:
如何使用nodeJs爬虫
如何使用node.js中render和send
热心网友 时间:2022-05-15 01:34
java回收器有gc
以下是工作原理:
强引用(StrongReference)
这个就不多说,我们写代码天天在用的就是强引用。如果一个对象被被人拥有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
Java 的对象是位于 heap 中的,heap 中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚。对于对象是属于哪种可及的对象,由他的最强的引用决定。如下
代码:
String abc=new String("abc"); //1
SoftReference<String> softRef=new SoftReference<String>(abc); //2
WeakReference<String> weakRef = new WeakReference<String>(abc); //3
abc=null; //4
softRef.clear();//5
第一行在 heap 堆中创建内容为“abc”的对象,并建立 abc 到该对象的强引用,该对象是强可及的。
第二行和第三行分别建立对 heap 中对象的软引用和弱引用,此时 heap 中的 abc 对象已经有 3 个引用,显然此时 abc 对象仍是强可及的。
第四行之后 heap 中对象不再是强可及的,变成软可及的。
第五行执行之后变成弱可及的。
软引用(SoftReference)
如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
软引用是主要用于内存敏感的高速缓存。在 jvm 报告内存不足之前会清除所有的软引用,这样以来 gc 就有可能收集软可及的对象,可能解决内存吃紧问题,避免内存溢出。什么时候会被收集取决于 gc 的算法和 gc 运行时可用内存的大小。当 gc 决定要收集软引用时执行以下过程,以上面的 softRef 为例:
1 首先将 softRef 的 referent(abc)设置为 null,不再引用 heap 中的 new String("abc")对象。
2 将 heap 中的 new String("abc")对象设置为可结束的(finalizable)。
3 当 heap 中的 new String("abc")对象的 finalize()方法被运行而且该对象占用的内存被释放, softRef
被添加到它的 ReferenceQueue(如果有的话)中。
注意:对 ReferenceQueue 软引用和弱引用可以有可无,但是虚引用必须有。
被 Soft Reference 指到的对象,即使没有任何 Direct Reference,也不会被清除。一直要到 JVM 内存
不足且没有 Direct Reference 时才会清除,SoftReference 是用来设计 object-cache 之用的。如此一来
SoftReference 不但可以把对象 cache 起来,也不会造成内存不足的错误 (OutOfMemoryError)。
弱引用(WeakReference)
如果一个对象只具有弱引用, 那该类就是可有可无的对象, 因为只要该对象被 gc 扫描到了随时都会把它干掉。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个
对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被
垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
建立虚引用之后通过 get 方法返回结果始终为 null,通过源代码你会发现,虚引用通向会把引用的对象写进
referent,只是 get 方法返回结果为 null。先看一下和 gc 交互的过程再说一下他的作用。
1 不把 referent 设置为 null, 直接把 heap 中的 new String("abc")对象设置为可结束的(finalizable)。
2 与软引用和弱引用不同, 先把 PhantomRefrence 对象添加到它的 ReferenceQueue 中.然后在释放虚可及的对象。