Heim >Web-Frontend >js-Tutorial >Detaillierte Erläuterung der Verwendung des Garbage Collectors

Detaillierte Erläuterung der Verwendung des Garbage Collectors

php中世界最好的语言
php中世界最好的语言Original
2018-05-22 09:33:471625Durchsuche

Dieses Mal erkläre ich Ihnen ausführlich, welche Vorsichtsmaßnahmen bei der Verwendung des Müllsammlers zu beachten sind. Schauen wir uns das hier an.

Der Müllsammler ist ein zweischneidiges Schwert. Der Vorteil besteht darin, dass der Speicherverwaltungscode des Programms erheblich vereinfacht werden kann, da für die Speicherverwaltung kein Programmierer erforderlich ist, wodurch Speicherlecks in Programmen mit langer Laufzeit reduziert (aber nicht beseitigt) werden. Für einige Programmierer kann es sogar die Leistung ihres Codes verbessern.

Andererseits bedeutet die Wahl eines Garbage Collectors, dass das Programm den Speicher nicht vollständig kontrollieren kann, und das ist der Kern der Entwicklung mobiler Endgeräte. Für

JavaScript gibt es keine Möglichkeit der Speicherverwaltung im Programm – der ECMAScript-Standard stellt keine Garbage-Collector-Schnittstelle zur Verfügung. Webanwendungen haben keine Möglichkeit, den Speicher zu verwalten oder den Garbage Collector aufzufordern.

Genau genommen sind Sprachen, die Garbage Collectors verwenden, nicht unbedingt leistungsstärker oder schlechter als Sprachen, die keine Garbage Collectors verwenden. In der

C-Sprache kann das Zuweisen und Freigeben von Speicher sehr kostspielige Vorgänge sein. Damit der zugewiesene Speicher in Zukunft freigegeben werden kann, wird die Heap-Verwaltung tendenziell kompliziert sein. In Sprachen mit verwaltetem Speicher wird durch die Speicherzuweisung oft nur ein Zeiger hinzugefügt. Aber dann werden wir die enormen Kosten sehen, die entstehen, wenn der Garbage Collector eingreift, um die Daten zu sammeln, wenn der Speicher erschöpft ist. Ein unerforschter Garbage Collector führt zu langen und unvorhersehbaren Pausen bei der Ausführung des Programms, was sich direkt auf das Erlebnis bei der Verwendung interaktiver Systeme (insbesondere solcher mit Animationseffekten) auswirkt. Referenzzählsysteme werden oft als Alternative zur Garbage Collection angepriesen, können aber auch unerwartete Pausen verursachen, wenn das letzte Objekt in einem großen Untergraphen dereferenziert wird. Darüber hinaus stellt das Referenzzählsystem auch eine erhebliche Leistungsbelastung dar, wenn es häufig Lese-, Neuschreib- und Speichervorgänge durchführt.

JavaScript braucht im Guten wie im Schlechten einen Garbage Collector. Die Garbage-Collector-Implementierung von V8 ist mittlerweile ausgereift und bietet hervorragende Leistung, kurze Pausen und eine sehr kontrollierbare Leistungsbelastung.

Grundlegende Konzepte

Das grundlegendste Problem, das der Garbage Collector lösen muss, besteht darin, den Speicher zu identifizieren, der recycelt werden muss. Sobald diese Speicherbereiche identifiziert sind, können sie bei zukünftigen Zuweisungen wiederverwendet oder an das Betriebssystem zurückgegeben werden. Ein Objekt ist tot (Unsinn), wenn es nicht aktiv ist. Ein Objekt ist genau dann aktiv, wenn auf es von einem Stammobjekt oder einem anderen aktiven Objekt verwiesen wird. Das Stammobjekt wird als aktives Objekt definiert und ist das Objekt, auf das der Browser oder V8 verweist. Beispielsweise gehören Objekte, auf die lokale Variablen verweisen, zum Stammobjekt, da ihr Stapel als Stammobjekt betrachtet wird. Globale Objekte gehören ebenfalls zum Stammobjekt, da auf sie immer zugegriffen werden kann, z. B. DOM-Elemente . Obwohl es sich in einigen Fällen nur um schwache Referenzen handelt.

