android開(kāi)發(fā)需要了解的堆和棧

引言

Android開(kāi)發(fā)中經(jīng)常會(huì)遇到各種內(nèi)存問(wèn)題,比如內(nèi)存溢出,內(nèi)存泄露,棧溢出等常見(jiàn)的問(wèn)題,也會(huì)經(jīng)常聽(tīng)到關(guān)于內(nèi)存中的堆的概念和棧的概念,要想更好的解決這些問(wèn)題,還是得站在一個(gè)整體的高度對(duì)這些東西有一個(gè)全面的認(rèn)識(shí)

jvm虛擬機(jī)

每一個(gè)Android的app進(jìn)程都是運(yùn)行在一個(gè)獨(dú)立的jvm虛擬機(jī)上面,通過(guò)adb shell ps可以簡(jiǎn)單的看出用戶(hù)進(jìn)程的父進(jìn)程都是zygote進(jìn)程,往上追溯就能發(fā)現(xiàn)zygote進(jìn)程里面初始化了一個(gè)獨(dú)立的jvm虛擬機(jī),所以簡(jiǎn)單的理解就是每個(gè)用戶(hù)進(jìn)程都是運(yùn)行在獨(dú)立的jvm環(huán)境中,這樣也保證了一個(gè)app的崩潰不會(huì)影響其他app進(jìn)程的運(yùn)行。

不管是java文件還是kotlin文件,通過(guò)編譯后都是.class的java字節(jié)碼文件,jvm的類(lèi)加載機(jī)制加載.class文件,規(guī)定了一套自己的內(nèi)存模型,所以Android的應(yīng)用程序中的內(nèi)存分配也就跟jvm的內(nèi)存模型嚴(yán)格對(duì)應(yīng)起來(lái)。

拋開(kāi)jvm中的類(lèi)加載器和執(zhí)行引擎,單純的分析和內(nèi)存相關(guān)的運(yùn)行時(shí)數(shù)據(jù)區(qū),主要分為線(xiàn)程共享的內(nèi)存區(qū)域和線(xiàn)程獨(dú)立的內(nèi)存區(qū)域,而在線(xiàn)程共享的區(qū)域里面就有我們最常見(jiàn)的堆區(qū),還有一個(gè)方法區(qū),而線(xiàn)程獨(dú)占的區(qū)域里面就是常見(jiàn)的概念棧


jvm運(yùn)行時(shí)數(shù)據(jù)區(qū).png
方法區(qū)

每一個(gè)jvm有一個(gè)方法區(qū),生命周期跟jvm一致,里面放的數(shù)據(jù)有

  • 類(lèi)信息(.class類(lèi)文件解析的信息,用于后續(xù)對(duì)象的實(shí)例)

  • 常量/靜態(tài)變量(static/static final修飾的變量和常量也分配在方法區(qū))

  • 即時(shí)編譯期編譯的代碼(編譯就是將.java文件編譯成.class文件過(guò)程后的內(nèi)容)

堆區(qū)
  • 數(shù)組

  • 對(duì)象實(shí)例(基本上所有通過(guò)new關(guān)鍵字實(shí)例的對(duì)象都分配在對(duì)象,當(dāng)然也存在對(duì)象逃逸在棧上分配)

在堆上的對(duì)象也是所謂的gc垃圾回收主要回收的目標(biāo),而堆里面又有不同的細(xì)分區(qū)域,在后面會(huì)分析到

這一部分是線(xiàn)程共享的,不同的線(xiàn)程都可以進(jìn)行實(shí)例化對(duì)象,實(shí)例化對(duì)象就需要找到對(duì)應(yīng)的.class文件,所以方法區(qū)中的是線(xiàn)程共享的,而對(duì)象的實(shí)例化分配在堆上,不同的線(xiàn)程中持有的是對(duì)象的引用,這部分基本上就是一個(gè)內(nèi)存地址,所以這部分也是線(xiàn)程可以共享的

虛擬機(jī)棧

