介紹堆和棧以前先介紹些內存存儲預備知識:java
1)靜態的:靜態存儲分配指在編譯時就能肯定每一個數據目標在運行時刻的存儲空間需求,於是在編譯時就能夠給它們分配固定的內存空間,這種分配策略要求程序代碼中不容許有可變數據結構的存在,也不容許有嵌套或者遞歸的結構出現,由於它們都會致使編譯程序沒法計算準確的存儲空間需求。程序員
2)棧式的:棧式存儲分配也可稱爲動態存儲分配,是由一個相似於堆棧的運行棧類實現的,和靜態存儲分配相反,在棧式存儲方案中,程序對數據區的需求在編譯時是徹底未知的,只有到運行的時候纔可以知道,但規定在運行中進入一個程序模塊時,必須知道該程序模塊所需的數據區大小纔可以爲其分配內存,和數據結構中的棧同樣,按照後進先出的原則進行分配。編程
3)堆式的:靜態存儲分配要求在編譯時能肯定全部變量的存儲要求,棧式存儲分配要求在過程的入口處必須知道全部的存儲要求,而堆式存儲分配則專門負責在編譯時或運行時模塊入口都沒法肯定存儲要求的數據結構的內存分配。好比可變長度字符串、對象實例。堆由大片的可利用塊或空閒塊組成,堆中的內存能夠按照任意順序分配和釋放。數組
堆棧有兩種解釋,一種是數據結構中的堆棧,一種是操做系統內存存儲方式的堆棧。數據結構
數據結構的棧和堆:多線程
首先在數據結構上要知道堆棧,儘管咱們這麼稱呼它,但實際上堆棧是兩種數據結構:堆和棧。函數
堆和棧都是一種數據項按序排列的數據結構。性能
棧做爲一種數據結構,是一種只能在一端進行插入和刪除的特殊線性表,這一端稱爲棧頂,按照後進先出的原則存儲數據,先進入的數據被壓入棧底,最後的數據在棧頂,須要讀數據的時候從棧頂開始彈出數據。經常使用到的兩個操做出棧POP和入棧PUSH。操作系統
棧就像裝數據的桶或箱子,它是一種具備後進先出性質的數據結構,也就是說後存放的先取,先存放的後取。這就如同咱們要取出放在箱子裏面底下的東西(放入的比較早的物體),咱們首先要移開壓在它上面的物體(放入的比較晚的物體)。線程
堆像一棵倒過來的樹
堆是一種通過排序的樹形數據結構,每一個結點都有一個值。
一般咱們所說的堆的數據結構,是指二叉堆。
堆的特色是根結點的值最小(或最大),且根結點的兩個子樹也是一個堆。因爲堆的這個特性,經常使用來實現優先隊列,堆的存取是隨意,這就如同咱們在圖書館的書架上取書,雖然書的擺放是有順序的,可是咱們想取任意一本時沒必要像棧同樣,先取出前面全部的書,書架這種機制不一樣於箱子,咱們能夠直接取出咱們想要的書。
操做系統內存分配的堆和棧:
棧:
1)棧常常與 sp 寄存器(譯者注:」stack pointer」,瞭解彙編的朋友應該都知道)一塊兒工做,最初 sp 指向棧頂(棧的高地址)。
2)CPU 用 push 指令來將數據壓棧,用 pop 指令來彈棧。當用 push 壓棧時,sp 值減小(向低地址擴展)。當用 pop 彈棧時,sp 值增大。存儲和獲取數據都是 CPU 寄存器的值。
3)當函數被調用時,CPU使用特定的指令把當前的 IP (譯者注:「instruction pointer」,是一個寄存器,用來記錄 CPU 指令的位置)壓棧。即執行代碼的地址。CPU 接下來將調用函數地址賦給 IP ,進行調用。當函數返回時,舊的 IP 被彈棧,CPU 繼續去函數調用以前的代碼。
4)當進入函數時,sp 向下擴展,擴展到確保爲函數的局部變量留足夠大小的空間。若是函數中有一個 32-bit 的局部變量會在棧中留夠四字節的空間。當函數返回時,sp 經過返回原來的位置來釋放空間。
5)若是函數有參數的話,在函數調用以前,會將參數壓棧。函數中的代碼經過 sp 的當前位置來定位參數並訪問它們。
6)函數嵌套調用和使用魔法同樣,每一次新調用的函數都會分配函數參數,返回值地址、局部變量空間、嵌套調用的活動記錄都要被壓入棧中。函數返回時,按照正確方式的撤銷。
7)棧要受到內存塊的限制,不斷的函數嵌套/爲局部變量分配太多的空間,可能會致使棧溢出。當棧中的內存區域都已經被使用完以後繼續向下寫(低地址),會觸發一個 CPU 異常。這個異常接下來會經過語言的運行時轉成各類類型的棧溢出異常。(譯者注:「不一樣語言的異常提示不一樣,所以經過語言運行時來轉換」我想他表達的是這個含義)。
堆:
1)堆包含一個鏈表來維護已用和空閒的內存塊。在堆上新分配(用 new 或者 malloc)內存是從空閒的內存塊中找到一些知足要求的合適塊。這個操做會更新堆中的塊鏈表。這些元信息也存儲在堆上,常常在每一個塊的頭部一個很小區域。
2)堆的增長新塊一般從低地址向高地址擴展。所以你能夠認爲堆隨着內存分配而不斷的增長大小。若是申請的內存大小很小的話,一般從底層操做系統中獲得比申請大小要多的內存。
3)申請和釋放許多小的塊可能會產生以下狀態:在已用塊之間存在不少小的空閒塊。進而申請大塊內存失敗,雖然空閒塊的總和足夠,可是空閒的小塊是零散的,不能知足申請的大小,。這叫作「堆碎片」。
4)當旁邊有空閒塊的已用塊被釋放時,新的空閒塊可能會與相鄰的空閒塊合併爲一個大的空閒塊,這樣能夠有效的減小「堆碎片」的產生。
在編程中,例如C/C++中,全部的方法調用都是經過棧來進行的,全部的局部變量、形式參數都是從棧中分配內存空間的。實際上也不是什麼分配,只是從棧頂向上用就行,就好像工廠中的傳送帶(conveyor belt)同樣,Stack Pointer會自動指引你到放東西的位置,你所要作的只是把東西放下來就行,退出函數的時候,修改棧指針就能夠把棧中的內容銷燬,這樣的模式速度最快,,所以固然要用來運行程序了。須要注意的是,在分配的時候,好比爲一個即將要調用的程序模塊分配數據區時,應事先知道這個數據區的大小,也就說是雖然分配是在程序運行時進行的,可是分配的大小多少是肯定的,不變的,而這個"大小多少"是在編譯時肯定的,不是在運行時。
堆是應用程序在運行的時候請求操做系統分配給本身內存,因爲從操做系統管理的內存分配,因此在分配和銷燬時都要佔用時間,所以用堆的效率很是低,可是堆的優勢在於,編譯器沒必要知道要從堆裏分配多少存儲空間,也沒必要知道存儲的數據要在堆裏停留多長的時間,所以,用堆保存數據時會獲得更大的靈活性。事實上,面向對象的多態性,堆內存分配是必不可少的,由於多態變量所需的存儲空間只有在運行時建立了對象以後才能肯定,在C++中,要求建立一個對象時,只需用 new命令編制相關的代碼便可。執行這些代碼時,會在堆裏自動進行數據的保存。固然,爲達到這種靈活性,必然會付出必定的代價:在堆裏分配存儲空間時會花掉更長的時間!這也正是致使效率低的緣由。
區別:
1 申請方式和回收方式不一樣
堆和棧的第一個區別就是申請方式不一樣:棧(英文名稱是stack)是系統自動分配空間的,例如咱們定義一個 char a;系統會自動在棧上爲其開闢空間。而堆(英文名稱是heap)則是程序員根據須要本身申請的空間,例如malloc(10);開闢十個字節的空間。
因爲棧上的空間是自動分配自動回收的,因此棧上的數據的生存週期只是在函數的運行過程當中,運行後就釋放掉,不能夠再訪問。而堆上的數據只要程序員不釋放空間,就一直能夠訪問到,不過缺點是一旦忘記釋放會形成內存泄露。
2 申請後系統的響應
棧:只要棧的剩餘空間大於所申請空間,系統將爲程序提供內存,不然將報異常提示棧溢出。
堆:首先應該知道操做系統有一個記錄空閒內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,而後將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的 delete語句才能正確的釋放本內存空間。另外,因爲找到的堆結點的大小不必定正好等於申請的大小,系統會自動的將多餘的那部分從新放入空閒鏈表中。也就是說堆會在申請後還要作一些後續的工做,同時這也會引出申請效率的問題。
3 申請效率的比較
根據第0點和第1點可知。
棧:由系統自動分配,速度較快。但程序員是沒法控制的。
堆:是由new分配的內存,通常速度比較慢,並且容易產生內存碎片,不過用起來最方便。
4 申請大小的限制
棧:在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就肯定的常數),若是申請的空間超過棧的剩餘空間時,將提示overflow。所以,能從棧得到的空間較小。
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是因爲系統是用鏈表來存儲的空閒內存地址的,天然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。因而可知,堆得到的空間比較靈活,也比較大。
5 堆和棧中的存儲內容
因爲棧的大小有限,因此用子函數仍是有物理意義的,而不只僅是邏輯意義。
棧: 在函數調用時,第一個進棧的是主函數中函數調用後的下一條指令(函數調用語句的下一條可執行語句)的地址,而後是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,而後是函數中的局部變量。注意靜態變量是不入棧的。當本次函數調用結束後,局部變量先出棧,而後是參數,最後棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:通常是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。
6 存取效率的比較
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在運行時刻賦值的;放在棧中。
而bbbbbbbbbbb是在編譯時就肯定的;放在堆中。
可是,在之後的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。
好比:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
對應的彙編代碼
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
關於堆和棧區別的比喻
堆和棧的區別能夠引用一位前輩的比喻來看出:
使用棧就象咱們去飯館裏吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,沒必要理會切菜、洗菜等準備工做和洗碗、刷鍋等掃尾工做,他的好處是快捷,可是自由度小。
使用堆就象是本身動手作喜歡吃的菜餚,比較麻煩,可是比較符合本身的口味,並且自由度大。
JVM中的堆和棧
JVM是基於堆棧的虛擬機,JVM爲每一個新建立的線程都分配一個堆棧,也就是說,對於一個Java程序來講,它的運行就是經過對堆棧的操做來完成的。堆棧以幀爲單位保存線程的狀態。JVM對堆棧只進行兩種操做:以幀爲單位的壓棧和出棧操做。 咱們知道,某個線程正在執行的方法稱爲此線程的當前方法,咱們可能不知道,當前方法使用的幀稱爲當前幀。當線程激活一個Java方法,JVM就會在線程的 Java堆棧裏新壓入一個幀。這個幀天然成爲了當前幀,在此方法執行期間,這個幀將用來保存參數、局部變量、中間計算過程和其餘數據,這個幀在這裏和編譯原理中的活動紀錄的概念是差很少的。
從Java的這種分配機制來看,堆棧又能夠這樣理解:堆棧(Stack)是操做系統在創建某個進程時或者線程(在支持多線程的操做系統中是線程)爲這個線程創建的存儲區域,該區域具備先進後出的特性。 每個Java應用都惟一對應一個JVM實例,每個實例惟一對應一個堆。應用程序在運行中所建立的全部類實例或數組都放在這個堆中,並由應用全部的線程共享。跟C/C++不一樣,Java中分配堆內存是自動初始化的。Java中全部對象的存儲空間都是在堆中分配的,可是這個對象的引用倒是在堆棧中分配,也就是說在創建一個對象時從兩個地方都分配內存,在堆中分配的內存實際創建這個對象,而在堆棧中分配的內存只是一個指向這個堆對象的指針(引用)而已。
在函數中定義的一些基本類型的變量和對象的引用變量都是在函數的棧內存中分配。當在一段代碼塊中定義一個變量時,java就在棧中爲這個變量分配內存空間,當超過變量的做用域後,java會自動釋放掉爲該變量分配的內存空間,該內存空間能夠馬上被另做他用。堆內存用於存放由new建立的對象和數組。在堆中分配的內存,由java虛擬機自動垃圾回收器來管理。在堆中產生了一個數組或者對象後,還能夠在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,在棧中的這個特殊的變量就變成了數組或者對象的引用變量,之後就能夠在程序中使用棧內存中的引用變量來訪問堆中的數組或者對象,引用變量至關於爲數組或者對象起的一個別名,或者代號。
引用變量是普通變量,定義時在棧中分配內存,引用變量在程序運行到做用域外釋放。而數組&對象自己在堆中分配,即便程序運行到使用new產生數組和對象的語句所在地代碼塊以外,數組和對象自己佔用的堆內存也不會被釋放,數組和對象在沒有引用變量指向它的時候,才變成垃圾,不能再被使用,可是仍然佔着內存,在隨後的一個不肯定的時間被垃圾回收器釋放掉。這個也是java比較佔內存的主要緣由,實際上,棧中的變量指向堆內存中的變量,這就是 Java 中的指針!
Java 中的堆和棧
Java把內存劃分紅兩種:一種是棧內存,一種是堆內存。
在函數中定義的一些基本類型的變量和對象的引用變量都在函數的棧內存中分配。當在一段代碼塊定義一個變量時,Java就在棧中爲這個變量分配內存空間,當超過變量的做用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間能夠當即被另做他用。
堆內存用來存放由new建立的對象和數組。 在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。
在堆中產生了一個數組或對象後,還能夠在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。 引用變量就至關因而爲數組或對象起的一個名稱,之後就能夠在程序中使用棧中的引用變量來訪問堆中的數組或對象。
具體的說:
棧與堆都是Java用來在Ram中存放數據的地方。與C++不一樣,Java自動管理棧和堆,程序員不能直接地設置棧或堆。
Java的堆是一個運行時數據區,類的對象從中分配空間。這些對象經過new、newarray、anewarray和multianewarray等指令創建,它們不須要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優點是能夠動態地分配內存大小,生存期也沒必要事先告訴編譯器,由於它是在運行時動態分配內存的,Java的垃圾收集器會自動收走這些再也不使用的數據。但缺點是,因爲要在運行時動態分配內存,存取速度較慢。
棧的優點是,存取速度比堆要快,僅次於寄存器,棧數據能夠共享。但缺點是,存在棧中的數據大小與生存期必須是肯定的,缺少靈活性。棧中主要存放一些基本類型的變量(,int, short, long, byte, float, double, boolean, char)和對象句柄。
棧有一個很重要的特殊性,就是存在棧中的數據能夠共享。假設咱們同時定義:
int a = 3;
int b = 3;
編譯器先處理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, 它是由編譯器完成的,它有利於節省空間。而一個對象引用變量修改了這個對象的內部狀態,會影響到另外一個對象引用變量。
Java爲何慢?
JVM的存在固然是一個緣由,但有人說,在Java中,除了簡單類型(int,char等)的數據結構,其它都是在堆中分配內存(因此說Java的一切都是對象),這也是程序慢的緣由之一。 個人想法是(應該說表明TIJ的觀點),若是沒有Garbage Collector(GC),上面的說法就是成立的。
堆不象棧是連續的空間,沒有辦法期望堆自己的內存分配可以象堆棧同樣擁有傳送帶般的速度。由於,沒有誰會爲你整理龐大的堆空間,讓你幾乎沒有延遲的從堆中獲取新的空間,這個時候,GC站出來解決問題,咱們都知道GC用來清除內存垃圾,爲堆騰出空間供程序使用,但GC同時也擔負了另一個重要的任務,就是要讓Java中堆的內存分配和其餘語言中堆棧的內存分配同樣快,由於速度的問題幾乎是衆口一詞的對Java的詬病。要達到這樣的目的,就必須使堆的分配也可以作到象傳送帶同樣,不用本身操心去找空閒空間,這樣,GC除了負責清除Garbage外,還要負責整理堆中的對象,把它們轉移到一個遠離Garbage的純淨空間中無間隔的排列起來,就象堆棧中同樣緊湊,這樣Heap Pointer就能夠方便的指向傳送帶的起始位置,或者說一個未使用的空間,爲下一個須要分配內存的對象"指引方向"。所以能夠這樣說,垃圾收集影響了對象的建立速度,聽起來很怪,對不對? 還有GC怎樣在堆中找到全部存活的對象呢?前面說了,在創建一個對象時,在堆中分配實際創建這個對象的內存,而在堆棧中分配一個指向這個堆對象的指針(引用),那麼只要在堆棧(也有可能在靜態存儲區)找到這個引用,就能夠跟蹤到全部存活的對象。找到以後。GC將它們從一個堆的塊中移到另一個堆的塊中,並將它們一個挨一個的排列起來,就象咱們上面說的那樣,模擬出了一個棧的結構,但又不是先進後出的分配,而是能夠任意分配的,在速度能夠保證的狀況下,但GC()的運行要佔用一個線程,這自己就是一個下降程序運行性能的缺陷,更況且這個線程還要在堆中把內存翻來覆去的折騰,不只如此,如上面所說,堆中存活的對象被搬移了位置,那麼全部對這些對象的引用都要從新賦值,這些開銷都會致使性能的下降。此消彼長,GC()的優勢帶來的效益是否蓋過了它的缺點致使的損失,我也沒有太多的體會。
注:
名稱解釋:
IP(Instruction Pointer):指令指針寄存器存儲的是下一個時鐘週期將要執行的指令所在的程序寄存器地址。
stack pointer堆棧指針:老是指向棧頂元素。堆棧的實現是往上長的(就是說往頂的方向長,其實質是棧底是定死的不能動,入棧的東西只能不斷往上疊,這就像在書桌上放書同樣,桌底是定死的,因此書只能一本一本地往上堆,往上長),計算機內部的堆棧的實現採起的就是這種模式,因此就得「先修改指針,而後插入數 據,出棧時恰好相反」,由於堆棧指針指向的老是棧頂元素,棧底不能動,因此數據入棧前要先修改指針使它指向新的空餘空間而後再把數據存進去,出棧的時候天然相反。