Von außen betrachtet ist die obige Definition sehr locker. Tatsächlich können wir sagen, dass ein Objekt aktiv ist, wenn es von einem Programm referenziert werden kann. Zum Beispiel:

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
Während der Ausführung dieses Algorithmus behalten wir immer zwei Zeiger im Out-Bereich bei: „locationPtr“ zeigt auf die Stelle, an der wir gerade Speicher für ein neues Objekt zuweisen, und „scanPtr“ zeigt auf das nächste eine, bei der wir gerade eine aktive Objektprüfung durchführen. Die Objekte vor der Adresse, auf die scanPtr zeigt, sind verarbeitete Objekte. Sie und ihre Nachbarn befinden sich alle im Out-Bereich, und ihre Zeiger wurden aktualisiert. Die Objekte zwischen scanPtr und AllocationPtr werden in den Out-Bereich kopiert, jedoch innerhalb dieser Objekte Da die enthaltenen Zeiger auf Objekte in der Zone zeigen, werden diese Objekte in der Zone nicht kopiert. Logischerweise können Sie sich die Objekte zwischen scanPtr undallokationPtr als eine Warteschlange von Objekten vorstellen, die für die Breitensuche verwendet werden.

Anmerkung: Bei der Breitensuche wird der Knoten normalerweise aus dem Kopf der Warteschlange genommen und erweitert, und die erweiterten untergeordneten Knoten werden am Ende der Warteschlange gespeichert, und der Prozess beginnt immer wieder von vorne wieder. Dieser Vorgang ähnelt dem Aktualisieren eines Objekts zwischen zwei Zeigern.

Zu Beginn des Algorithmus kopieren wir alle Objekte im neuen Bereich, die vom Stammobjekt aus erreichbar sind, und treten dann in eine große Schleife ein. Bei jeder Iteration der Schleife entfernen wir ein Objekt aus der Warteschlange, erhöhen scanPtr und verfolgen dann den Zeiger, der auf das Objekt zugreift. Wenn der Zeiger nicht auf den Eingabebereich zeigt, ignorieren Sie ihn, da er auf den alten Bereich zeigen muss und dies nicht unser Ziel ist. Und wenn der Zeiger auf ein Objekt im Eingangsbereich zeigt, wir es aber nicht kopiert haben (die Weiterleitungsadresse wurde nicht festgelegt), dann kopieren Sie dieses Objekt in den Ausgangsbereich, also fügen Sie es hinzu Das Ende unserer Warteschlange und gleichzeitig das AllocationPtr-Inkrement. Zu diesem Zeitpunkt speichern wir auch eine Weiterleitungsadresse für das erste Wort des Out-Zone-Objekts und ersetzen den Kartenzeiger. Diese Weiterleitungsadresse ist die Adresse, an der das Objekt nach dem Kopieren gespeichert wird. Der Garbage Collector kann die Weiterleitungsadresse leicht vom Kartenzeiger unterscheiden, da der Kartenzeiger markiert ist, diese Adresse jedoch nicht. Wenn wir einen Zeiger finden und das Objekt, auf das er zeigt, kopiert wurde (die Weiterleitungsadresse wurde festgelegt), aktualisieren wir den Zeiger auf die Weiterleitungsadresse und markieren ihn.

Der Algorithmus endet, wenn alle Objekte verarbeitet wurden (d. h. scanPtr undallokationPtr treffen aufeinander). Zu diesem Zeitpunkt kann der in die Zone eingegebene Inhalt als Müll betrachtet werden und möglicherweise in Zukunft freigegeben oder wiederverwendet werden.

Geheimwaffe: Schreibbarriere

Es gibt ein Detail, das oben ignoriert wurde: Wenn sich ein Objekt im neuen Bereich befindet, gibt es nur einen Zeiger darauf. Und dieser Zeiger befindet sich zufällig unter den Objekten im alten Studentenbereich. Wie können wir wissen, welches Objekt im neuen Studentenbereich aktiv ist? Offensichtlich möchten wir den alten Rohbereich nicht noch einmal durchqueren, da sich im alten Rohbereich viele Objekte befinden und dies einmalig zu viel verbraucht.

Um dieses Problem zu lösen, gibt es tatsächlich eine Liste im Schreibpuffer, die alle Objekte im alten Bereich aufzeichnet, die auf den neuen Bereich verweisen. Wenn ein neues Objekt geboren wird, gibt es keinen Zeiger darauf. Wenn ein Objekt im alten Bereich einen Zeiger auf ein Objekt im neuen Bereich hat, zeichnen wir einen solchen bereichsübergreifenden Zeiger auf. Da dieses Protokollierungsverhalten immer während eines Schreibvorgangs auftritt, spricht man von einer Schreibbarriere, da jeder Schreibvorgang eine solche Barriere passieren muss.