這部分是線(xiàn)程私有的,意思就是不同的線(xiàn)程就會(huì)有自己的虛擬機(jī)棧,而這個(gè)棧里面又是存放的一個(gè)一個(gè)的棧幀

  • 棧幀:就是線(xiàn)程中運(yùn)行的方法,當(dāng)調(diào)用其他方法的時(shí)候,就會(huì)產(chǎn)生一個(gè)新的棧幀壓入虛擬機(jī)棧,當(dāng)運(yùn)行完對(duì)應(yīng)的方法,方法出棧,接著運(yùn)行之前壓棧的方法,保證了方法的執(zhí)行順序
//舉個(gè)栗子
public void main() {
       a();
       b();
  }

private void a() {}

private void b() {}

比如線(xiàn)程執(zhí)行到main()方法,就會(huì)在對(duì)應(yīng)線(xiàn)程的虛擬機(jī)棧中產(chǎn)生一個(gè)main()方法的棧幀,然后執(zhí)行到對(duì)應(yīng)的a()/b()方法,又會(huì)產(chǎn)生兩個(gè)棧幀壓棧

線(xiàn)程方法棧.png

棧幀的示意結(jié)構(gòu),不同的方法就會(huì)產(chǎn)生不同的棧幀,然后棧幀的結(jié)構(gòu)里面有包含了【局部變量表/操作數(shù)棧/動(dòng)態(tài)鏈接/正常錯(cuò)誤返回】

局部變量表

局部變量表就是保存方法中局部變量引用,局部變量的作用域就是在對(duì)應(yīng)的方法體中,屬于棧幀私有的變量

操作數(shù)棧

雖然更虛擬機(jī)棧都是棧,但是一般說(shuō)的對(duì)象分配分配中的棧是指虛擬機(jī)棧,這個(gè)操作數(shù)棧的理解,就跟Java的字節(jié)碼指令有關(guān)系了,會(huì)將計(jì)算數(shù)和符合不斷的壓棧出棧進(jìn)行計(jì)算,這部分就不細(xì)講了

本地方法棧

本地方法棧就是一些native方法的調(diào)用棧,也是線(xiàn)程私有的

程序計(jì)數(shù)器

這個(gè)跟cpu的調(diào)度有關(guān),時(shí)間片輪轉(zhuǎn)會(huì)讓不同的線(xiàn)程都會(huì)有執(zhí)行的機(jī)會(huì),然后線(xiàn)程在運(yùn)行的時(shí)候,當(dāng)時(shí)間片運(yùn)行結(jié)束,就會(huì)掛起,這會(huì)就需要記錄當(dāng)前執(zhí)行的程序字節(jié)碼位置,以便后續(xù)得到時(shí)間片后繼續(xù)運(yùn)行

小結(jié)

上面就分析了jvm運(yùn)行時(shí)數(shù)據(jù)區(qū)的不同分區(qū),jvm就是一套內(nèi)存模型規(guī)范,所以程序運(yùn)行時(shí)就會(huì)把不同的數(shù)據(jù)存儲(chǔ)在不同的內(nèi)存地址區(qū)域


jvm運(yùn)行時(shí)數(shù)據(jù)區(qū).png
堆和棧

上面知道了堆和棧性質(zhì),那么對(duì)比分析一下

  • 用途

堆:用來(lái)存儲(chǔ)java中實(shí)例化的對(duì)象,不管是成員變量,局部變量還是類(lèi)變量,他們引用的實(shí)例化對(duì)象都是存儲(chǔ)在堆中

棧:用來(lái)存儲(chǔ)方法的調(diào)用過(guò)程,以棧幀的形式在棧上存儲(chǔ),棧上可以存儲(chǔ)方法調(diào)用過(guò)程中的局部變量,但是僅限于基本數(shù)據(jù)類(lèi)型的變量和對(duì)象的引用,這部分?jǐn)?shù)據(jù)是分配在棧上的,隨著方法執(zhí)行完,棧幀出棧,對(duì)應(yīng)的變量就會(huì)被自動(dòng)釋放

  • 跟線(xiàn)程的關(guān)系

