俗話說,自己寫的代碼,6個月後也是别人的代碼……複習!複習!複習!涉及到的知識點總結如下:
堆棧是棧
jvm棧和本地方法棧劃分
java中的堆,棧和c/c++中的堆,棧
資料結構層面的堆,棧
os層面的堆,棧
jvm的堆,棧和os如何對應
為啥方法的調用需要棧
屬于月經問題了,正好碰上有人問我這類比較基礎的知識,無奈我自覺回答不是有效果,現在深入淺出的總結下:
前一篇文章總結了:jvm 的記憶體主要分為3個分區 堆區(heap)-- 隻存對象(數組)本身(引用類型的資料),不存基本類型和對象的引用。jvm隻有一個堆區,這個“堆”是動态記憶體配置設定意義上的堆——用于管理動态生命周期的記憶體區域。jvm的堆被同一個jvm執行個體中的所有java線程共享,它通常由某種自動記憶體管理機制所管理,這種機制通常叫做“垃圾回收”(garbage collection,gc)。jvm規範并不強制要求jvm實作采用哪種gc算法。 棧區(stack)-- 棧中隻儲存基礎資料類型的對象和對象引用。每個線程一個棧區,每個棧區中的資料都是私有的,其他棧不能通路。棧内有幀(方法調用會生成棧幀)分三個部分:基本類型變量區,執行環境上下文,操作指令區。 方法區 -- 又叫靜态區,跟堆一樣,被所有線程共享。方法區包含所有的class和static變量。方法區包含的都是在整個程式中永遠唯一的元素。如:class,satic。
堆棧是啥?是堆還是棧?
之前初學c++的時候被人誤導過,說堆棧是堆……其實這個是翻譯的誤讀,堆棧,其實應該翻譯成棧更合适,和堆區分開來,因為英文的stack就是堆棧的意思, 位于ram(random access memory,随機通路存儲區),速度僅次于寄存器。存放基本變量和引用,存在棧中的資料可以共享。但是,棧中的資料大小和生存周期必須确定,這是棧的缺點。
堆棧不是堆,是棧。堆是存放了所有的java對象(逃逸分析除外)。
本地方法棧和jvm棧是如何劃分的?
jvm規範寫到:每個java線程擁有自己的獨立的jvm棧,也就是java方法的調用棧。同時jvm規範為了允許native代碼可以調用java代碼,以及允許java代碼調用native方法,還規定每個java線程擁有自己的獨立的native方法棧。都是jvm規範所規定的概念上的東西,并不是說具體的jvm實作真的要給每個java線程開兩個獨立的棧。以oracle jdk / openjdk的hotspot vm為例,它使用所謂的“mixed stack”——在同一個調用棧裡存放java方法的棧幀與native方法的棧幀,是以每個java線程其實隻有一個調用棧,融合了jvm規範的jvm棧與native方法棧這倆概念。如之前文章1的結構圖:
資料結構層面的堆和棧
資料結構裡面。
stack,中文翻譯為堆棧,其實指的是棧,這裡講的是資料結構的棧,不是記憶體配置設定裡面的堆和棧。棧是先進後出的資料的結構,好比你碟子一個一個堆起來,最後放的那個是堆在最上面的。
棧資料結構比較簡單。heap翻譯為堆,是
一種有序的樹。
jvm的堆,棧和c、c++的堆、棧一樣麼?
回答這個問題之前,先得回答程式運作時的記憶體配置設定政策,編譯原理的理論認為:程式運作的記憶體配置設定有三個政策:
靜态存儲配置設定:在編譯時就能确定每個資料目标在運作時刻的存儲空間需求,因而在編譯時就可以給他們配置設定固定的記憶體空間。這種配置設定政策要求程式代碼中不允許有可變資料結構(如可變數組),也不允許有嵌套或者遞歸的結構出現。因為它們都會導緻編譯程式無法計算準确的存儲空間需求。
棧式存儲配置設定:也叫動态存儲配置設定,和靜态存儲配置設定相反,棧就是暫時!在棧式存儲方案中,存儲的都是局部變量,臨時變量,比如基本資料類型,對象引用……從記憶體的配置設定角度來看,因為存儲的是基本的資料類型,編譯器事先已經知道了類型的大小,故直接可以進行有效的記憶體配置設定,比如int,計算機是知道其範圍的,是以直接由系統配置設定在棧中,無需程式自己去申請xxx記憶體!而引用類型,比如自己定義一個類,很明顯這個類是不知道大小的,應該有程式自己來申請記憶體空間,是以由堆來配置設定!棧配置設定模式規定在運作中進入一個程式子產品時,必須知道該程式子產品所需的資料區大小,才能夠為其配置設定記憶體。和我們在資料結構所熟知的棧一樣,棧式存儲配置設定按照先進後出的原則進行配置設定。
堆式存儲配置設定:則專門負責在編譯時或運作時子產品入口處都無法确定存儲要求的資料結構的記憶體配置設定,比如可變長度串和對象執行個體。堆由大片的可利用塊或空閑塊組成,堆中的記憶體可以按照任意順序配置設定和釋放。
是以我們斷定:堆主要用來存放對象,棧主要是用來執行程式的。而這種不同又主要是由于堆和棧的特點決定的: 例如c/c++……所有的方法調用都是通過棧來進行的,所有的局部變量,形式參數都是從棧中配置設定記憶體空間,就好像工廠中的傳送(conveyor belt)帶一樣,stack top pointer 會自動指引你放東西的位置,你所要做的隻是把東西放下來就行。退出函數的時候,修改棧頂指針就可以把棧中的内容銷毀。這樣的模式速度最快,當然要用來運作程式了。
現在言歸正傳,之前的文章1已經總結了——jvm是基于堆棧的虛拟機。每一個jvm執行個體都為每個新建立的線程配置設定一個棧,而多個線程共享唯一一個堆區,也就是說,對于一個java程式來說,它的運作就是通過對棧的操作來完成的。棧以幀為機關儲存線程的狀态。jvm對棧隻進行兩種操作:以幀為機關的壓棧和出棧操作。當某個線程正在執行某個方法時,我們就稱此線程為目前方法,而目前方法使用的幀稱為目前幀。當線程要調用一個java方法時,jvm就會先線上程的java棧裡新壓入一個幀。這個幀自然成為了目前幀。在此方法執行期間,這個幀将用來儲存方法的形參,局部變量,中間計算過程和其他資料……這個幀在這裡和編譯原理中的活動紀錄的概念是差不多的。
好了,羅嗦了半天,從這個棧式配置設定機制來看,棧可以這樣了解:棧(stack)是os在建立某個程序或者線程(在支援多線程的作業系統中是線程)時,為這個(程序)線程建立的存儲區域,該區域具有先進後出的特性。棧中的新加資料項放在其他資料的頂部,移除時你也隻能移除最頂部的資料(不能越位擷取)。類似這個紙:
再說堆,每一個jvm的執行個體有且隻有一個堆,這個唯一的堆被全局的線程共享!程式在運作中所建立的所有類執行個體或數組都放堆中,并由應用所有的線程共享。堆中的資料項位置沒有固定的順序,你可以以任何順序插入和删除,因為他們沒有“頂部”資料這一概念。如圖:
跟c/c++不同,java中配置設定堆記憶體是自動化管理的(java虛拟機的自動垃圾回收器來管理,缺點是,由于要在運作時動态配置設定記憶體,存取速度較慢)java中所有對象的存儲空間都是在堆中配置設定,但對象引用是在棧中配置設定,而堆中配置設定的記憶體才是實際的這個對象本身,棧中配置設定的記憶體隻是一個指向這個對象的指針(引用)變量而已(變量的取值等于數組或對象在堆記憶體中的首位址)。而c++的堆記憶體管理,需要程式員手動管理的,new,delete運算符……
記憶體管理中的棧配置設定方法有什麼特點?優缺點又是什麼?
首先想到就是該快記憶體filo的特性,還有經過前面這麼羅嗦的哔哔,又得出一個結論:棧中的資料可以共享。
編譯器先處理int a = 3; 會在棧中建立一個變量為a的引用,然後查找棧中是否有3這個值,如果沒找到,就将3存放進來,然後将a指向3。接着處理int b = 3; 在建立完b的引用變量後,因為在棧中已經有3這個值,便将b直接指向3。這樣,就出現了a與b同時均指向3的情況。這時,如果再令a=4; 那麼編譯器會重新搜尋棧中是否有4值,如果沒有,則将4存放進來,并令a指向4; 如果已經有了,則直接将a指向這個位址。是以a值的改變不會影響到b的值。要注意這種資料的共享與兩個對象的引用同時指向一個對象的這種共享是不同的,因為這種情況a的修改并不會影響到b, 它是由編譯器完成的,它有利于節省空間。而一個對象引用變量修改了這個對象的内部狀态,會影響到另一個對象引用變量。
優點:速度快,不用管理記憶體,缺點是太小,方法調用過度,容易記憶體溢出,還有棧就是暫時,資料有生命周期,屬于臨時存儲。
站在實際的計算機實體記憶體的角度上看,棧和堆在哪兒?
在通常情況下由作業系統(os)和語言的運作時(runtime)控制嗎?
它們的作用範圍是什麼?
它們的大小由什麼決定?
哪個更快?
jvm的棧如何對應os?
回答這個問題之前,必須先知道記憶體管理的機制根據不同的編譯器和處理器架構的不同而不同!為了幫助了解,先總結幾個原理:
什麼是局部性原理?
os的教科書這樣寫到局部性原理:cpu通路存儲器時,無論是存取指令還是存取資料,所通路的存儲單元都趨于聚集在一個較小的連續區域中。
我是這樣了解的:計算機的存儲系統從小到大,分為寄存器,一級緩存,二級緩存,三級緩存,記憶體,磁盤……而寄存器是cpu存放計算資料的地方,cpu要工作了,需要資料或者位址,先從一級緩存裡面找,找不到就從二級緩存裡面找,二級找不到就去三級找……假如找到磁盤才有了目标資料,那麼該資料就會先放入記憶體,再存入三級緩存、二級緩存、一級緩存,最後存入寄存器,才能被cpu使用。可以說,一級緩存是寄存器的緩存,二級緩存是一級緩存的緩存,三級緩存是二級緩存的緩存……下面一層是上面一層的緩存。而局部性原理,通俗的說就是因為cpu的運轉速度非常非常快!是高速存儲的!而磁盤和記憶體之間的存取速度很慢(i/o瓶頸繞不開……),如果cpu需要的資料更多的在磁盤,記憶體……這樣會花非常多的等待時間,故我們就設定了高速緩存!當cpu頻繁的用了某塊資料,計算機會遇見性的把它及其附近位址上的資料都存入高速緩存内,因為預判這些資料再次被用到的可能性很大,計算機就把它們存到越接近寄存器的層次,也就是cpu所通路的資料,都趨于集中在一個較小的連續區域中,這也才是緩存的真正意義。那麼,現在的問題就變為回答:
計算機怎樣才能判斷一個資料接下來可能被用到?
時間局部性:如果一個資料正在被通路,那麼在近期它很可能還會被再次通路。這當然是正确的,用過的資料當然可能再次被用到。
空間局部性:在最近的将來将用到的資訊很可能與現在正在使用的資訊在空間位址上是臨近的,正在使用的這個資料位址旁邊的資料,當然也是很可能被用到的。比如數組什麼的……
哦了。前面幾個問題已經得出這樣的結論:棧和堆都是用來從底層作業系統中擷取記憶體的。在多線程環境下每一個線程都可以有他自己完全的獨立的棧,但是他們共享堆。并行存取被堆控制而不是棧。
堆:包含一個連結清單來維護已用和空閑的記憶體塊。
在堆上新配置設定(用 new 或者 malloc)記憶體是從空閑的記憶體塊中找到一些滿足要求的合适塊。這個操作會更新堆中的塊連結清單。這些元資訊也存儲在堆上,經常在每個塊的頭部一個很小區域。堆增加新塊通常從低位址向高位址擴充,也就是說堆是向上增長的!是以可以認為堆随着記憶體配置設定而不斷的增加大小。如果申請的記憶體大小很小的話,通常從底層作業系統中得到比申請大小要多的記憶體。申請和釋放許多小的塊可能會産生如下狀态:在已用塊之間存在很多小的浪費的空閑塊……進而導緻申請大塊記憶體失敗,雖然空閑塊的總和足夠,但是空閑的小塊是零散的,不能滿足申請的大小,這叫做“記憶體碎片”。當旁邊有空閑塊的已用塊被釋放時,新的空閑塊可能會與相鄰的空閑塊合并為一個大的空閑塊,這樣可以有效的減少“碎片”的産生。
堆的管理依賴于運作時環境,c 使用 malloc ,free,c++ 使用 new 和delete,但是很多語言有垃圾回收機制,比如java的gc。
棧:棧經常與 sp 寄存器一起工作,最初 sp 指向棧頂(棧的高位址)。棧是向下增長的!
cpu 用 push 指令來将資料壓棧,用 pop 指令來彈棧。當用 push 壓棧時,sp 值減少(向低位址擴充)。當用 pop 彈棧時,sp 值增大。存儲和擷取資料都是 cpu 寄存器的值。
當函數被調用時,cpu使用特定的指令把目前的 ip 壓棧,接下來将調用函數的位址賦給 ip,讓cpu去調用函數。當函數傳回時,舊的 ip 被彈棧,cpu 繼續去函數調用之前的代碼。
當進入函數時,sp 向下擴充,擴充到確定為函數的局部變量留足夠大小的空間。如果函數中有一個 32-bit 的局部變量會在棧中留夠四位元組的空間。當函數傳回時,sp 通過傳回原來的位置來釋放空間。
如果函數有參數的話,在函數調用之前,會将參數壓棧。函數中的代碼通過 sp 的目前位置來定位參數并通路它們。
函數嵌套調用,每一次新調用的函數都會配置設定函數參數,傳回值位址、局部變量空間、嵌套調用的活動記錄都要被壓入棧中。函數傳回時,按照正确方式的撤銷。
棧要受到記憶體塊的限制,不斷的函數嵌套……為局部變量配置設定太多的空間,可能會導緻棧溢出。當棧中的記憶體區域都已經被使用完之後繼續向下寫(低位址),會觸發一個 cpu 異常。這個異常接下會通過語言的運作時轉成各種類型的棧溢出異常。總的來說,棧以更低層次的特性與處理器架構緊密的結合到一起,當堆不夠時可以擴充空間。但是,擴充棧通常來說是不可能的,因為在棧溢出的時候,執行線程就被作業系統關閉了,這已經太晚了。
現在可以回答這幾個問題:
在通常情況下由作業系統(os)和語言的運作時(runtime)控制嗎?
如前所述,堆和棧是一個統稱,可以有很多的實作方式。計算機程式通常有一個棧叫做調用棧,用來存儲目前函數調用相關的資訊(比如:主調函數的位址,局部變量),因為函數調用之後需要傳回給主調函數。棧通過擴充和收縮來承載資訊。實際上,程式不是由運作時來控制的,它由程式設計語言、作業系統甚至是系統架構來決定。堆是在任何記憶體中動态和随機配置設定的(記憶體的)統稱;也就是無序的。記憶體通常由作業系統配置設定,通過應用程式調用 api 接口去實作配置設定。在管理動态配置設定記憶體上會有一些額外的開銷,不過這由作業系統來處理。
它們的作用範圍是什麼?
調用棧是一個低層次的概念,就程式而言,它和“作用範圍”沒什麼關系。就進階語言而言,語言有它自己的範圍規則。一旦函數傳回,函數中的局部變量會直接釋放。在堆中,也很難去定義。作用範圍是由作業系統限定的,但是程式設計語言可能增加它自己的一些規則,去限定堆在應用程式中的範圍。體系架構和作業系統是使用虛拟位址的,然後由處理器翻譯到實際的實體位址中,還有頁面錯誤等等。它們記錄那個頁面屬于那個應用程式。不過你不用關心這些,因為你僅僅在程式設計語言中配置設定和釋放記憶體,和一些錯誤檢查(出現配置設定失敗和釋放失敗的原因)。
它們的大小由什麼決定?
依賴于語言,編譯器,作業系統和架構。棧通常提前配置設定好了,因為棧必須是連續的記憶體塊。語言的編譯器或者作業系統決定它的大小。不要在棧上存儲大塊資料,這樣可以保證有足夠的空間不會溢出,除非出現了無限遞歸的情況或者其它。堆是任何可以動态配置設定的記憶體的統稱。它的大小是變動的。在現代處理器中和作業系統的工作方式是高度抽象的,是以你在正常情況下不需要擔心它實際的大小,除非你必須要使用你還沒有配置設定的記憶體或者已經釋放了的記憶體。
哪個更快一些?
棧更快因為所有的空閑記憶體都是連續的,是以不需要對空閑記憶體塊通過清單來維護。隻是一個簡單的指向目前棧頂的指針。編譯器通常用一個專門的、快速的寄存器sp來實作。更重要的一點是,随後的棧上操作會遵循局部性原理。
jvm的棧如何對應os?
以linux 中一個程序的虛拟記憶體分布為例:
圖中0号位址在最下邊,越往上記憶體位址越大。以32位作業系統為例,一個程序可擁有的虛拟記憶體位址範圍為0-2^32。分為兩部分,一部分留給kernel使用(kernel virtual memory),剩下的是程序本身使用, 即圖中的process virtual memory。普通java 程式使用的就是process virtual memory。上圖中最頂端的一部分記憶體叫做user stack. 這就是棧stack,32位的棧頂指針寄存器是esp,中間有 runtime heap。就是堆,注意他們和資料結構裡的stack 和 heap 不是一回事。前面總結了,stack 是向下生長的,heap是向上生長的。當程式進行函數調用時,每個函數都在stack上有一個 call frame(幀)。
小結,總結了那麼多,現在最後一個問題:為啥方法的調用需要棧
其實确切的說:并不是方法的調用需要用棧來實作,而是它設計成用棧實作!我們知道,各個方法的活動記錄(即局部或者自動變量)被配置設定在棧上, 這樣做不但存儲了這些變量,而且可以用來嵌套方法的追蹤。因為我們經過觀察可以知道,方法的調用過程是這樣的:
1,計算參數,傳參
2,儲存方法的傳回位址
3,控制轉移至callee
4,儲存必要的caller現場
以上一些步驟之間的順序是可變的,但理論上并沒有哪個步驟是必須用棧來實作的。理論上如果有很多寄存器,我們完全可以抛棄棧,然而實際上我們并沒有,是以從現實的角度來說,棧是一個适合的實作方法,簡單說就是方法調用的局部資料的存活時間滿足“先進後出(filo)”的順序,之是以用棧來記錄是因為棧的基本操作正好就是支援這種順序的通路。而堆是無法實作的。
文章總結參考資料:《java程式設計思想》、《現代作業系統》,《深入了解計算機系統》、《現代編譯原理》,《深入了解java虛拟機》、《jvm規範 7》、知乎、stackoverflow……
辛苦的勞動,轉載請注明出處,謝謝……