Sie fragen sich vielleicht: Wenn jeder Schreibvorgang eine Schreibbarriere durchlaufen müsste, gäbe es dann nicht viel zusätzlichen Code? Ja, das ist einer der Kosten unseres Müllsammelmechanismus. Aber die Situation ist nicht so ernst, wie Sie denken. Schließlich gibt es weniger Schreibvorgänge als Lesevorgänge. Einige Garbage-Collection-Algorithmen (nicht V8) verwenden Lesebarrieren, die Hardwareunterstützung erfordern, um geringere Kosten zu gewährleisten. V8 verfügt außerdem über einige Optimierungen, um den durch Schreibbarrieren verursachten Verbrauch zu reduzieren:

Der Großteil der Skriptausführungszeit findet in Crankshaft statt, und Crankshaft kann oft statisch bestimmen, ob sich ein Objekt im neuen Bereich befindet. Für Schreibvorgänge auf diese Objekte ist keine Schreibbarriere erforderlich.

In Crankshaft gibt es eine neue Optimierung. Das heißt, wenn auf ein Objekt keine nicht-lokale Referenz verweist, wird das Objekt auf dem Stapel zugewiesen. Der Schreibvorgang, der sich auf ein Objekt auf dem Stapel bezieht, erfordert offensichtlich keine Schreibbarriere. (Übersetzung: Der neue Studentenbereich und der alte Studentenbereich liegen auf dem Haufen.)

Die Situation „alt → neu“ ist relativ selten, so dass durch die Kombination der beiden üblichen Situationen „neu → neu“ und „alt → alt“ Codeoptimierung kann in den meisten Situationen die Leistung relativ verbessern. Jede Seite ist auf 1 MB ausgerichtet, sodass anhand der Speicheradresse eines Objekts die Seite, auf der es sich befindet, durch Herausfiltern der unteren 20 Bits schnell gefunden werden kann und der Seitenkopf eine relevante Identifizierung aufweist, um anzuzeigen, ob es zum neuen Bereich gehört oder der alte Bereich, also durch Bestimmen des Bereichs, zu dem zwei Objekte gehören, kann auch schnell festgestellt werden, ob sie „alt → neu“ sind.

Sobald wir den Zeiger „alt → neu“ gefunden haben, können wir ihn am Ende des Schreibpuffers aufzeichnen. Nach einer gewissen Zeit (wenn der Schreibpuffer voll ist) sortieren wir es, führen identische Elemente zusammen und entfernen dann Zeiger, die nicht mehr zur Situation „alt → neu“ passen. (Anmerkung: Auf diese Weise wird die Anzahl der Zeiger reduziert und die Zeit zum Schreiben der Barriere entsprechend verkürzt)

„Mark-Sweep“-Algorithmus und „Mark-Compact“-Algorithmus

Der Scavenge-Algorithmus dient dem schnellen Recycling. Das Komprimieren kleiner Speicherabschnitte funktioniert gut, verbraucht jedoch zu viel für große Speicherabschnitte. Da der Scavenge-Algorithmus zwei Bereiche erfordert, den Ausgangsbereich und den Eingangsbereich, ist dies für kleine Speicherbereiche akzeptabel, wird jedoch für Speicher mit mehr als einigen MB unpraktisch. Der alte Studentenbereich enthält Hunderte von MB an Daten. Für einen so großen Bereich verwenden wir zwei andere Algorithmen, die relativ nahe beieinander liegen: den „Mark-Clear“-Algorithmus und den „Mark-Compact“-Algorithmus.

Beide Algorithmen bestehen aus zwei Phasen: Markierungsphase und Reinigungs- oder Verdichtungsphase.

