java 栈内存结构_JVM内存结构概念解析
一. Java 內存結構
Java代碼運行在虛擬機上,虛擬機在運行過程將程序(也就是進程)所占有內存分為幾個不同的數據區域。不同的區域有不同的職責。
Java運行時內存結構圖如下:
Java運行時內存結構圖
1. PC寄存器(程序計數器):
當前線程執行的字節碼的行號指示器。字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。
線程私有(每條線程有獨立的計數器)
沒有OOM的區域
如執行的是Native方法,則計數器的值為空
2. 虛擬機棧:
也就是我們常說的線程方法棧。大致符合所了解的先入后出的棧結構特性,其出入棧的數據結構稱為棧幀,也就是調用某個方法A時,則會入棧一個棧幀,這個棧幀結構包括了方法A中的局部變量區、操作數據棧,動態鏈接、方法的返回地址等。
編譯期間確定棧幀的大小。虛擬機棧大小也比較小,大概1M左右,故而可能發生StackOverfollowError, 比如無限遞歸調用。
局部變量區:局部變量,在方法內聲明的變量。其編譯期間就確定這個區間的大小。
操作數棧:也是先入后出,JVM的指令集在操作時都是對操作棧上的數據進行操作,比如說算術運算、方法調用時的參數傳遞。
動態鏈接:棧幀中包含一個運行時常量池中該棧幀所屬方法的引用
方法A調用方法B的調用過程大致是:方法A存有方法B的在常量池的符號引用b,然后根據指令將符號引用b作為參數,并將b解析為方法B真正所在的內存地址,
直接引用,然后進行方法B真正調用。生成的棧幀就持有了自身方法的引用。
這些符號引用一部分在類加載階段或第一次使用的時候就被轉換直接引用的,就稱為靜態解析,比如靜態方法以及私有方法。在運行期被轉換為直接引用的過程稱之為動態鏈接。
方法返回地址:方法退出時(正常return或異常退出),回到方法被調用的位置,并會返回一個值給上層方法(若有的話),并恢復上層方法的執行狀態。
3. 本地方法棧:
當調用原生代碼時的方法棧。作用類似于虛擬機棧。
而數據結構、使用方式。都由虛擬機自由發揮。
4. 堆:
堆棧...我們比較熟悉也可能長掛在嘴邊的一個詞。棧我們都知道了,就是上面的虛擬機棧,也確實是有先進后出的一種特性。但這里的堆就跟數據結構中的堆完全不同概念了,也沒有什么共性。
堆:就是一個主要用于存放對象的內存區域,線程共享的一塊區域,堆允許程序在運行時動態地申請某個大小的內存空間。
堆中有一個不得不提的事 --- 垃圾回收(GC)
堆劃分為兩個區域: 新生代和老年代,默認比例1:2。新生代又劃分為Eden和 Survivor,而Survivor又劃分為from和to兩個區域。其中默認比例為 Eden:from:to = 8:1:1, 運行時,from和to有一塊區域是會處于閑置狀態,GC的時候,兩個狀態發生切換。
當我們新建一個對象的時候,絕大多數新建的對象被放在 Eden 區域(例外的有,需要更多的一大片連續的儲存空間只有老年代才能滿足的時候)。
而這個對象不再使用的時候,系統則需要對其回收,以便更好分配內存。
GC分為兩種,Minor GC 和 Full GC
GC時間不可預測,滿足觸發條件,系統才GC,若GC后的空間仍不滿足,則會發生OOM。GC過程會 stop the world, 根據對象的引用鏈,標記不可達對象??烧{用System.gc() 提醒系統觸發GC(Full GC)。
判斷對象是否存活
引用計數:判斷對象被引用的次數,無法解決相互引用的問題。
可達性分析:從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象不存在于任何一條GC Root的引用鏈的時候,則證明此對象是不可達的,則會進行一次標記。
GC Roots 包括:
虛擬機棧中引用的對象。
方法區中類靜態屬性實體引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI引用的對象。
不可達不一定會立馬被回收,如果你重寫了Object.finalize()的話:
要知道這個方法是在對象不可達之后,GC時將該方法交給優先級更低Finalizer線程去執行(只執行一次)。
同時該對象放入可回收隊列(虛引用出現的地方?)
并在下次GC時再次判斷是否可達以及finalize()是否被執行,然后再回收。
現在是不建議覆蓋此方法的。畢竟影響GC。
GC 算法
復制算法
將區域分為兩塊,其中一塊閑置。GC時將可達的對象復制至閑置的區域,清除當前區域。循環往復。
標記-清除算法
先根據引用鏈標記所有對象的存活狀態,然后并從內存中清除所有不可達對象進行清除
標記-壓縮算法
標記-清除算法的改進版,將所有存活的對象壓縮整理到同一側區域,較少內存空間碎片。然后清理邊界外的區域。
Minor GC
針對新生代的GC, Eden滿了之后會觸發。
Minor GC時,會將eden和from區域的存活對象都復制至Surivor的to區域(假設當前to閑置),當對象在在surivor存在足夠久,比如熬過了默認的15次GC,那么就會被存入老年代。
當然若to區域空間放不下所有存活的對象,那么多余的都會進入老年代。
這過程顯然的用的是 復制算法。
Full GC
對所有空間進行GC,包括堆和方法區(類卸載)
觸發條件:
System.gc()
老年代的空間不足(包括新申請、從eden或surivor復制過來的)
方法區空間不足
老年代區域使用的是標記-壓縮算法。
5.方法區:
方法區,概念上的區域,指明該區具有什么功能。不同的虛擬機有不同的實現,儲存的內存區域不定。
永久代與元空間:均是方法區的實現。永久代將大多數據放在堆內存上,容易誘發OOM。
JDK 1.8之后廢棄永久代使用元空間,將方法區數據放在本地內存,理論上可僅受系統內存限制。
存放有運行時常量池,以及class加載后的產物(類字節碼、class/method/field等元數據對象、static-final常量、static變量),以及JIT過程的生成的代碼。線程共享
先說下,JIT, 即時編譯.JVM通過解釋器java字節碼(.class文件)執行時,由于效率問題,引入JIT,即JVM發現某個代碼塊運行頻繁,則會將其編譯為相關的機器碼,存在方法區,供下次使用。
運行時常量池
字節碼文件--class文件中包含了很多信息,比如魔數、屬性表、方法表等,其中還有一個常量池,常量池則包含了這個類所用到的各種字面量和引用量
class A {
final int a = 0;
String s = "hhh"
String s1 = "aa" + s
void test() {
String s2 = "ssss"
}
}
字面量
這個好理解,其實就是 各種 文本字符串,以及基本數據類型,final 常量等,注例如
那么 0、"hhh"、"aa"、"ssss" 都屬于字面量
引用量:
也就是符號引用,類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符
還是上面的Class A
那么 類全限定名
packagename.A、a、test、packagename.A.test:()V這些就是符號引用
類加載的時候,常量池就會被加載入運行時常量池中!
同時運行時常量池還具有動態性,比如
void test(String a) {
String s = new String("ddd")
s.intern();
}
那么判斷常量池是否已經存在"ddd",若不存在然后將"ddd"加入常量池,存在則s指向常量池中"ddd"的地址。
最后
總結下,其實Java內存結構 也就是JVM內存結構,由虛擬機規范定義。描述的是在程序執行過程中,由JVM管理的不同數據區域,各個區域有特定的功能。
2. Java 內存模型
稍微聊一下JMM,即 Java Memory Model, 這是一個抽象的概念,其模型圖如下。
JMM內存模型
之前對JMM的概念真的分不清,以下整段拷貝...
Java內存模型定義了線程和內存的交互方式,在JMM抽象模型中,分為主內存、工作內存。主內存是所有線程共享的,Java內存模型規定了所有的變量都存儲在主內存中;每條線程還有自己的工作內存,線程的工作內存中保存了被該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。
我們知道,多線程之間的通信是通過共享內存進行通信的
這里的主內存可以說就是上面JVM中的共享內存。而工作內存則是一個抽象概念,其實現比如有什么CPU各級緩存,寄存器什么的
那么根據這多線程并發通信過程存在的可見性、原子性、指令重排問題,JMM則定義了一些語法集,也就是我們常見的synchronize、volatile等關鍵字。
emm...大致就先這樣吧,反正不要混淆與JVM內存結構這個概念吧。有時間再寫一篇并發通信的文章。
另外還可以了解下happen-before的規則
深入理解happens-before規則
3. Java對象模型
這個就指一個對象在內存中的儲存結構了。
Java 對象模型
jvm在加載class時,會創建instanceKlass,表示其元數據,包括常量池、字段、方法等,存放在方法區;instanceKlass是jvm中的數據結構;
在new一個對象時,jvm創建instanceOopDesc,來表示這個對象,存放在堆區,其引用,存放在棧區;它用來表示對象的實例信息,看起來像個指針實際上是藏在指針里的對象;instanceOopDesc對應java中的對象實例;
instanceKlass對java上層來說并不可見。我們能看到就是根據instanceKlass而創建的Class對象。也就是 Object.getClass() 返回的這個玩意。
4. 問題:
1.堆內存真的完全線程共享嗎?
線程在創建對象時,會進行內存的分配,分配如何避免多個線程并發?就不提了,也不懂。但是一般由于需要頻繁的創建對象,故而使用了TLAB (Thread Loacle allocation Buffer)來提高效率,也就是每個線程給予先分配一小塊獨享的堆內存,當線程需要進行內存分配時,則直接在該塊內存進行分配。分配時,獨顯,分配后的對象是可以被其他線程讀取的。
TLAB存在于eden區域,也就是新生代,并不影響內存回收。當申請的內存大于TLAB的剩余空間,其他策略處理。
2. 對象一定儲存在堆中分配內存嗎?
在JIT過程中,會對代碼進行優化,部分目的是為減少內存分配壓力,其中用到了逃逸分析,即若分析得到某個對象的使用范圍不超過該方法或者不超過本線程,那么就可能會被優化在棧上分配內存!
當然這里會考慮,棧本身大小就幾百K--1M的問題。還有就是逃逸分析,就是在對象是否為超出本線程或這個方法,那么該分析也可用于鎖消除優化。
鎖消除優化
比如下面這段代碼
fun test() {
val buffer = StringBuffer()
buffer.append("test")
}
大家都知道StringBuffer的方法都用了synchronize修飾,但是在經過逃逸分析后,顯然buffer這個局部變量時本身就是線程安全,不可能被其他線程引用,那么JVM會自動StringBuffer對象的內部鎖??梢哉f是當做StringBuilder來看待。
參考文章
總結
以上是生活随笔為你收集整理的java 栈内存结构_JVM内存结构概念解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 方法中定义类_在Java方法中
- 下一篇: yar java_Yar 的传输协议学习