堆:堆中的對(duì)象是所以線(xiàn)程共享的,分配在堆上的對(duì)象可以被所有線(xiàn)程訪(fǎng)問(wèn)

棧:棧是歸屬于單個(gè)線(xiàn)程,所以棧內(nèi)存中的變量也不用考慮線(xiàn)程同步的問(wèn)題,屬于線(xiàn)程的私有內(nèi)存

  • 大小

堆:堆的大小跟jvm設(shè)計(jì)相關(guān),可以通過(guò)-Xmx設(shè)置堆區(qū)內(nèi)存可被分配的上限,使用-Xms設(shè)置堆區(qū)的初始大小

棧:棧幀的局部變量表和操作數(shù)棧的大小也是編譯時(shí)確定的,取決于jvm虛擬機(jī)的實(shí)現(xiàn),棧的深度也是有限的,如果棧幀超出了限制,就會(huì)拋出常見(jiàn)的StackOverFlow的錯(cuò)誤

深入堆

一般在開(kāi)發(fā)中,打交道最多的還是堆上的內(nèi)存對(duì)象,內(nèi)存溢出,內(nèi)存泄露等問(wèn)題更多的是出現(xiàn)在堆上的對(duì)象沒(méi)法回收,堆上的內(nèi)存滿(mǎn)了,無(wú)法繼續(xù)分配導(dǎo)致,當(dāng)然由于機(jī)器的內(nèi)存是固定的大小,也會(huì)出現(xiàn)棧上的內(nèi)存溢出,或者方法區(qū)的內(nèi)存溢出

new對(duì)象

當(dāng)遇到new關(guān)鍵字去實(shí)例化一個(gè)對(duì)象,就會(huì)進(jìn)行類(lèi)加載檢測(cè),如果類(lèi)加載器中檢測(cè)出對(duì)應(yīng)的.class文件,就會(huì)去堆上分配內(nèi)存,分配了內(nèi)存會(huì)會(huì)對(duì)內(nèi)存空間進(jìn)行初始化,比如對(duì)基本類(lèi)型設(shè)置初始值,然后才是設(shè)置對(duì)應(yīng)對(duì)象中的賦值和引用

對(duì)象的大小

總是在說(shuō)對(duì)象會(huì)在堆上占內(nèi)存,那么一個(gè)對(duì)象到底占多數(shù)內(nèi)存,對(duì)象是由三部分組成的,對(duì)象頭+實(shí)例數(shù)據(jù)+對(duì)齊

對(duì)象頭

對(duì)象頭大小占8個(gè)字節(jié),里面會(huì)包含有對(duì)象的哈希值,GC的分代年齡,鎖狀態(tài),線(xiàn)程持有的鎖,偏向線(xiàn)程id等信息,還會(huì)包含類(lèi)型指針,如果對(duì)象是數(shù)組,對(duì)象頭中還會(huì)記錄數(shù)組的長(zhǎng)度

實(shí)例數(shù)據(jù)

實(shí)例數(shù)據(jù)中不同的基本類(lèi)型所占的空間大小不一樣

對(duì)齊

由于計(jì)算機(jī)的位數(shù),為了更方便的存取對(duì)象,會(huì)對(duì)對(duì)象大小進(jìn)行一個(gè)8字節(jié)整數(shù)倍的對(duì)齊


java對(duì)象內(nèi)存分布.png
對(duì)象的分配

實(shí)例化了對(duì)象后,就會(huì)將對(duì)象進(jìn)行一個(gè)堆上的分配,這會(huì)就得提到堆上對(duì)應(yīng)的分區(qū),都是一些非常常見(jiàn)的概念

堆內(nèi)存區(qū)域劃分了多個(gè)區(qū)域用來(lái)存儲(chǔ)不同的對(duì)象,這樣的目的主要是為了更效率的垃圾回收,主要有新生代和老年代,新生代里面又分了eden區(qū),survive區(qū)