在标记阶段,所有堆上的活跃对象都会被标记。每个页都会包含一个用来标记的位图,位图中的每一位对应页中的一字(译注:一个指针就是一字大小)。这个标记非常有必要,因为指针可能会在任何字对齐的地方出现。显然,这样的位图要占据一定的空间(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引入了两项改进来减少垃圾回收所引起的停顿,并且效果显著:增量标记和惰性清理。

Die inkrementelle Markierung ermöglicht die Markierung des Heaps in mehreren kleinen Pausen von 5–10 ms (mobile Geräte). Die inkrementelle Markierung wird aktiviert, wenn die Heap-Größe einen bestimmten Schwellenwert erreicht. Nach der Aktivierung wird die Ausführung des Skripts angehalten und eine inkrementelle Markierung durchgeführt. Genau wie die normale Markierung ist auch die inkrementelle Markierung eine Tiefensuche und verwendet den gleichen Weiß-Grau-Schwarz-Mechanismus zur Klassifizierung von Objekten.

Aber der Unterschied zwischen inkrementellen Markern und gewöhnlichen Markern besteht darin, dass sich die Diagrammbeziehung des Objekts ändern kann! Worauf wir besonders achten müssen, sind die neuen Zeiger, die vom schwarzen Objekt zum weißen Objekt zeigen. Denken Sie daran, dass ein schwarzes Objekt bedeutet, dass es vollständig vom Garbage Collector gescannt wurde und nicht erneut gescannt wird. Wenn daher ein Zeiger wie „Schwarz → Weiß“ erscheint, übersehen wir möglicherweise das weiße Objekt und behandeln es versehentlich als totes Objekt. (Anmerkung: Die verbleibenden weißen Objekte nach dem Markierungsvorgang gelten als tote Objekte.) Daher mussten wir die Schreibsperre erneut aktivieren. Jetzt zeichnet die Schreibbarriere nicht nur den Zeiger „alt → neu“ auf, sondern auch den Zeiger „schwarz → weiß“. Sobald ein solcher Zeiger gefunden wird, wird das schwarze Objekt in ein graues Objekt umgefärbt und wieder in die Deque eingefügt. Wenn der Algorithmus das Objekt herausnimmt, werden die darin enthaltenen Zeiger erneut gescannt, sodass aktive weiße Objekte nicht übersehen werden.

Nachdem die inkrementelle Markierung abgeschlossen ist, beginnt die verzögerte Bereinigung. Alle Objekte wurden verarbeitet, es ist also entweder tot oder lebendig, und es ist eine ausgemachte Sache, wie viel Platz auf dem Heap frei werden kann. Zum jetzigen Zeitpunkt können wir die Freigabe dieser Räume nicht überstürzen, und es ist keine große Sache, den Reinigungsprozess zu verzögern. Daher ist es nicht erforderlich, alle Seiten auf einmal zu bereinigen. Der Garbage Collector bereinigt sie nach Bedarf einzeln, bis alle Seiten bereinigt sind. Zu diesem Zeitpunkt ist die Inkrementalmarke wieder einsatzbereit.

Google hat kürzlich auch Unterstützung für die parallele Reinigung hinzugefügt. Da der Ausführungsthread des Skripts das tote Objekt nicht mehr berührt, kann die Seitenbereinigungsaufgabe mit minimalem Synchronisierungsaufwand in einem separaten Thread ausgeführt werden. An der gleichen Unterstützung wird für die parallele Markierung gearbeitet, befindet sich jedoch derzeit in einem frühen experimentellen Stadium.

Zusammenfassung

Die Müllabfuhr ist wirklich kompliziert. Ich habe viele Details im Artikel übersprungen und er ist trotzdem sehr lang geworden. Ein Kollege von mir sagte, dass er das Gefühl habe, dass die Arbeit am Garbage Collector beängstigender sei als die Registerzuweisung, und ich sagte, dass dies tatsächlich der Fall sei. Mit anderen Worten: Ich überlasse diese mühsamen Details lieber der Laufzeit, als sie allen Anwendungsentwicklern zu überlassen. Obwohl die Garbage Collection einige Leistungsprobleme und gelegentliche Seltsamkeiten mit sich bringt, befreit sie uns von vielen Details, sodass wir uns auf wichtigere Dinge konzentrieren können.

Ich glaube, dass Sie die Methode beherrschen, nachdem Sie den Fall in diesem Artikel gelesen haben. Weitere spannende Informationen finden Sie in anderen verwandten Artikeln auf der chinesischen PHP-Website!

Empfohlene Lektüre

Detaillierte Erläuterung des Anwendungsfalls der Vue+Toast-Popup-Komponente

Detaillierte Erläuterung des Schritte zum Ändern des integrierten Konfigurationspfads von Nodejs

Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der Verwendung des Garbage Collectors. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn