一.操作系統(tǒng)相關(guān)基礎(chǔ)知識
1.物理內(nèi)存权薯、虛擬內(nèi)存嗓化、邏輯地址與交換空間
物理內(nèi)存(RAM):加載到內(nèi)存地址寄存器中的內(nèi)存又叫“硬件內(nèi)存”拷呆,是內(nèi)存單元真正的地址(也叫物理地址)闲坎。RAM作為進程運行不可或缺的資源,對系統(tǒng)和穩(wěn)定性有著決定性的影響茬斧。另外,RAM的一部分被操作系統(tǒng)留作他用梗逮,比如顯存等项秉。
邏輯地址:由CPU控制生成的地址,是一個程序級別的概念慷彤。這里引用一個淺顯的例子——我們在C語言指針編程中娄蔼,可以讀取指針變量本身的值(&操作)怖喻,這里取得的值就是邏輯地址——也就是說,這個(&操作)取得的值是CPU控制生成的一個邏輯地址岁诉,并不是這個指針變量在RAM中的真正地址锚沸。
??那么,我們?yōu)槭裁匆@么一個并不是真正地址的邏輯地址呢涕癣?深層次的原因這里不予以探究哗蜈,但是一個比較淺顯的原因就是,邏輯地址的分配非常靈活——在一個數(shù)組中坠韩,我們通過邏輯地址可以保證數(shù)組中元素地址的連續(xù)性距潘。當然這個邏輯地址最終還是要通過一定的方式映射到RAM中的物理地址上,這個物理地址才是元素存儲的真正地址只搁,而這個物理地址音比,不一定是連續(xù)的。
虛擬內(nèi)存:是操作系統(tǒng)級別的概念氢惋,指計算機呈現(xiàn)出要比實際擁有的內(nèi)存大得多的內(nèi)存量洞翩。它使得每個應(yīng)用程序都認為自己擁有獨立且連續(xù)的可用的內(nèi)存空間(一段連續(xù)完整的地址空間),這個內(nèi)存大小跟操作系統(tǒng)的位數(shù)有關(guān)焰望。比如32位系統(tǒng)骚亿,邏輯內(nèi)存的最大為2^23。而實際上柿估,它通常是被映射到多個物理內(nèi)存段(在真正的物理地址上不一定是連續(xù)的)循未,還有部分暫時存儲在外部磁盤存儲器上,在需要時再加載到內(nèi)存中來秫舌。
??上一段我們我們說了半天的邏輯地址的妖,筆者的理解就是虛擬內(nèi)存中的地址。OK足陨,現(xiàn)在我們知道了虛擬內(nèi)存有兩個特點——一個是在虛擬內(nèi)存中虛擬地址/邏輯地址是連續(xù)的嫂粟,便于靈活分配;二是虛擬內(nèi)存可以是計算機呈現(xiàn)出比實際內(nèi)存大的多的內(nèi)存墨缘。那么為什么虛擬內(nèi)存會呈現(xiàn)出這么大的內(nèi)存的神奇功能呢星虹?或者說這多出來的額內(nèi)存是哪來的?這就要用到我們接下來講的交換(Swap)空間镊讼。
交換(Swap)空間:在系統(tǒng)中運行的每個進程都需要使用到內(nèi)存宽涌,但不是每個進程都需要每時每刻使用系統(tǒng)分配的內(nèi)存空間。當系統(tǒng)運行所需內(nèi)存超過實際的物理內(nèi)存蝶棋,內(nèi)核會釋放某些進程所占用但未使用的部分或所有物理內(nèi)存铡恕,將這部分釋放的數(shù)據(jù)存儲在磁盤上直到進程下一次調(diào)用同仆,并將釋放出的內(nèi)存提供給有需要的進程使用详炬。
??引用一個容易理解但不是很恰當?shù)谋扔鳎耗悴恍枰荛L的軌道就可以讓一列火車從上海開到北京。你只需要足夠長的鐵軌(比如說3公里)就可以完成這個任務(wù)段直。采取的方法是把后面的鐵軌立刻鋪到火車的前面,只要你的操作足夠快并能滿足要求溶诞,列車就能象在一條完整的軌道上運行鸯檬。
??swap和虛擬內(nèi)存結(jié)伴而來的。如果系統(tǒng)是64位螺垢,最大虛擬內(nèi)存可以是2的64次方喧务,沒有計算機會有這么大的內(nèi)存。當內(nèi)存不夠用的時候只能映射到磁盤甩苛。linux專門開辟了一個swap磁盤分區(qū)蹂楣,當物理內(nèi)存不夠用的時候(程序并不知道),將內(nèi)存中很久不使用的內(nèi)存區(qū)域交換到swap區(qū)讯蒲。也即是說:用作虛擬內(nèi)存的磁盤空間稱為交換空間(swap空間)痊土。
2.進程的地址空間
在32位操作系統(tǒng)中,進程的地址空間是0到4GB墨林。這里我們需要強調(diào)一點:進程所擁有的內(nèi)存空間指的是“虛擬內(nèi)存”赁酝,虛擬地址/邏輯地址與進程息息相關(guān),不同進程里同一個虛擬地址指向的物理地址不一定是相同的旭等,所以離開進程談虛擬內(nèi)存沒有任何意義酌呆。下圖展示了Linux進程地址空間(虛擬內(nèi)存)的組成:
Stack(棧)與Heap(堆)
Stack空間(進棧和出棧)由操作系統(tǒng)控制,其主要儲存函數(shù)地址搔耕、函數(shù)參數(shù)隙袁、局部變量等等,所以Stack空間不需要很大弃榨,一般幾MB大小菩收。
??Heap空間由程序員控制,主要包括實例域鲸睛、靜態(tài)域娜饵、數(shù)組元素等,儲存空間比較大官辈,一般為幾百NB到幾GB箱舞。正是由于Heap空間由程序員管理,所以容易出現(xiàn)使用不當?shù)膯栴}(如內(nèi)存泄漏)拳亿,當然晴股,Heap內(nèi)存也是系統(tǒng)GC發(fā)生的區(qū)域。
3.Android中的進程
Android中的進程主要分為native進程和Java進程——native進程指的是采用C/C++實現(xiàn)的肺魁,不包括Dalvik實例的Linux進程队魏;java進程:實例化了Dalvik虛擬機的Linux進程,我們開發(fā)的APP就是出于java進程中的万搔。
??我們上面說過胡桨,Heap(堆)內(nèi)存是由程序員控制的。我們使用malloc瞬雹、C++ new和java new所申請的空間都是heap空間昧谊,只不過C/C++申請的內(nèi)存空間在native heap中,而java申請的內(nèi)存空間則在dalvik heap中酗捌。在平時的開發(fā)中呢诬,我們打交道的最多的就是dalvik heap,我們的實例域胖缤、靜態(tài)域尚镰、數(shù)組元素等都是在dalvik的heap中,虛擬機的GC也發(fā)生在其中哪廓。
二. DVM(Dalvik虛擬機)
1.DVM與JVM
(1)什么JVM?
JVM是一個虛構(gòu)出來的計算機狗唉,是通過在實際的計算機上仿真模擬各種計算機功能來實現(xiàn)的。它有自己完善的(虛擬)硬件架構(gòu)(如處理器涡真、堆棧分俯、寄存器等),還具有相應(yīng)的指令系統(tǒng)哆料。使用“Java虛擬機”程序就是為了支持與操作系統(tǒng)無關(guān)缸剪、在任何系統(tǒng)中都可以運行的程序。
??Java語言的一個非常重要的特點就是與平臺的無關(guān)性东亦。而使用Java虛擬機是實現(xiàn)這一特點的關(guān)鍵杏节。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼典阵。而引入Java語言虛擬機后奋渔,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關(guān)的信息萄喳,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節(jié)碼卒稳,就可以在多種平臺上不加修改地運行。Java虛擬機在執(zhí)行字節(jié)碼時他巨,把字節(jié)碼解釋成具體平臺上的機器指令執(zhí)行充坑。這就是Java能夠“一次編譯,到處運行”的原因染突。
(2)什么是DVM捻爷?
Dalvik是Google公司自己設(shè)計用于Android平臺的Java虛擬機,也就是說份企,本質(zhì)上也榄,Dalvik也是一個Java虛擬機。是Android中Java程序的運行基礎(chǔ)。
??其指令集基于寄存器架構(gòu)甜紫,執(zhí)行其特有的文件格式——dex字節(jié)碼來完成對象生命周期管理降宅、堆棧管理、線程管理囚霸、安全異常管理腰根、垃圾回收等重要功能。它的核心內(nèi)容是實現(xiàn)庫(libdvm.so)拓型,大體由C語言實現(xiàn)额嘿。
(3)JVM 與 Dalvik VM的關(guān)系
上面我們說JVM是一個虛構(gòu)出來的計算機,這種說法并不是很準確——嚴格來說劣挫,JVM是一種規(guī)范册养,或者說實現(xiàn)這種規(guī)范的實例,從這個角度來說压固,Dalvik VM也是一個 特殊的JVM球拦,這點在上面也有提到。說的更通俗一點邓夕,能符合規(guī)范正確執(zhí)行Java的.class文件的就是JVM刘莹;那么Android開發(fā)包中的dx與Dalvik VM結(jié)合起來,就可以看成是一個JVM了(要把一個東西稱為“JVM”必須要通過JCK(Java Compliance Kit)的測試并獲得授權(quán)后才能行焚刚,所以嚴格來說dx + Dalvik VM不能叫做JVM点弯,因為沒授權(quán))。
2.JVM和DVM的區(qū)別與聯(lián)系
(1).Dalvik VM是基于寄存器的架構(gòu)(reg based),而JVM是堆棧結(jié)構(gòu)(stack based)矿咕。
這里的寄存器架構(gòu)和堆棧結(jié)構(gòu)指的是計算機指令系統(tǒng)抢肛,計算機指令系統(tǒng)分為四種:堆棧型,累加器型碳柱,寄存器-儲存器型和寄存器-寄存器型捡絮。四種分類的依據(jù)是操作數(shù)的來源。堆棧型默認的操作數(shù)都在棧頂莲镣,累加器型默認一個操作數(shù)是累加器福稳,寄存器-存儲器型的操作數(shù)可以是寄存器或者內(nèi)存。寄存器-寄存器型除了訪存指令瑞侮,操作數(shù)都是寄存器的圆。
??x86一開始并沒有使用太多的通用寄存器,原因之一(注意半火,只是之一)是當時的編譯器無力進行寄存器分配越妈,讓編譯器自動決定程序中眾多變量哪些應(yīng)該裝入寄存器、哪些應(yīng)該換出钮糖、哪些變量應(yīng)該映射到同一個寄存器上梅掠,并不是一件易事,JVM采用堆棧結(jié)構(gòu)的原因之一就是不信任編譯器的寄存器分配能力,轉(zhuǎn)而使用堆棧結(jié)構(gòu)阎抒,躲開寄存器分配的難題酪我。
??如今的CPU早就有足夠的晶體管來支持復(fù)雜設(shè)計,為了性能著想挠蛉,大量使用寄存器型的指令祭示,原因在于寄存器離CPU最近,所以延時最短谴古,取指最快,有利于主頻提高稠歉。
那么基于棧與基于寄存器的架構(gòu)掰担,誰更快呢?intel的X86還保留有累加器指令和堆棧型指令怒炸,這是為了歷史兼容带饱。很多現(xiàn)今的處理器,除了load和store指令訪存外阅羹,只支持對寄存器操作勺疼,不支持對堆棧以及內(nèi)存的直接操作——這也從側(cè)面反映出基于寄存器比基于棧的架構(gòu)更與實際的處理器接近。
①.dvm速度快捏鱼!寄存器存取速度比椫绰快的多,dvm可以根據(jù)硬件實現(xiàn)最大的優(yōu)化导梆,比較適合移動設(shè)備轨淌。JAVA虛擬機基于棧結(jié)構(gòu),程序在運行時虛擬機需要頻繁的從棧上讀取寫入數(shù)據(jù)看尼,這個過程需要更多的指令分派與內(nèi)存訪問次數(shù)递鹉,會耗費很多CPU時間。
??②.指令數(shù)胁卣丁躏结!dvm基于寄存器,所以它的指令是二地址和三地址混合狰域,指令中指明了操作數(shù)的地址媳拴;jvm基于棧,它的指令是零地址北专,指令的操作數(shù)對象默認是操作數(shù)棧中的幾個位置禀挫。這樣帶來的結(jié)果就是dvm的指令數(shù)相對于jvm的指令數(shù)會小很多,jvm需要多條指令而dvm可能只需要一條指令拓颓。
(2).Dalvik 執(zhí)行速度比 JVM 快语婴,但移植性稍差.
Dalvik 執(zhí)行速度比 JVM 快的原因,上面已經(jīng)做了一些說明,這里在綜合移植性說一下砰左。在一個解釋器上執(zhí)行VM指令匿醒,包含三個步驟:指令分派、訪問操作數(shù)和執(zhí)行計算缠导。
①.指令分派
??指令分派負責從內(nèi)存中讀取 VM 指令廉羔,然后跳轉(zhuǎn)到相應(yīng)的解釋器代碼中。上面提到過僻造,完成同樣的事情憋他,基于棧的虛擬機需要更多的指令,意味著更多的指令分派和 內(nèi)存訪問次數(shù)髓削,這是 JVM 的執(zhí)行性能不如 Dalvik VM 的原因之一竹挡。
②.訪問操作數(shù)
??訪問操作數(shù)是指讀取和寫回源操作數(shù)和目的操作數(shù)。Dalvik VM 通過虛擬寄存器來訪問操作數(shù)立膛, 由于具有相近的血緣揪罕, Dalvik 的虛擬寄存器在映射到物理寄存器方面具有更充分的優(yōu)勢, 這也是 Dalvik VM 性能較佳的一個原因宝泵。
??JVM 的操作數(shù)通過操作數(shù)棧來訪問好啰, 而因為指令中沒有使用任何通用寄存器,在虛擬機的實現(xiàn)中可以比較自由的分配實際機器的寄存器儿奶,因而可移植性高框往。
??作為一個優(yōu)化,操作數(shù)棧也可以由編譯器映射到物理寄存器上廓握,減少數(shù)據(jù)移動的開銷搅窿。
③.指令執(zhí)行
(3).Dalvik執(zhí)行的是特有的DEX文件格式,而JVM運行的是.class文件格式.*
在Java程序中隙券,Java類會被編譯成一個或多個class文件男应,然后打包到j(luò)ar文件中,接著Java虛擬機會從相應(yīng)的class文件和jar文件中獲取對應(yīng)的字節(jié)碼娱仔;Android應(yīng)用雖然也使用Java語言沐飘,但是在編譯成class文件后,還會通過DEX工具將所有的class文件轉(zhuǎn)換成一個dex文件牲迫,Dalvik虛擬機再從中讀取指令和數(shù)據(jù)耐朴。
優(yōu)勢:
??class文件去冗余:class文件存在很多的冗余信息,dex工具會去除冗余信息(多個class中的字符串常量合并為一個盹憎,比如對于Ljava/lang/Oject字符常量筛峭,每個class文件基本都有該字符常量,存在很大的冗余)陪每,并把所有的.class文件整合到.dex文件中影晓。減少了I/O操作镰吵,提高了類的查找速度。
缺點:
??方法數(shù)受限:多個class文件變成一個dex文件所帶來的問題就是方法數(shù)超過65535時報錯挂签,由此引出MultiDex技術(shù)疤祭。
可以看到,這里最終生成了一個.odex文件饵婆,odex是為了在運行過程中進一步提高性能勺馆,對dex文件的進一步優(yōu)化,優(yōu)化后的文件大小會有所增加侨核,應(yīng)該是原DEX文件的1-4倍草穆。
更多關(guān)于.class文件與.dex文件的知識可以參照這篇文章:深入理解Android(二):Java虛擬機Dalvik,這篇文章中對這兩個文件的結(jié)構(gòu)做了非常詳細的分析芹关,這里就不多說了续挟。
(4).Dalvik可以允許多個instance 運行,也就是說每一個Android 的App是獨立跑在一個VM中.
一個應(yīng)用侥衬,一個進程,一個Dalvik跑芳!
Zygote是一個虛擬機進程轴总,同時也是一個虛擬機實例的孵化器,它通過init進程啟動博个。首先會孵化出System_Server怀樟,他是android絕大多系統(tǒng)服務(wù)的守護進程,它會監(jiān)聽socket等待請求命令盆佣,當有一個應(yīng)用程序啟動時往堡,就會向它發(fā)出請求,zygote就會FORK出一個新的應(yīng)用程序進程共耍。
??這樣做的好處是:Zygote進程是在系統(tǒng)啟動時產(chǎn)生的虑灰,它會完成虛擬機的初始化,庫的加載痹兜,預(yù)置類庫的加載和初始化等等操作穆咐,而在系統(tǒng)需要一個新的虛擬機實例時,Zygote通過復(fù)制自身字旭,最快速的提供個進程对湃;另外,對于一些只讀的系統(tǒng)庫遗淳,所有虛擬機實例都和Zygote共享一塊內(nèi)存區(qū)域拍柒,大大節(jié)省了內(nèi)存開銷。
??每一個app啟動的時候屈暗,就會有自己的進程與Dalvik虛擬機實例拆讯。而這樣做的好處是一個App crash只會影響到自身的VM脂男,不會影響到其他。 Dalvik的設(shè)計是每一個Dalvik的VM都是Linux下面的一個進程往果。那么這就需要高效的IPC疆液。另外每一個VM是單獨運行的好處還有可以動態(tài)active/deactive自己的VM而不會影響到其他VM。
3.Android為什么會出現(xiàn)OOM陕贮?
通過上面的介紹堕油,我們對Dalvik虛擬機已經(jīng)有了一些初步的了解,現(xiàn)在我門回到Android的內(nèi)存管理中來肮之。前面我們說過Heap(堆)內(nèi)存是由程序員控制的掉缺,用C/C++申請的內(nèi)存空間在native heap中,而java申請的內(nèi)存空間則在dalvik heap中
??那么為什么會出現(xiàn)OOM的情況呢戈擒?這個是因為Android系統(tǒng)對dalvik虛擬機的heap大小作了硬性限制眶明,當java進程申請的空間超過這個閾值時,就會拋出OOM異常(這個閾值可以是48M筐高、24M搜囱、16M等,視機型而定)柑土。
??也就是說蜀肘,程序發(fā)生OMM并不表示RAM不足,而是因為程序申請的java heap對象超過了dalvik vm heapgrowthlimit稽屏。也就是說扮宠,在RAM充足的情況下,也可能發(fā)生OOM狐榔。
??這樣設(shè)計的目的是為了讓Android系統(tǒng)能同時讓比較多的進程常駐內(nèi)存(RAM)坛增,這樣程序啟動時就不用每次都重新加載到內(nèi)存,能夠給用戶更快的響應(yīng)薄腻。迫使每個應(yīng)用程序使用較小的內(nèi)存收捣,移動設(shè)備非常有限的RAM就能使比較多的app常駐其中。
java程序發(fā)生OMM并不是表示RAM不足被廓,如果RAM真的不足坏晦,Android的memory killer會起作用,當RAM所剩不多時嫁乘,memory killer會殺死一些優(yōu)先級比較低的進程來釋放物理內(nèi)存昆婿,讓高優(yōu)先級程序得到更多的內(nèi)存。
三.JVM / Dalvik VM垃圾回收機制
這里為什么要講JVM的垃圾回收機制蜓斧?——前面我們說過仓蛆,Davlik VM本質(zhì)上也是一種JVM,因此我們從大處著手挎春,并在其中我們會穿插講解一下Dalvik VM在具體實現(xiàn)上的一些不同看疙。
1.JVM的基本架構(gòu)
如圖豆拨,Java VM規(guī)則將JVM所管理的內(nèi)存分為以下幾個部分:
(1).方法區(qū)
各個線程所共享的,用于存儲已被虛擬機加載類信息能庆、常量施禾、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)搁胆。根據(jù)Java虛擬機規(guī)范的規(guī)定弥搞,當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常渠旁。
常量池
運行時常量池是方法區(qū)的一部分攀例。用于存放編譯器生成的各種字面量和符號引用。運行期間也可以將新的常量放入常量池中顾腊,用得比較多的就是String類的intern()方法粤铭,當一個String實例調(diào)用intern時,Java查找常量池中是否有相同的Unicode的字符串常量杂靶,若有梆惯,則返回其引用;若沒有吗垮,則在常量池中增加一個Unicode等于該實例字符串并返回它的引用加袋。
(2).Java堆
① Java堆
虛擬機管理內(nèi)存中最大的一塊,被所有線程共享抱既,該區(qū)域用于存放對象實例,幾乎所有的對象(實例變量扁誓,數(shù)組)都在該區(qū)域分配,是內(nèi)存回收的主要區(qū)域防泵。每個對象都包含一個與之對應(yīng)的class信息(我們常說的類類型,Clazz.getClass()等方式獲然雀摇)捷泞。【這里的“對象”,不包括基本數(shù)據(jù)類型】
??從內(nèi)存回收角度看寿谴,由于現(xiàn)在的收集器大都采用分代收集算法锁右,所以Java堆還可以細分為:新生代和老年代,再細分一點的話可以分為Eden空間讶泰、From Survivor空間咏瑟、To Survivor空間等(這個后面會講)。根據(jù)Java虛擬機規(guī)范規(guī)定痪署,Java堆可以處于物理上不連續(xù)的空間码泞,只要邏輯上是連續(xù)的就行。如果在堆中沒有內(nèi)存可分配時狼犯,并且堆也無法擴展時余寥,將會拋出OutOfMemoryError異常领铐。
② Dalvik堆
Dalvik VM的堆結(jié)構(gòu)相對于JVM的堆結(jié)構(gòu)有所區(qū)別,只而主要體現(xiàn)在Dalvik將堆分成了Active堆和Zygote堆宋舷。之前我們有說過绪撵,這個Zygote是一個虛擬機進程,同時也是一個虛擬機實例的孵化器——那么同樣的祝蝠,zygote堆是Zygote進程在啟動時的預(yù)加載的類音诈、資源和對象;除此之外所有的對象,包括我們在代碼中創(chuàng)建的實例续膳、靜態(tài)域和數(shù)組改艇,都是儲存在Active堆里邊的。
??為什么要把Dalvik堆分成Zygote堆和Active堆坟岔?這主要是因為Android通過fork方法創(chuàng)建一個新的zygote進程谒兄,為了盡可能的避免父進程和子進程之間的數(shù)據(jù)拷貝,fork方法使用寫時拷貝技術(shù)社付,簡單講就是fork的時候不立即拷貝父進程的數(shù)據(jù)到子進程中承疲,而是在子進程或者父進程對內(nèi)存進行寫操作時才對內(nèi)容進行復(fù)制。
??Dalvik的Zygote堆存放的預(yù)加載類都是Android核心類和Java運行時庫鸥咖,這部分很少被修改燕鸽,大多數(shù)情況下父進程和子進程共享這塊區(qū)域,因此沒有必要對這部分類進行垃圾回收之類的修改啼辣,直接復(fù)制即可啊研。而Active堆作為我們程序代碼中創(chuàng)建實例對象的存放堆,是垃圾回收的重點區(qū)域鸥拧,因此將兩個堆分開党远。
(3).Java 棧 / Java虛擬機棧(Java Virtual Machine Stacks)
線程私有,它的生命周期與線程相同富弦。 Java虛擬機棧描述的是Java方法(區(qū)別于native的本地方法)執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表沟娱、操作棧、動作鏈接腕柜、方法出口等信息济似。每個方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程盏缤。
??我們所用的大多數(shù) JVM 都是基于 Java 棧的運行機制砰蠢,而有一個例外的實現(xiàn),Google 移動設(shè)備操作系統(tǒng) Android 的虛擬機 Dalvik 則是基于寄存器的機制(Dalvik 雖然支持 Java 語言開發(fā)蛾找,但從虛擬機的角度看娩脾,并不符合 Java VM 標準),關(guān)于虛擬機實現(xiàn)時打毛,棧和寄存器機制的比較柿赊,請參考論文“Virtual Machine Showdown: Stack Versus Registers”;
Java棧俩功,劃分為操作數(shù)棧、棧幀數(shù)據(jù)和局部變量區(qū)碰声,方法中分配的局部變量在棧中诡蜓,同時每一次方法的調(diào)用都會在棧中分配棧幀。對于基于棧的 Java 虛擬機胰挑,方法的調(diào)用和執(zhí)行伴隨著壓棧和出棧操作蔓罚。每個線程有各自獨立的棧,由虛擬機來管理棧的大小瞻颂,但我們應(yīng)該對它的大小有個概念豺谈。棧的大小是把雙刃劍,如果太小贡这,可能會導(dǎo)致棧溢出茬末,特別是在該線程內(nèi)有遞歸、大的循環(huán)時出現(xiàn)溢出的可能性更大盖矫,如果過大丽惭,就會影響到可創(chuàng)建棧的數(shù)量,如果是多線程的應(yīng)用辈双,就會導(dǎo)致內(nèi)存溢出责掏。
來看一段字節(jié)碼在 Java 棧中的執(zhí)行示例,100 與 98 相加:
iload_0 // 載入局部變量 0湃望,整型换衬,壓入棧中
iload_1 // 載入局部變量 1,整型证芭,壓入棧中
iadd // 彈出兩個整型數(shù)冗疮,相加,將結(jié)果壓入棧
istore_2 // 彈出整型數(shù)檩帐,存入局部變量 2
(4).本地方法棧(Native Method Stacks)
本地方法棧與Java虛擬機棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)另萤,而本地方法棧則為虛擬機所使用到的Native方法服務(wù)湃密。說白了,這是 Java 調(diào)用操作系統(tǒng)本地庫的地方四敞,用來實現(xiàn) JNI(Java Native Interface泛源,Java 本地接口)
(5).程序計數(shù)器(Program Counter Register)
程序計數(shù)器是一塊較小的內(nèi)存空間,它的作用可以看做是當前線程所執(zhí)行字節(jié)碼的行號指示器忿危,字節(jié)碼解釋器工作時通過改變該計數(shù)器的值來選擇下一條需要執(zhí)行的字節(jié)碼指令达箍。是線程私有,生命周期與線程相同铺厨。
2.垃圾回收算法相關(guān)
(1).可回收對象的判定
①.引用計數(shù)算法
給對象添加一個引用計數(shù)器缎玫,每當有一個地方引用它的時候硬纤,計數(shù)器的值就加1;當引用失效的時候赃磨,計數(shù)器的值就減1筝家;任何時刻計數(shù)器為0的對象是不可能再被引用的。
??這種方法實現(xiàn)簡單邻辉,判斷效率也很高溪王;但是該算法有一個致命的缺點就是難以解決對象相互引用的問題:試想有兩個對象,相互持有對方的引用值骇,而沒有別的對象引用到這兩者莹菱,那么這兩個對象就是無用的對象,理應(yīng)被回收吱瘩,但是由于他們互相持有對方的引用道伟,因此他們的引用計數(shù)器不為0,因此他們不能被回收。
②.可達性分析算法
為了解決上面循環(huán)引用的問題宪祥,Java采用了一種全新的算法——可達性分析算法官卡。這個算法的核心思想是,通過一系列稱為“GC Roots”的對象作為起始點娜汁,從這些結(jié)點開始向下搜索,搜索所走過的路徑成為“引用鏈”兄朋,當一個對象到GC Roots沒有一個對象相連時掐禁,則證明此對象是不可用的(不可達)。
在Java語言中颅和,可作為GC Roots的對象包括下面幾種:
- 上面說的JVM棧(棧幀數(shù)據(jù)中的本地變量表)中引用的對象傅事。
- 方法區(qū)中類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用的對象峡扩。
- Native 方法棧中JNI引用的對象蹭越。
需要注意一點,即使在可達性分析算法中不可達對象教届,也并非是“非死不可”的响鹃,要真正宣告一個對象的死亡,至少需要經(jīng)歷兩次標記的過程:
??如果一個對象在進行可達性分析之后發(fā)現(xiàn)沒有與GC Roots相連的引用鏈案训,那么他將會第一次標記并且****买置。當對象沒有復(fù)寫finalize()方法,或者finalize()方法已經(jīng)被虛擬機調(diào)用過强霎,虛擬機講著兩種情況都視為“沒有必要執(zhí)行finalize()方法”忿项。
??如果這個對象被判定為有必要執(zhí)行finalize()方法,那么這個對象會被加入一個“F-Queue”隊列中,并在稍后由一個虛擬機建立的轩触、優(yōu)先級低的Finalize線程寞酿,去觸發(fā)這個方法,但并不承諾會等待他運行結(jié)束怕膛。
??finalize()方法是對象逃脫死亡厄運的最后一次機會熟嫩,稍后的GC會對在“F-Queue”隊列中的對象進行第二次小規(guī)模的標記;
??如果對象要在finalize()中拯救自己褐捻,只需要重新與引用鏈上的對象就行關(guān)聯(lián)即可掸茅,那么在第二次標記時它將被移出“即將回收”的集合;
??如果對象這個時候還是沒有逃脫柠逞,那基本上他就真的被回收了昧狮。
③.引用
無論是引用計數(shù)法還是可達性分析算法,判斷對象的存活與否都與“引用”有關(guān)板壮。在JDK1.2之前逗鸣,“引用”的解釋為:如果reference類型的數(shù)據(jù)中儲存的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這個數(shù)據(jù)代表著一個引用绰精。在JDK1.2之后撒璧,Java對引用的概念進行了擴充,將引用分為強引用笨使、軟引用卿樱、弱引用、虛引用硫椰。
- 強引用:就是指在程序代碼之中普遍存在的繁调,類似于“Object obj = new Object();”這樣的引用,只要強引用還存在靶草,垃圾回收器永遠不會回收掉被引用的對象蹄胰。
- 軟引用:用來描述一些還有用但并非必須的對象。對于軟引用關(guān)聯(lián)的對象奕翔,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前裕寨,將會把這些對象列進回收的范圍,進行第二次回收——如果這次回收還沒有騰出足夠的內(nèi)存派继,才會內(nèi)存溢出拋出異常帮坚。在JDK1.2之后,提供了SoftReference來實現(xiàn)軟引用互艾。
- 弱引用:也是用來描述非必須對象的,但是他的強度比軟引用更弱一些讯泣。被弱引用引用的對象纫普,只能生存到下一次GC之前,當GC發(fā)生時,無論無論當前內(nèi)存是否足夠昨稼,都會回收掉被弱引用關(guān)聯(lián)的對象节视。JDK1.2之后,提供了WeakRefernce類來實現(xiàn)弱引用假栓。
- 虛引用:是最弱的一種引用寻行,一個對象有虛引用的存在,完全不會對其生存時間構(gòu)成影響匾荆,也無法通過虛引用來獲得一個對象的實例拌蜘。為一個對象設(shè)置一個虛引用關(guān)聯(lián)的唯一目的就是能夠在這個對象唄收集器回收的的時候收到一個系統(tǒng)的通知。
(2).Stop The World
有了上面的垃圾對象的判定牙丽,我們還要考慮一個問題简卧,那就是Stop The World。垃圾回收的時候烤芦,需要保持整個引用狀態(tài)不變:假設(shè)一個對象沒有被標記到举娩,或者沒有與GC Roots產(chǎn)生關(guān)聯(lián),那么他被判定為垃圾构罗,需要回收铜涉;但是等我一會執(zhí)行回收的時候,他又被別的對象引用了——這樣的話整個GC過程就無法執(zhí)行了遂唧。
??因此芙代,在GC的過程中,其他所有程序進程處于暫停狀態(tài)蠢箩,也就是俗稱的卡住了链蕊。所有的GC卡頓問題均由此而來,這個暫時無法解決谬泌。幸運的是滔韵,這個卡頓是非常短暫的,尤其是在Java堆的新生代(待會會講)掌实,對程序的影響微乎其微陪蜻。
(3).幾種辣雞回收算法
①.標記清除算法 (Mark-Sweep)
標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務(wù)是標記出所有需要被回收的對象贱鼻,清除階段就是回收被標記的對象所占用的空間宴卖。
??這種算法的缺點是容易產(chǎn)生內(nèi)存碎片,碎片太多可能會導(dǎo)致后續(xù)過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發(fā)新的一次垃圾收集動作邻悬。
②.復(fù)制算法 (Copying)
復(fù)制算法將可用內(nèi)存按容量劃分為大小相等的兩塊症昏,每次只使用其中的一塊。當這一塊的內(nèi)存用完了父丰,就將還存活著的對象復(fù)制到另外一塊上面肝谭,然后再把已使用的另一半內(nèi)存空間中的對象一次性全部清理掉,這樣一來就不容易出現(xiàn)內(nèi)存碎片的問題。
??這種算法的優(yōu)點就是攘烛,實現(xiàn)簡單魏滚,運行高效且不容易產(chǎn)生內(nèi)存碎片;缺點也顯而易見:將可用內(nèi)存縮小為了原來的一半坟漱,代價非常高昂鼠次。
??從算法原理我們可以看出Copying算法的效率跟存活對象的數(shù)目多少有很大的關(guān)系,如果存活對象很多芋齿,那么Copying算法的效率將會大大降低(要復(fù)制的對象比較多)腥寇。
③.標記整理算法 (Mark-Compact)
該算法標記階段和Mark-Sweep一樣,但是在完成標記之后沟突,它不是直接清理可回收對象花颗,而是將存活對象都向一端移動,然后清理掉端邊界以外的內(nèi)存惠拭。
??這種算法特別適用于存活對象多扩劝,回收對象少的情況,因為回收的對象少职辅,標記完了之后需要移動的對象就相對較少棒呛。
④.分代回收算法
當前的商業(yè)虛擬機的垃圾收集器都采用“分代收集”算法,這種算法并沒有什么新的思想域携,只是根據(jù)對象的存活的周期不同將內(nèi)存劃分為幾塊簇秒。
??前面我們說過,復(fù)制算法:適用于存活對象很少秀鞭,回收對象多趋观;標記整理算法:適用于存活對象多,回收對象很少的情況锋边。這兩種算法情況正好互補皱坛!
??一般情況下我們把Java的對分為新生代和老年代,在新生代豆巨,每次垃圾收集時剩辟,都會有大批的對象死去,只有少量存活往扔,因此適用復(fù)制算法贩猎;而在老年代,因為對象存活率高萍膛、沒有額外的空間對它進行分配擔保吭服,就必須使用“標記-清除”或者“標記-整理”算法來進行回收。
3.Java堆內(nèi)存模型
上面我們已經(jīng)簡略的說過Java堆和Dalvik堆的區(qū)別蝗罗,這里我們復(fù)習(xí)一下:Java堆用于存放對象實例艇棕,幾乎所有的對象(實例變量麦到,數(shù)組)都在該區(qū)域分配,是內(nèi)存回收的主要區(qū)域;Dalvik將堆分成了Active堆和Zygote堆欠肾,zygote堆是Zygote進程在啟動時的預(yù)加載的類、資源和對象拟赊;除此之外所有的對象,包括我們在代碼中創(chuàng)建的實例刺桃、靜態(tài)域和數(shù)組,都是儲存在Active堆里邊吸祟。
- Java堆按照對象存活的時間可分為新生代和老年代
- 新生代又分為三個部分:一個內(nèi)存較大的Eden區(qū)瑟慈,和兩個內(nèi)存較小且大小相同的Survivor區(qū),比例為8:1:1.
- Eden區(qū)存放新生的對象
- Survivor存放每次垃圾回收后存活的對象
(1).新生代又分為三個部分:一個內(nèi)存較大的Eden區(qū)屋匕,和兩個內(nèi)存較小且大小相同的Survivor區(qū)葛碧,比例為8:1:1.
對象的內(nèi)存分配,主要分配在新生代的Eden(伊甸園)區(qū)上过吻,當Eden區(qū)沒有足夠的空間進行分配時进泼,虛擬機將發(fā)起一次“復(fù)制算法”的GC,在這個過程中纤虽,存活下來的對象被放到Survivor 0區(qū)乳绕;當?shù)诙蜧C來臨的時候,Survivor 0空間的存活對象也需要再次用復(fù)制算法逼纸,放到Survivor 1空間洋措,二把剛剛分配對象的Survivor 0空間和Eden空間清除;第三次GC時杰刽,又把Survivor 1空間的存活對象復(fù)制到Survivor 0的空間菠发,就這樣來回倒騰。
??通過上面的分析我們不難理解新生代為什么這么分配了:Eden區(qū)是對象分配的主要區(qū)域贺嫂,這是很頻繁的滓鸠,尤其是大量的局部變量產(chǎn)生的臨時對象,因此他占的比例為8/10涝婉,至于為什么是8哥力,這個我也不是很清楚,我們只需要知道這個區(qū)域確實占了很大比例就行墩弯;這個區(qū)域分配的對象大多數(shù)都是“朝生夕滅”吩跋,因此存活下來的對象較少,故采用“復(fù)制算法”渔工; 至于兩個Survivor的比例為什么是1:1锌钮,這個應(yīng)該很好理解。
(2).什么樣的對象會被移入老生帶引矩?
①.新生代中經(jīng)歷過15次GC的對象
虛擬機給每個對象定義了一個對象年齡計數(shù)器梁丘,如果對象在Eden出生并經(jīng)過第一次GC后仍然存活侵浸,將被移動到Survivor空間中,并且對象的年齡設(shè)為1氛谜;對象在Survivor區(qū)中每“熬過”一個GC掏觉,年齡就增加1歲,當它年齡增加到一定程度(默認為15歲)值漫,就會晉升到老年帶中澳腹。
②.大對象直接進入老年代
所謂大對象是指,需要連續(xù)內(nèi)存空間的Java對象杨何,最典型的大對象就是那種很長的字符串以及數(shù)組酱塔,虛擬機提供了一個PretenureSizeThreshold參數(shù),令大于這個這個值的對象直接在老生代中分配危虱。這樣做主要是為了避免在Eden區(qū)和兩個Survivor區(qū)之間復(fù)制算法執(zhí)行的時候產(chǎn)生大量的內(nèi)存復(fù)制羊娃。
4.觸發(fā)GC的類型
了解這些是為了解決實際問題,Java虛擬機會把每次觸發(fā)GC的信息打印出來來幫助我們分析問題埃跷,所以掌握觸發(fā)GC的類型是分析日志的基礎(chǔ)蕊玷。
GC_FOR_MALLOC: 表示是在堆上分配對象時內(nèi)存不足觸發(fā)的GC。
GC_CONCURRENT: 當我們應(yīng)用程序的堆內(nèi)存達到一定量捌蚊,或者可以理解為快要滿的時候集畅,系統(tǒng)會自動觸發(fā)GC操作來釋放內(nèi)存。
GC_EXPLICIT: 表示是應(yīng)用程序調(diào)用System.gc缅糟、VMRuntime.gc接口或者收到SIGUSR1信號時觸發(fā)的GC挺智。
GC_BEFORE_OOM: 表示是在準備拋OOM異常之前進行的最后努力而觸發(fā)的GC。
5.安卓分配與回收
Android系統(tǒng)并不會對Heap中空閑內(nèi)存區(qū)域做碎片整理窗宦。系統(tǒng)僅僅會在新的內(nèi)存分配之前判斷Heap的尾端剩余空間是否足夠赦颇,如果空間不夠會觸發(fā)gc操作,從而騰出更多空閑的內(nèi)存空間赴涵。
??在Android的高級系統(tǒng)版本里面針對Heap空間有一個Generational Heap Memory的模型媒怯,這個思想和JVM的逐代回收法很類似,就是最近分配的對象會存放在Young Generation區(qū)域髓窜,當這個對象在這個區(qū)域停留的時間達到一定程度扇苞,它會被移動到Old Generation,最后累積一定時間再移動到Permanent Generation區(qū)域寄纵。系統(tǒng)會根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的gc操作鳖敷。
站在巨人的肩膀上摘蘋果:
理解Java垃圾回收機制
從虛擬機視角談 Java 應(yīng)用性能優(yōu)化
Dalvik 虛擬機和 Sun JVM 在架構(gòu)和執(zhí)行方面有什么本質(zhì)區(qū)別?
JVM程拭、DVM(Dalvik VM)和ART虛擬機對比
《深入理解Java虛擬機 第2版》