JVM01---Java中的內存區域以及重點介紹堆與棧
文章目錄
一些基本概念
數據類型
基本數據類型
引用數據類型
堆和棧
棧中存什么?堆中存什么?
Java中的參數傳遞時傳值?還是傳引用?
為什么要把堆和棧區分出來呢?
運行時數據區域
1. 程序計數器
2. Java虛擬機棧
3. 本地方法棧
4.Java堆
新生代
老年代
持久代
5.方法區
6. 運行時常量池
7.直接內存
面試的幾個問題
總結
參考
一些基本概念
數據類型
基本數據類型
從事Java開發的小伙伴都知道Java有八種基本數據類型,分別是byte, boolean,char,short,int,float,long,double。其中各個數據類型所占的字節數如下圖所示:
基本數據類型的變量保存的是原始值,即:它代表的值是數值本身,一般而言一個英文字母占用1個字節,大部分漢字占用2個字節。
引用數據類型
與基本數據類型不同的就是引用數據類型,Java中所有的對象都是引用類型,包括基本類型的包裝類以及String類。引用類型的變量保存的是引用值,引用值代表了某個對象的引用,而不是對象本身,對象本身是放在引用值所代表的位置,對象是保存在堆上的,這個后面會詳細說。
堆和棧
堆和棧是程序運行的關鍵,我們需要記住的是:棧是運行時的單位,解決的是程序運行的問題,即程序如何執行,或者如何處理數據,堆是存儲單位,解決的是數據存儲的問題,即數據怎么放,放哪兒。
在Java中一個線程就會有一個相應的線程棧與之對應,因為不同的線程執行邏輯不同,所以需要獨立的線程棧。棧因為是運行單位,因此里面存儲的信息都是當前線程(或程序)相關的信息。包括局部變量、程序運行狀態、方法返回值等;而堆只負責存放對象信息。
我將通過如下這段代碼,展示程序運行時棧的存儲情況。
public static void main(String[] args) { int a = 1; int ret = 0; int res = 0; ret = add(3, 5); res = a + ret; printf("%d", res); } int add(int x, int y) { int sum = 0; sum = x + y; return sum; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如上,這段代碼 main()方法調用了add()方法,獲取計算結果。并且與臨時變量a相加,最后打印res的值。運行main()方法之后,我們可以畫出方法的調用棧,如下圖所示:
對于有sum = x + y這種有符號的運算,編譯器就是通過兩個棧來實現的。其中一個保存操作數的棧,另一個是保存運算符的棧。
棧中存什么?堆中存什么?
堆中存的是對象,棧中存的是基本數據類型和堆中對象的引用,一個對象的大小不可估計或者說可以動態變化的,但是在棧中,一個對象只對應一個4byte的引用。
為啥不把基本類型放在堆中呢?因為其占用的空間一般是1~8個字節—需要空間比較少,而且因為是基本類型,所以不會出現動態增長的情況,長度固定,因此棧中存儲就夠了,如果把他存在堆中沒有什么意義。可以說,基本類型和對象的引用都是存放在棧中,而且都是幾個字節的一個數,因此在程序運行時,他們的處理方式是統一的。但是基本類型、對象引用和對象本身就有所區別了,因為一個是棧中的數據一個是堆中的數據,下面是一個常見的問題是。
Java在方法調用傳入參數時,因為沒有指針,所以它都是進行傳值調用,基本類型和引用類型的處理是一樣的,都是傳值。所以,如果是傳引用的方法調用,可以理解為傳引用值的傳值調用,即引用的處理和基本類型是完全一樣的。但是當進入被調用方法時,被傳遞的這個引用的值,被程序查找到堆中的對象,這個時候對應到真正的對象,如果此時進行修改,修改的就是引用對應的對象,而不是引用本身,即:修改的是堆中的數據,所以這個修改是可以保持的。
對象,,從某種意義上說,是由基本類型組成的。可以把一個對象看作為一棵樹,對象的屬性如果還是對象,則還是一顆樹(即非葉子節點),基本類型則為樹的葉子節點。 程序參數傳遞時,被傳遞的值本身都是不能修改的,但是如果這個值是一個非葉子節點(即一個對象引用),則是可以修改這個節點下面的所有內容的。
為什么要把堆和棧區分出來呢?
從軟件設計的角度看,棧代表了處理邏輯,而堆代表了數據。這樣分開,使得處理邏輯更為清晰,分而治之的思想。
堆和棧的分離,使得堆中的內容可以被多個棧共享,一方面這種共享提供了一種有效的數據交互方式,另一方面,堆中的共享常量和緩存可以被所有棧訪問,節省了空間。
棧因為運行時的需要,比如保存系統運行的上下文,需要進行地址段的劃分,由于棧只能向上增長,因此就會限制住棧存儲的內容能力。而堆不同,堆中的對象是可以根據需要動態增長的,因此棧和堆的拆分,使得動態增長成為可能。相應的棧中只需要記錄堆中的一個地址即可。
面向對象就是堆和棧的完美結合。其實,面向對象方式的程序與以前結構化的程序在執行上沒有任何區別。當我們把對象拆開,你會發現,對象的屬性其實就是數據,存放在堆中;而對象的行為(方法),就是運行邏輯,放在棧中。我們在編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。
運行時數據區域
Java 虛擬機在執行 Java 程序的過程中會把它管理的內存劃分成若干個不同的數據區域。
這些組成部分一些是線程私有的,其他的則是線程共享的。
線程私有的有: 1. 程序計數器;2.虛擬機棧;3.本地方法棧
線程共享的有: 1. 堆;2:方法區;3.直接內存
1. 程序計數器
程序計數器是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
程序計數器是唯一不會出現OutOfMemoryError的內存區域,它的生命周期隨著線程的創建而創建,隨著線程的結束而死亡。
2. Java虛擬機棧
Java虛擬機棧也是線程私有的,他的生命周期與線程相同。虛擬機棧描述的是Java方法執行的線程內存模型,每個方法被執行的時候,Java虛擬機都會同步創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態連接、方法出口等信息。每一個方法被調用直至執行完畢的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
虛擬機棧中的局部變量表存放了編譯器可知的各種Java虛擬機基本數據類型,對象引用(reference類型,它并不等同于對象本身,可能是一個指向對象起始地址的引用類型指針,也可能指向一個達標對象的句柄或者其他與此對象相關的位置)以及returnAddress類型(指向了一條字節碼指令的地址)。Java虛擬機規范中,對Java虛擬機棧這個內存區域規定了兩類異常狀況,如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果Java虛擬機棧容量可以動態擴展,當棧擴展時無法申請到足夠的內存會拋出OutOfMemoryError異常
3. 本地方法棧
本地方法棧與虛擬機棧所發揮的作用是非常相似的,其區別只是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的本地(Native)方法服務。
本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用于存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。
方法執行完畢后相應的棧幀也會出棧并釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種異常。
4.Java堆
Java堆是虛擬機所管理的內存中最大的一塊。Java堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放實例。Java堆是垃圾收集器管理的內存管理區域,因此也被稱為GC堆,
從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,所以Java堆還可以細分為:新生代和老年代:在細致一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。Eden的空間和From Survivor以及To Survivor空間的內存大小的默認占比是8:1:1。在JVM1.8之后移除了永久代。
所有新生成的對象首先都是放在新生代,新生代的目標就是盡可能快速的收集掉那些生命周期短的對象,新生代分為三個區,一個是Eden區,兩個Survivor區(一般而言)。大部分對象都是在Eden區中生成。
當Eden區滿時,還存活的對象將被復制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被復制到另外一個Survivor區,當這個Survivor區也滿了的時候,從第一個Survivor區復制過來的并且此時還存活的對象,將被復制到"老年代(Tenured)"。對象每經歷一次Minor GC,年齡加1,達到“晉升年齡閾值”后,對象將被放到老年代,這個過程也稱為“晉升”。顯然,“晉升年齡閾值”的大小直接影響著對象在新生代中的停留時間,在Serial和ParNew GC兩種回收器中,“晉升年齡閾值”通過參數MaxTenuringThreshold設定,默認值為15。
需要注意,Survivor的兩個區是對稱的,沒先后關系,所以同一個區中可能同時存在從Eden復制過來的對象,和從前一個Survivor復制過來的對象,而復制到年老區的只有從第一個Survivor區過來的對象。而且,Survivor區總是一個是空的。同時,根據程序需要,Survivor區是可以配置多個的,這樣可以增加對象再新生代中的存在時間,減少被放到老年代的可能。
在新生代經歷了15次minor GC后仍然存活的對象,在第16次minor GC會被復制到老年代,因此,可以認為老年代中存放的都是一些生命周期較長的對象。該區域中對象存活率高,老年代的垃圾回收(又稱為Major GC)通常使用“標記-整理” 算法,整堆包括新生代和老年代的垃圾回收稱為Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都會同時收集整個GC堆,包括新生代)。
用于存放靜態文件,如Java類,方法等,持久代對垃圾回收沒有顯著影響,但是有些應用可能會動態生成或調用一些class,例如Hibernate等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過 -XX:MaxPermSize 來設置。
在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。
5.方法區
方法區(Method Area)和Java堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類型信息,常量,靜態變量,即時編譯器編譯后的代碼緩存等數據。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分, 但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
6. 運行時常量池
運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用于存放編譯期生成的各種字面量和符號引用)
既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 異常。
JDK1.7及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池。
7.直接內存
直接內存并不是虛擬機運行時數據區的一部分,也不是虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致OutOfMemoryError異常出現。
JDK1.4中新加入的 NIO(New Input/Output) 類,引入了一種基于通道(Channel) 與緩存區(Buffer) 的 I/O 方式,它可以直接使用Native函數庫直接分配堆外內存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆之間來回復制數據。
面試的幾個問題
1. 年輕代中為什么需要兩個Survivor區?
答:因為年輕代使用的是復制算法,第一次Minor GC之后,存活的對象會被復制到其中一個Survivor區,接下來Minor GC之后,會將Eden區和Survivor區存活的對象復制到另一個Survivor區,循環往復,只有兩個Survivor區,復制算法才能一直不中斷的執行。
2. 為什么Eden和Survivor的比例是8:1?
年輕代中,大部分對象生命周期都很短,很快就會被清除,根本沒有必要設置大容量的Survivor區來存放存活的對象。
3. 新生代的對象經過多少次GC之后還存活的對象會被復制到老年代?
新生代對象每經歷一次Minor GC之后,如果對象還存活,則會復制到survivor區,年齡會加1,當達到“晉升年齡閾值”后,還存活的對象就會被復制到老年代中。這個過程也稱為“晉升”。顯然,“晉升年齡閾值”的大小直接影響著對象在新生代中的停留時間,在Serial和ParNew GC兩種收集器中,“晉升年齡閾值”通過參數MaxTenuringThreshold設定,默認值為15。
4. 哪些大對象會直接放進老年代?
這里的大對象主要指字符串和數組,虛擬機提供了一個-XX:PretenureSizeThreshold參數,大于這個值的參數直接在老年代分配。這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存復制(新生代采用標記-復制算法)
有兩種情況,對象會直接分配到老年代。
如果在新生代分配失敗且對象是一個不含任何對象引用的大數組,可被直接分配到老年代,通過在老年代的分配避免新生代的一次垃圾回收。
-XX:PretenureSizeThreshold=<字節大小> 可以設置分配到新生代對象的大小限制,任何比這個大的對象都不會嘗試在新生代分配,將在老年代分配內存。PretenureSizeThreshold默認值是0,也就是說任何對象都會現在新生代分配內存。具體可以參考-XX:PretenureSizeThreshold的默認值和作用淺析
總結
本文首先介紹Java中的數據類型,無非就是八種基本數據類型和引用類型,接著重點介紹了堆和棧的知識,棧是程序運行最根本的東西。程序運行可以沒有堆,但是不能沒有棧。而堆是為棧進行數據存儲服務,說白了堆就是一塊共享的內存。不過,正是因為堆和棧的分離的思想,才使得Java的垃圾回收成為可能。接著就是詳細介紹了JVM執行Java程序時將內存劃分的區域。主要還是兩類,一類是線程私有的,生命周期與線程的生命周期相同, 一類是線程共享的。主要是Java堆,這一塊是垃圾收集器管理的主要區域。
參考
《深入理解Java虛擬機》 第2章第2節
-XX:PretenureSizeThreshold的默認值和作用淺析
JVM Q&A
Java JVM
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。