最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

JavaScript的內(nèi)存管理

2020-11-09 18:35 作者:廣州藍(lán)景  | 我要投稿
為什么要關(guān)注內(nèi)存
如果我們有內(nèi)存溢出,程序占用的內(nèi)存會越來越大,最終引起客戶端卡頓,甚至無響應(yīng)。如果我們使用Node.js做后端應(yīng)用,因為后端程序會長時間運行,如果有內(nèi)存溢出,造成的后果會更嚴(yán)重,服務(wù)器內(nèi)存可能會很快就消耗光,應(yīng)用不能正常運行。

JS數(shù)據(jù)類型與JS內(nèi)存機制

JS有如下數(shù)據(jù)類型

原始數(shù)據(jù)類型:String, Number, Boolean, Null, Undefined, Symbol

引用數(shù)據(jù)類型:Object(對象,數(shù)組,函數(shù))

而存放這些數(shù)據(jù)的內(nèi)存又可以分為兩部分:棧內(nèi)存(Stack)和堆內(nèi)存(Heap)。

原始數(shù)據(jù)類型(基本類型)的值直接存在棧內(nèi)存中,值與值之間是獨立存在,修改一個變量的值不會影響其他變量的值

對象類型(引用類型)存在堆內(nèi)存中,每創(chuàng)建一個對象,就會在堆內(nèi)存中開辟出一個新的空間,而變量保存的對象的內(nèi)存地址(對象的引用,如果兩個變量保存的同一個對象,當(dāng)一個變量改變屬性值時,另一個變量的屬性值也會變 )

棧內(nèi)存

棧是一種只能一端進(jìn)出的數(shù)據(jù)結(jié)構(gòu),先進(jìn)后出,后進(jìn)先出。假如我們有如下三個變量:

var a = 10;

var b = 'hello';

var c = true;

根據(jù)我們的定義順序,a會首先入棧,然后是b,最后是c。最終結(jié)構(gòu)圖如下所示:


我們定義一個變量是按照如下順序進(jìn)行的,以var a = 10; 為例,我們先將10放入內(nèi)存,然后申明一個變量a,這時候a的值是undefined,最后進(jìn)行賦值,就是將a與10關(guān)聯(lián)起來。



從一個棧刪除元素就是出棧,從棧頂刪除,他相鄰的元素成為新的棧頂元素。



堆內(nèi)存

JS中原始數(shù)據(jù)類型的內(nèi)存大小是固定的,由系統(tǒng)自動分配內(nèi)存。

但是引用數(shù)據(jù)類型,比如Object, Array,他們的大小不是固定的,所以是存在堆內(nèi)存的。

JS不允許直接操作堆內(nèi)存,我們在操作對象時,操作的實際是對象的引用,而不是實際的對象。

可以理解為對象在棧里面存了一個內(nèi)存地址,這個地址指向了堆里面實際的對象。所以引用類型的值是一個指向堆內(nèi)存的引用地址。



函數(shù)也是引用類型,當(dāng)我們定義一個函數(shù)時,會在堆內(nèi)存中開辟一塊內(nèi)存空間,將函數(shù)體代碼以字符串的形式存進(jìn)去。然后將這塊內(nèi)存的地址賦值給函數(shù)名,函數(shù)名和引用地址會存在棧上。



可以在Chrome調(diào)試工具中嘗試一下,定義一個方法,然后不加括號調(diào)用,直接輸出函數(shù),可以看到,打印出來的是函數(shù)體字符串:


垃圾回收

垃圾回收就是找出那些不再繼續(xù)使用的變量,然后釋放其占用的內(nèi)存,垃圾回收器會按照固定的時間間隔周期性執(zhí)行這一操作。JS使用垃圾回收機制來自動管理內(nèi)存,但是他是一把雙刃劍:

