您好,登錄后才能下訂單哦!
今天小編給大家分享一下JavaScript中的內存管理方法是什么的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
JavaScript 數據類型分為 基本類型
與 引用類型
。
基本類型:在語言最低層且不可變的值稱為原始值。所有原始值都可以使用 typeof 運算符測試所屬基本類型(除了null,因為typeof null === "object")。所有原始值都有它們相應的對象包裝類(除了 null 和 undefined),這為原始值提供可用的方法。基本類型的對象包裝類有 Boolean、Number、String、Symbol。
引用類型:表示內存中的可變的值,JavaScript 中對象是唯一可變的。Object、Array、函數等都屬于對象。給對象定義屬性可通過 Object.defineProperty() 方法,讀取對象屬性信息可通過 Object.getOwnPropertyDescriptor()。
基本類型與引用類型可以互轉,轉換的行為稱為 裝箱
與 拆箱
。
裝箱:基本類型 => 引用類型 e.g: new String('call_me')
拆箱:引用類型 => 基本類型 e.g: new String('64').valueOf()、new String('64').toString()
以下是一些開發過程中常見的類型轉換:
number -> string: let a = 1 => a+"" / String(a)
string -> number: let a = "1" => +a / ~~a / Number(a)
any -> boolean: let a = {} => !a / !!a / Boolean(a)
從內存角度區分基本類型與應用類型,關鍵在于值在內存中是否可變,基本類型更新會重新開辟空間并改變指針地址,引用類型更新不會改變指針地址但會改變指針所指向的對象;從代碼上看,引用類型由基本類型和 {} 組成。
JavaScript 程序運行時 V8 會給程序分配內存,這種內存稱為 Resident Set(常駐內存集合)
,V8 常駐內存進一步細分成 Stack
和 Heap
。
Stack(棧) 是自動分配大小固定的內存空間,并由系統自動釋放。棧數據結構遵循先進后出的原則,線性有序存儲,容量小,系統分配效率高。
Heap(堆) 是動態分配大小不固定的內存空間,不會自動釋放(釋放依賴 GC)。堆數據結構是一棵二叉樹結構,容量大,速度慢。
一個線程只有一個棧內存空間,一個進程只有一個堆空間。
棧內存空間默認大小是 864KB
,也可通過 node --v8-options | grep -B0 -A1 stack-size
查看。
棧結構其實經常可以看到,當寫了一段報錯代碼時,控制臺的錯誤提示就是一個棧結構。從下往上看調用路徑,最頂部就是錯誤位置。例如最頂部拋出 Maxium call stack size exceeded 錯誤就代表當前調用超出了棧的限制。
堆中的結構劃分為 新空間(New Space)
、舊空間(Old Space)
、大型對象空間(Large object space)
、代碼空間(Code-space)
、單元空間(Cell Space)
、屬性單元空間(Property Cell Space)
和 映射空間(Map Space)
,新空間和舊空間在后面會詳細介紹。
大型對象空間(Large object space):大于其他空間大小限制的對象存放在這里。每個對象都有自己的內存區域,這里的對象不會被垃圾回收器移動。
代碼空間(Code-space):存儲已編譯的代碼塊,是唯一可執行的內存空間。
單元空間(Cell Space)、屬性單元空間(Property Cell Space)和映射空間(Map Space):這些空間分別存放 Cell,PropertyCell 和 Map。這些空間包含的對象大小相同,并且對對象類型有些限制,可以簡化回收工作。
每個空間(除了大型對象空間(Large object space))都由若干個 Page
組成。一個 page 是由操作系統分配的一個連續內存塊,一個內存塊大小為 1MB
。
從內存角度區分棧與堆,關鍵在于用完是否立即釋放。
相信讀者們看到這里肯定會聯想到數據類型與堆棧的關聯,網上和一些書籍的結論是:原始值分配在棧上,而對象分配在堆上。這個說法真的對嗎?帶著問題我們進入第二步:使用分配的內存。
Node 提供了 process.memoryUsage() 方法描述 Node.js 進程的內存使用情況(以字節 Bytes 為單位)
$ node > process.memoryUsage()
假設原始值分配在棧上,而對象分配在堆上是對的,結合棧空間只有 864KB
。如果我們聲明一個 10MB 的字符串,看看堆內存是否會發生變化。
const beforeMemeryUsed = process.memoryUsage().heapUsed / 1024 / 1024; const bigString = 'x'.repeat(10*1024*1024) // 10 MB console.log(bigString); // need to use the string otherwise the compiler would just optimize it into nothingness const afterMemeryUsed = process.memoryUsage().heapUsed / 1024 / 1024; console.log(`Before memory used: ${beforeMemeryUsed} MB`); // Before memory used: 3.7668304443359375 MB console.log(`After memory used: ${afterMemeryUsed} MB`); // After memory used: 13.8348388671875 MB
堆內存消耗接近 10 MB,說明字符串存儲在堆中。
那么小字符串以及其他基本類型是否同樣的存儲在堆中呢,我們借助谷歌瀏覽器的 Memery 堆快照(Heap snapshot)
進行分析。
打開谷歌瀏覽器無痕模式 Console 中輸入以下代碼,并分析執行前后的變量變化。
function testHeap() { const smi = 18; const heapNumber = 18.18; const nextHeapNumber = 18.18; const boolean = true; const muNull = null; const myUndefined = undefined; const symbol = Symbol("my-symbol"); const emptyString = ""; const string = "my-string"; const nextString = "my-string"; } testHeap()
從圖中可以看出函數執行后堆中變量分配情況。小數、字符串、symbol 都開辟了堆空間,說明分配在堆中。
有兩個相同的"my-string"字符串,但并沒有重復開辟兩個字符串空間,因為 v8 內部存在名為 stringTable 的 hashmap 緩存了所有字符串,在 V8 閱讀代碼并轉換為 AST 時,每遇到一個字符串都會換算為一個 hash 值插入到 hashmap 中。所以在我們創建字符串的時候,V8 會先從內存哈希表中查找是否有已經創建的完全一致的字符串,若存在,直接復用。若不存在,則開辟一塊新的內存空間存儲。這也是為什么字符串是不可變的,修改字符串時需要重新開辟新的空間而不能再原來的空間上作修改。
小整數、boolean、undefined、null、空字符串并沒有額外開辟空間,對這些數據類型有兩種猜測:
存放在棧空間中;
存放在堆中但在系統啟動時就已經開辟。
其實 V8 中有一個特殊的原始值子集,稱為 Oddball
。它們在運行之前由 V8 預先分配在堆上,無論 JavaScript 程序是否實際使用到它們。從整個堆空間查看這些類型的分配,boolean、undefined、null、空字符串分配在堆內存中且屬于 Oddball 類型。無論何時分配空間對應的內存地址永遠是固定的(空字符串@77
、null@71
、undefined@67
、true@73
)。但并未找到小整數,證明函數局部變量小整數存在棧中,但定義在全局中的小整數則是分配在堆中。
同樣都是表示 Number 類型,小整數和小數在存儲上有什么區別呢?
一般編程語言在區分 Number 類型時需要關心 Int、Float、32、64。在 JavaScript 中統稱為 Number,但 v8 內部對 Number 類型的實現可沒看起來這么簡單,在 V8 內部 Number 分為 smi
和 heapNumber
,分別用于存儲小整數與小數(包括大整數)。ECMAScript 標準約定 Number 需要被當成 64 位雙精度浮點數處理,但事實上一直使用 64 位去存儲任何數字在時間和空間上非常低效的,并且 smi 大量使用位運算,所以為了提高性能 JavaScript 引擎在存儲 smi 的時候使用 32 位去存儲數字而 heapNumber 使用 32 位或 64 位存儲數字。
以上是局部變量在函數中的內存分布,接下來驗證對象的內存分布。谷歌瀏覽器無痕模式 Console 中輸入以下代碼,并在 Class filter
中輸入 TestClass 查看其內存分布情況。
function TestClass() { this.number = 123; this.number2 = 123; this.heapNumebr = 123.18; this.heapNumber2 = 123.18; this.string = "abc"; this.string2 = "abc"; this.boolean = true; this.symbol = Symbol('test') this.undefined = undefined; this.null = null this.object = { name: 'pp' } this.array = [1, 2, 3]; } let testobject = new TestClass()
和上一個案例不同的是內存中多了 smi number 類型。由于對象本身就存儲在堆中,所以小整數也存儲在堆中。shallow size 大小為 0,證明了小整數雖在堆中卻不占內存空間。是什么原因導致小整數不占內存空間?
這和 V8 中使用 指針標記技術
有關,指針標記技術使得指針標記位可以存儲地址或者標記值。整數的值直接存儲在指針中,而不必為其分配額外的存儲空間;對象的值需要開辟額外內存空間,指針中存放其地址。這也導致了對象中的小整數數值相同地址也相同。
|------ 32位架構 -----| |_____address_____ w1| 指針 |___int31_value____ 0| Smi |---------------- 64位架構 ----------------| |________base________|_____offset______ w1| 指針 |--------------------|___int31_value____ 0| Smi
V8 使用最低有效位來區分 Smi 和對象指針。對于對象指針,它使用第二個最低有效位來區分強引用
和弱引用
。
在 32 位架構中 Smi 值只能攜帶 31 位有效載荷。包括符號位,Int32類型的范圍是 -(2^31) ~ 2^31 - 1, 所以Smi的范圍實際上是Int31類型的范圍(-(2^30) ~ 2^30 - 1)。對象指針有 30 位可用作堆對象地址有效負載。
由于單線程和 v8 垃圾回收機制的限制,內存越大回收的過程中 JavaScript 線程會阻塞且嚴重影響程序的性能和響應能力,出于性能以及避免空間浪費的考慮,大部分瀏覽器以及 Node15+ 的內存上限為 4G(4G 剛好是 2^32 byte)。以內存上限為 4G 為例,V8 中的堆布局需要保證無論是 64 位系統還是 32 位系統都只使用32位的空間來儲存。在 64 位架構中 Smi 同樣使用 31 位有效負載,與 32 位架構保持一致;對象指針使用 62 位有效負載,其中前 32 位表示 base(基址),其值指向 4G 內存中間段的地址。后 32 位的前 30 位表示 offset,指前后 2G 內存空間的偏移量。
v8 可以通過以下代碼查看內存上限。
const v8 = require('v8') console.log('heap_size_limit:',v8.getHeapStatistics().heap_size_limit) // 查詢堆內存上限設置,不同 node 版本默認設置是不一樣
通過設置環境 export NODE_OPTIONS=--max_old_space_size=8192
或者啟動時傳遞 --max-old-space-size
(或 --max-new-space-size
)參數修改內存上限。
通過以上兩個案例,細心的讀者可能已經發現 heap number 作為函數私有變量時存在復用但作為對象的屬性時不存在復用(地址不相同)。作者猜測函數中的私有變量做了類似字符串的 hashmap 優化,而作為對象屬性時為了避免每次修改變量重新開辟空間而導致內存消耗大,無論數值是否相同都會重新開辟空間,修改時直接修改指針所指向的具體值。
以執行函數為例簡單概括 JavaScript 的內存模型
使用完內存我們需要對內存進行釋放以及歸還,像 C 語言這樣的底層語言一般都有底層的堆內存管理接口,比如 malloc() 和 free()。相反,JavaScript 是在創建變量(對象,字符串等)時自動進行了分配內存,并且在不使用它們時"自動"釋放。釋放的過程稱為 垃圾回收
。釋放過程不是實時的,因為其開銷比較大,所以垃圾回收器會按照固定的時間間隔周期性的執行,這讓 JavaScript 開發者錯誤的認為可以不關心垃圾回收機制及策略。
這是最初級的垃圾收集算法。此算法把"對象是否不再需要"簡化定義為"對象有沒有其他對象引用到它"。假設有一個對象A,任何一個對象對A的引用,那么對象A的引用計數器+1,當引用清除時,對象A的引用計數器就-1,如果對象A的計算器的值為 0,就說明對象A沒有引用了,可以被回收。
但該算法有個限制:無法處理循環引用問題。在下面的例子中,兩個對象被創建,并互相引用,形成了一個循環。它們被調用之后會離開函數作用域,所以它們已經沒有用了,可以被回收了。然而,引用計數算法考慮到它們互相都有至少一次引用,所以它們不會被回收。
function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o return ""; } f();
這個算法把"對象是否不再需要"簡化定義為"對象是否可達",解決了循環引用的問題。這個算法假定設置一個叫做根(root)的對象(在 Javascript 里,根是全局對象)。垃圾回收器將定期從根開始,不具備可達性的元素將被回收。可達性指的是一個變量是否能夠直接或間接通過全局對象訪問到,如果可以那么該變量就是可達的,否則就是不可達。
但標記清除法對比引用計數法 缺乏時效性,只有在有效內存空間耗盡了,V8引擎將會停止應用程序的運行并開啟 GC 線程,然后開始進行標記工作。所以這種方式效率低,標記和清除都需要遍歷所有對象,并且在 GC 時,需要停止應用程序,對于交互性要求比較高的應用而言這個體驗是非常差的;通過標記清除算法清理出來的內容碎片化較為嚴重,因為被回收的對象可能存在于內存的各個角落,所以清理出來的內存是不連貫的。
標記壓縮算法是在標記清除算法的基礎之上,做了優化改進的算法。和標記清除算法一樣,也是從根節點開始,對對象的引用進行標記,在清理階段,并不是簡單的清理未標記的對象,而是將存活的對象壓縮到內存的一端,然后清理邊界以外的垃圾,從而解決了碎片化的問題。
標記壓縮算法解決了標記清除算法的碎片化的問題,同時,標記壓縮算法多了一步,對象移動內存位置的步驟,其效率也有一定的影響。
標記壓縮算法只解決了標記清除法的內存碎片化問題,但是沒有解決停頓問題。為了減少全停頓的時間,V8 使用了如下優化,改進后,最大停頓時間減少到原來的1/6。
增量 GC:GC 是在多個增量步驟中完成,而不是一步完成。
并發標記: 標記空間的對象哪些是活的哪些是死的是使用多個輔助線程并發進行,不影響 JavaScript 的主線程。
并發清掃/壓縮:清掃和壓縮也是在輔助線程中并發進行,不影響 JavaScript 的主線程。
延遲清掃:延遲刪除垃圾,直到有內存需求或者主線程空閑時再刪除。
JavaScript 中的 垃圾回收策略采用分代回收的思想
。Heap(堆)內存中只有新空間(New Space)和舊空間(Old Space)由 GC 管理。
新空間(New Space):新對象存活的地方,駐留在此處的對象稱為New Generation(新生代)。Minor GC 作為該空間的回收機制,該空間采用 Scavenge 算法 + 標記清除法
。
Minor GC 保持新空間的緊湊和干凈,其中有一個分配指針,每當我們想為新的對象分配內存空間時,就會遞增這個指針。當該指針達到新空間的末端時,就會觸發一次 Minor GC。這個過程也被稱為 Scavenger
,它實現了 Cheney 算法。由于空間很小(1-8MB 之間)導致 Minor GC 經常被觸發,所以這些對象的生命周期都很短,而且 Minor GC 過程使用并行的輔助線程,速度非常快,內存分配的成本很低。
新空間由兩個大小 Semi-Space 組成,為了區分二者 Minor GC 將二者命名為 from-space
和 to-space
。內存分配發生在 from-space 空間,當 from-space 空間被填滿時,就會觸發 Minor GC。將還存活著的對象遷移到 to-space 空間,并將 from-space 和 to-space 的名字交換一下,交換后所有的對象都在 from-space 空間,to-space 空間是空的。一段時間后 from-space 又被填滿時再次觸發 Minor GC,第二次存活的對象將會被遷移到舊空間(Old Space),第一次存活下來的新對象被遷移到 to-space 空間,如此周而復始操作就形成了 Minor GC 的過程。
舊空間(Old Space):在新空間(New Space)被兩次 Minor GC 后依舊存活的對象會被遷移到這里,駐留在此處的對象稱為Old Generation(老生代)。 Major GC 作為該空間的回收機制,該空間采用標記清除、標記壓縮、增量標記算法
。
V8 根據某種算法計算,確定沒有足夠的舊空間就會觸發 Major GC。Cheney 算法對于小數據量來說是完美的,但對于 Heap 中的舊空間來說是不切實際的,因為算法本身有內存開銷,所以 Major GC 使用標記清除、標記壓縮、增量標記算法。
舊空間分為舊址針空間和舊數據空間:舊指針空間包含具有指向其他對象的指針的對象;舊數據空間包含數據的對象(沒有指向其他對象的指針)。
并不是所有內存都會被回收,當程序運行時由于某種原因未能被 GC 而造成內存空間的浪費稱為 內存泄漏
。輕微的內存泄漏或許不太會對程序造成什么影響,嚴重的內存泄漏則會影響程序的性能,甚至導致程序的崩潰。
以下是一些導致內存泄漏的場景
var theThing = null; const replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("someMessage"); } }; // 如果在此處添加 `originalThing = null`,則不會導致內存泄漏。 }; setInterval(replaceThing, 1000);
這是一個非常經典的閉包內存泄漏案例,unused 中引用了 originalThing,所以強制它保持活動狀態,阻止了它的回收。unused 本身并未被使用所以函數執行結束后會被 gc 回收。但 somemethod 與 unused 在同一個上下文,共享閉包范圍。每次執行 replaceThing 時閉包函數 someMethod 中都會引用上一個 theThing 對象。
function foo(arg) { bar = "隱式全局變量"; } // 等同于: function foo(arg) { window.bar = "顯式全局變量"; }
定義大量的全局變量會導致內存泄漏。在瀏覽器中全局對象是“ window”。在 NodeJs 中全局對象是“global”或“process”。此處變量 bar 永遠無法被收集。
還有一種情況是使用 this 生成全局變量。
function fn () { this.bar = "全局變量"; // 這里的 this 的指向 window, 因此 bar 同樣會被掛載到 window 對象下 } fn();
避免此問題的辦法是在文件頭部或者函數的頂部加上 'use strict', 開啟嚴格模式使得 this 的指向為 undefined。
若必須使用全局變量存儲大量數據時,確保用完后設置為 null 即可。
setInterval/setTimeout 未被清除會導致內存泄漏。在執行 clearInterval/clearTimeout 之前,系統不會釋放 setInterval/setTimeout 回調函數中的變量,及時釋放內存就需要手動執行clearInterval/clearTimeout。
若 setTimeout 執行完成則沒有內存泄漏的問題,因為執行結束后就會立即釋放內存。
當組件掛載事件處理函數后,在組件銷毀時不主動將其清除,事件處理函數被認為是需要的而不會被 GC。如果內部引用的變量存儲了大量數據,可能會引起頁面占用內存過高,造成內存泄漏。
把 DOM 存儲在字典(JSON 鍵值對)或者數組中,當元素從 DOM 中刪除時,而 DOM 的引用還是存于內存中,則 DOM 的引用不會被 GC 回收而需要手動清除,所以存儲 DOM 通常使用弱引用的方式。
舊版瀏覽器 (IE6–7) 因無法處理 DOM 對象和 JavaScript 對象之間的循環引用而導致內存泄漏。
有時錯誤的瀏覽器擴展可能會導致內存泄漏。
若程序運行一段時間后慢慢變卡甚至崩潰,就要開始排查、定位以及修復內存泄漏,常用的內存泄漏排查方式有四種:
使用 Chrome 瀏覽器的 Performance
查看是否存在內存泄漏,使用 Memory
定位泄漏源。
使用 Node.js 提供的 process.memoryUsage 方法,查看 heapUsed
走勢;
使用node --inspect xxx.js
啟動服務并訪問chrome://inspect
,打開 Memory 定位泄漏源;
應用接入 grafana 的前提下,可通過 ab 壓測觀察 grafana 內存走勢。
內存分布對大部分開發者來說都是一個黑盒,v8 中實現的 JavaScript 內存模型非常復雜,99% 的開發都不用去關心,甚至在 ECMAScript 規范中也沒有找到任何關于內存布局的信息。若是興趣使然,完全可以看看 v8 引擎的源碼。在工作中如果你已經開始專研 JavaScript 內存分布的問題,說明你有能力開始編寫更底層的語言了。
以上就是“JavaScript中的內存管理方法是什么”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。