整體來(lái)說(shuō),新生代和老年代的大小是個(gè)1:2的關(guān)系,新生代中eden區(qū)和survive區(qū)是一個(gè)8:2的關(guān)系,survive區(qū)又分為了兩個(gè)一樣大小的S0和S1區(qū)域

堆上的這么劃分,主要就是為了垃圾回收的效率,當(dāng)一個(gè)分區(qū)內(nèi)存分配滿(mǎn)了后,都會(huì)觸發(fā)gc,而gc回收又是一個(gè)占cpu的行為,也可能停止用戶(hù)線(xiàn)程,所以更好的分配策略和回收算法,可以提供更好的用戶(hù)體驗(yàn)

step1

對(duì)象的創(chuàng)建基本都是直接分配在新生代的eden區(qū),除了一些大對(duì)象會(huì)直接進(jìn)入老年區(qū),很明顯的eden區(qū)的內(nèi)存空間有限,分配大對(duì)象可能很容易觸發(fā)gc,所以大對(duì)象會(huì)直接進(jìn)入老年代

step2

當(dāng)eden區(qū)滿(mǎn)了后,如果之前實(shí)例化的對(duì)象還存活,就會(huì)被復(fù)制到S0區(qū)域,由于對(duì)象的生命特性,eden區(qū)中絕大多數(shù)的對(duì)象都會(huì)被回收清理掉,所以8:1:1的設(shè)計(jì)也是為了更好的空間利用,當(dāng)從eden去移動(dòng)到S0區(qū)域的時(shí)候,對(duì)象頭上面的分代年齡就會(huì)+1

step3

當(dāng)繼續(xù)觸發(fā)gc進(jìn)行垃圾回收,存活在S0中的對(duì)象,就會(huì)被標(biāo)記整理到S1區(qū)域中,S0區(qū)域格式化清空,然后對(duì)象的分代年齡繼續(xù)+1,隨著gc的進(jìn)行,一直重復(fù)在survive區(qū)中來(lái)回交換

step4

當(dāng)對(duì)象的分代年齡達(dá)到15的時(shí)候(一般情況,可以設(shè)置),長(zhǎng)時(shí)間的回收不掉,就會(huì)把對(duì)象移動(dòng)到老年代,所以老年代中的對(duì)象生命周期都比較長(zhǎng),到后續(xù)對(duì)象直到生命周期結(jié)束前都在老年代中,直到被回收

持久代

堆上的內(nèi)存分為新生代和老年代,對(duì)應(yīng)的還有一個(gè)持久代的概念,這個(gè)就是最上面jvm內(nèi)存模型里面的方法區(qū),用來(lái)存放類(lèi)文件和靜態(tài)類(lèi)型數(shù)據(jù),放在一起對(duì)比概念叫法


堆內(nèi)存和持久代.png
gc

最后需要提到的概念就是gc,對(duì)象實(shí)例化后會(huì)占據(jù)內(nèi)存,如果內(nèi)存不夠分配,就會(huì)觸發(fā)gc,去回收可以會(huì)回收的內(nèi)存,怎么判斷這個(gè)可以回收的內(nèi)存,就是常見(jiàn)的根可達(dá)算法,GC Roots

GC Roots

可以作為GC Roots的變量就是一些長(zhǎng)期存活或者短期內(nèi)不會(huì)被回收的變量

  • 靜態(tài)變量/常量池(方法區(qū)中,gc回收在堆區(qū),方法區(qū)中能跟進(jìn)程生命周期一致)

  • 線(xiàn)程棧變量(線(xiàn)程的局部變量在棧上存活,方法沒(méi)有執(zhí)行完,棧幀不會(huì)被回收釋放)

  • JNI指針(也就是本地方法棧,跟線(xiàn)程棧理解類(lèi)似)

如果一個(gè)對(duì)象的引用鏈最頭部是一個(gè)GC Roots對(duì)象,那么對(duì)應(yīng)引用鏈上的對(duì)象就不可被回收,反之,如果沒(méi)有被GC Roots持有,那就是gc回收的對(duì)象