優(yōu)勢:?可以大幅簡化程序的內(nèi)存管理代碼,降低程序員負(fù)擔(dān),減少因為長時間運行而帶來的內(nèi)存泄漏問題。?劣勢:?程序員無法掌控內(nèi)存,JS沒有暴露任何關(guān)于內(nèi)存的API,我們無法進(jìn)行強制垃圾回收,更無法干預(yù)內(nèi)存管理。

引用計數(shù)(reference counting)

引用計數(shù)是一種回收策略,它跟蹤記錄每個值被引用的次數(shù),每次引用的時候加一,被釋放時減一,如果一個值的引用次數(shù)變成0了,就可以將其內(nèi)存空間回收。

const obj = {a: 10}; ?// 引用 +1

const obj1 = {a: 10}; ?// 引用 +1

const obj = {}; ?// 引用 -1

const obj1 = null; ?// 引用為 0

當(dāng)聲明了一個變量并將一個引用類型值賦值該變量時,則這個值的引用次數(shù)就是1.如果同一個值又被賦給另外一個變量,則該值得引用次數(shù)加1。相反,如果包含對這個值引用的變量又取 得了另外一個值,則這個值的引用次數(shù)減 1。當(dāng)這個值的引用次數(shù)變成 0時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的內(nèi)存空間回收回來。這樣,當(dāng)垃圾收集器下次再運行時,它就會釋放那 些引用次數(shù)為零的值所占用的內(nèi)存。

使用引用計數(shù)會有一個很嚴(yán)重的問題:循環(huán)引用。循環(huán)引用指的是對象A中包含一個指向?qū)ο驜的指針,而對象B中也包含一個指向?qū)ο驛的引用。

當(dāng)聲明了一個變量并將一個引用類型值賦值該變量時,則這個值的引用次數(shù)就是1.如果同一個值又被賦給另外一個變量,則該值得引用次數(shù)加1。相反,如果包含對這個值引用的變量又取 得了另外一個值,則這個值的引用次數(shù)減 1。當(dāng)這個值的引用次數(shù)變成 0時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的內(nèi)存空間回收回來。這樣,當(dāng)垃圾收集器下次再運行時,它就會釋放那 些引用次數(shù)為零的值所占用的內(nèi)存。

使用引用計數(shù)會有一個很嚴(yán)重的問題:循環(huán)引用。循環(huán)引用指的是對象A中包含一個指向?qū)ο驜的指針,而對象B中也包含一個指向?qū)ο驛的引用。

function problem(){ ?

?var objectA = {}; ?

?var objectB = {}; ?

?objectA.a = objectB; ?

??objectB.b = objectA;

}


在這個例子中,objectA 和 objectB 通過各自的屬性相互引用;也就是說,這兩個對象的引用次數(shù)都是 2。當(dāng)函數(shù)執(zhí)行完畢后,objectA 和 objectB 還將繼續(xù)存在,因為它們的引用次數(shù)永遠(yuǎn)不會是 0。

因為引用計數(shù)有這樣的問題,現(xiàn)在瀏覽器已經(jīng)不再使用這個算法了,這個算法主要存在于IE 8及以前的版本,現(xiàn)代瀏覽器更多的采用標(biāo)記-清除算法。在老版的IE中一部分對象并不是原生 JavaScript 對象。例如,其 BOM 和 DOM 中的對象就是使用 C++以 COM(Component Object Model,組件對象模型)對象的形式實現(xiàn)的,而 COM對象的垃圾 收集機制采用的就是引用計數(shù)策略。

因此,即使 IE的 JavaScript引擎是使用標(biāo)記清除策略來實現(xiàn)的,但 JavaScript訪問的 COM對象依然是基于引用計數(shù)策略的。換句話說,只要在IE中涉及 COM對象,就會存在循環(huán)引用的問題。

下面這個簡單的例子,展示了使用 COM對象導(dǎo)致的循環(huán)引用問題:

var element = document.getElementById("some_element");

var myObject = new Object();

myObject.element = element;?

element.someObject = myObject;


這個例子在一個 DOM元素(element)與一個原生 JavaScript對象(myObject)之間創(chuàng)建了循環(huán)引用。

其中,變量 myObject 有一個名為 element 的屬性指向 element 對象;而變量 element 也有 一個屬性名叫 someObject 回指 myObject。

由于存在這個循環(huán)引用,即使將例子中的 DOM從頁面中移除,它也永遠(yuǎn)不會被回收。

為了避免類似這樣的循環(huán)引用問題,最好是在不使用它們的時候手工斷開原生 JavaScript 對象與 DOM元素之間的連接。例如,可以使用下面的代碼消除前面例子創(chuàng)建的循環(huán)引用:

myObject.element = null; element.someObject = null;

將變量設(shè)置為 null 意味著切斷變量與它此前引用的值之間的連接。當(dāng)垃圾收集器下次運行時,就會刪除這些值并回收它們占用的內(nèi)存。

為了解決上述問題,IE9把 BOM和 DOM對象都轉(zhuǎn)換成了真正的 JavaScript對象。這樣,就避免了兩種垃圾收集算法并存導(dǎo)致的問題,也消除了常見的內(nèi)存泄漏現(xiàn)象。

標(biāo)記-清除算法

標(biāo)記-清除算法就是當(dāng)變量進(jìn)入環(huán)境是,這個變量標(biāo)記位“進(jìn)入環(huán)境”;而當(dāng)變量離開環(huán)境時,標(biāo)記為“離開環(huán)境”,當(dāng)垃圾回收時銷毀那些帶標(biāo)記的值并回收他們的內(nèi)存空間。這里說的環(huán)境就是執(zhí)行環(huán)境,執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的數(shù)據(jù)。每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的變量對象(variable object),環(huán)境中所定義的所有變量和函數(shù)都保存在這個對象中。某個執(zhí)行環(huán)境中所有代碼執(zhí)行完畢后,改環(huán)境被銷毀,保存在其中的所有變量和函數(shù)也隨之銷毀。

全局執(zhí)行環(huán)境

全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境,在瀏覽器中,全局環(huán)境是window,Node.js中是global對象。全局變量和函數(shù)都是作為window或者global的屬性和方法創(chuàng)建的。全局環(huán)境只有當(dāng)程序退出或者關(guān)閉網(wǎng)頁或者關(guān)閉瀏覽器的時候才會銷毀。

局部執(zhí)行環(huán)境(環(huán)境棧)

每個函數(shù)都有自己的執(zhí)行環(huán)境。當(dāng)執(zhí)行流進(jìn)入一個函數(shù)時,函數(shù)的環(huán)境會被推入一個環(huán)境棧中。當(dāng)這個函數(shù)執(zhí)行之后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的環(huán)境。ECMAScript程序中的執(zhí)行流就是這個機制控制的。



在一個環(huán)境中聲明變量的時候,垃圾回收器將其標(biāo)記為“進(jìn)入環(huán)境”,當(dāng)函數(shù)執(zhí)行完畢時,將其標(biāo)記為“離開環(huán)境”,內(nèi)存被回收。

可能造成內(nèi)存泄露的情況