引用類(lèi)型

上面提到的gc roots引用關(guān)系,就是一般的=賦值的強(qiáng)引用,Java中處理強(qiáng)引用,還有其他類(lèi)型的引用,也一并記錄

  • 強(qiáng)引用-> =賦值的操作,如果有g(shù)c roots的引用關(guān)系,就不可被回收,即使拋出OOM,也不會(huì)去回收

  • 軟引用-> SoftReference,在內(nèi)存不足的時(shí)候,觸發(fā)gc,如果gc后還是不足,就會(huì)回收軟引用對(duì)象

  • 弱引用->WeakReference,觸發(fā)gc就會(huì)被回收

  • 虛引用->PhantomRefrence,這個(gè)用的比較少,直接通過(guò)虛引用get對(duì)象會(huì)是一個(gè)null值,這個(gè)的用途設(shè)計(jì)到一個(gè)ReferenceQueue,就不詳細(xì)分析了

gc清理方式

在知道了哪些對(duì)象可以回收的情況下,最后需要了解的概念就是gc在不同的堆內(nèi)存分代中的回收方式

可能最上面對(duì)于新生代的8:1:1的空間劃分存在疑惑,這點(diǎn)也主要和清理方式有關(guān)

復(fù)制算法

復(fù)制算法就是把內(nèi)存空間分成兩等分,拿一份作為預(yù)留空間,在需要gc回收的時(shí)候,將其中回收的區(qū)域內(nèi)存活的對(duì)象,復(fù)制到對(duì)應(yīng)預(yù)留區(qū)聯(lián)系的內(nèi)存空間內(nèi),這樣的回收方式后都是連續(xù)的內(nèi)存空間,但是預(yù)留一半的,會(huì)造成空間的利用率降低,這就是典型的堆區(qū)新生代中的survive區(qū)1:1的原因,這部分就使用了復(fù)制算法進(jìn)行清理

那么為什么是8:1:1,新生代還得劃分一個(gè)eden區(qū),而不是5:5直接使用復(fù)制算法

因?yàn)橥ǔ6阎械膶?duì)象生命周期都比較短,一次gc后存活的對(duì)象很少,為了最大程度的提高空間利用率,就使用了8:1:1的比例

從eden取到S0的整理也是復(fù)制算法,將還存活的對(duì)象直接復(fù)制到survive中

標(biāo)記清理

標(biāo)記整理算法,就是遍歷堆中的存活對(duì)象和可清理對(duì)象,對(duì)需要清除的對(duì)象進(jìn)行標(biāo)記,然后再統(tǒng)一對(duì)這些標(biāo)記的對(duì)象進(jìn)行回收,這種算法的優(yōu)點(diǎn)就是不需要內(nèi)存復(fù)制,內(nèi)存也是100%利用,但是缺點(diǎn)就是會(huì)有內(nèi)存碎片,整理后的內(nèi)存不是連續(xù)的空間

標(biāo)記整理

相比于標(biāo)記清理產(chǎn)生內(nèi)存碎片,標(biāo)記整理算法,會(huì)把存活的對(duì)象復(fù)制移動(dòng)到連續(xù)的內(nèi)存空間,這樣空間的利用率是100%,但是效率會(huì)低很多,整理的時(shí)候,比如兩個(gè)對(duì)象中只要1m的空間,整理一個(gè)2m的對(duì)象過(guò)來(lái),還得進(jìn)行對(duì)象的移動(dòng)

對(duì)象的整理會(huì)反向觸發(fā)對(duì)象的引用地址的變更,所以對(duì)象的移動(dòng)會(huì)有額外的開(kāi)銷(xiāo)

總結(jié)

從jvm的內(nèi)存模型分區(qū),到對(duì)象的分配,和對(duì)象的回收都進(jìn)行了一遍梳理,具如果有錯(cuò)誤和不足還望路過(guò)的大佬指點(diǎn)一二

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容