上面我們提到了兩種可能造成內(nèi)存泄露的情況:

    1. 對象之間的循環(huán)引用


      1. 老版IE(IE8及以前)里面DOM與對象之間的循環(huán)引用


      其他也可能造成循環(huán)引用的情況:

      1. 全局變量會存在于整個應(yīng)用生命周期,應(yīng)用不退出不會回收,使用嚴(yán)格模式可以避免這種情況

      2. 閉包因為自身特性,將函數(shù)內(nèi)部變量暴露到了外部作用域,當(dāng)其自身執(zhí)行結(jié)束時,所暴露的變量并不會回收

      3. 沒有clear的定時器

      V8的內(nèi)存管理

      V8是有內(nèi)存限制的,因為它最開始是為瀏覽器設(shè)計的,不太可能遇到大量內(nèi)存的使用場景。關(guān)鍵原因還是垃圾回收所導(dǎo)致的線程暫停執(zhí)行的時間過長。根據(jù)官方說法,以1.5G內(nèi)存為例,V8一次小的垃圾回收需要50ms,而一次非增量的,即全量的垃圾回收更需要一秒。這顯然是不可接受的。因此V8限制了內(nèi)存使用的大小,但是Node.js是可以通過配置修改的,更好的做法是使用Buffer對象,因為Buffer的內(nèi)存是底層C++分配的,不占用JS內(nèi)存,所以他也就不受V8限制。

      新生代

      新生代內(nèi)存中的垃圾回收主要通過 Scavenge 算法進(jìn)行,具體實現(xiàn)時主要采用了 Cheney 算法。新生代的堆內(nèi)存被分為多個Semispace,每個Semispace分為兩部分from和to,只有from的空間是使用中的,分配對象空間時,只在from中進(jìn)行分配,to是閑置的。進(jìn)行垃圾回收時按照如下步驟進(jìn)行:

      1. 找出from中還在使用的對象,即存活的對象

      2. 將這些活著的對象全部復(fù)制到to

      ?3. 反轉(zhuǎn)from和to,這時候from中全部是存活對象,to全部是死亡對象?

      4. 對to進(jìn)行全部回收



      可以看到在新生代中我們復(fù)制的是存活的對象,死亡對象都留在原地,最后被全部回收。這是因為對于大多數(shù)新增變量來說,可能只是用一下,很快就需要釋放,那在新生代中每次回收會發(fā)現(xiàn)存活的是少數(shù),死亡的是多數(shù)。那我們復(fù)制的就是少數(shù)對象,這樣效率更高。如果一個變量在新生代中經(jīng)過幾次復(fù)制還活著,那他生命周期可能就比較長,會晉升到老生代。有兩種情況會對對象進(jìn)行晉升:

      1. 新生代垃圾回收過程中,當(dāng)一個對象經(jīng)過多次復(fù)制后還存活,移動到老生代;

      ?2. 在from和to進(jìn)行反轉(zhuǎn)的過程中,如果to空間的使用量超過了25%,那么from的對象全部晉升到老生代

      老生代

      老生代存放的是生命周期較長的對象,他的結(jié)構(gòu)是一個連續(xù)的結(jié)構(gòu),不像新生代分為from和to兩部分。老生代垃圾回收有兩種方式,標(biāo)記清除和標(biāo)記合并。



      標(biāo)記清除

      標(biāo)記清除是標(biāo)記死亡的對象,直接其空間釋放掉。在標(biāo)記清除方法清除掉死亡對象后,內(nèi)存空間就變成不連續(xù)的了,所以出現(xiàn)了另一個方案:標(biāo)記合并。



      標(biāo)記合并

      這個方案有點像新生代的Cheney算法,將存活的對象移動到一邊,將需要被回收的對象移動到另一邊,然后對需要被回收的對象區(qū)域進(jìn)行整體的垃圾回收。



      與新生代算法相比,老生代主要操作死亡對象,因為老生代都是生命周期較長的對象,每次回收死亡的比較少;而新生代主要操作的存活對象,因為新生代都是生命周期較短的對象,每次回收存活的較少。這樣無論新生代還是老生代,每次回收時都盡可能操作更少的對象,效率就提高了。


      JavaScript的內(nèi)存管理的評論 (共 條)

      分享到微博請遵守國家法律
      华宁县| 沙坪坝区| 钟祥市| 蓬安县| 米脂县| 达日县| 海伦市| 德安县| 吉水县| 普陀区| 左权县| 子洲县| 平武县| 大埔县| 滨州市| 长海县| 千阳县| 嘉禾县| 宁波市| 太保市| 景宁| 苗栗市| 四子王旗| 武功县| 江西省| 克东县| 合水县| 铜川市| 孟津县| 九龙城区| 安西县| 黄大仙区| 海城市| 库伦旗| 老河口市| 苗栗市| 门头沟区| 罗田县| 施甸县| 巴林左旗| 衡阳县|