JVM詳解

爲了讓自己以後更好查閱,所以根據自己理解記錄jvm。
可參考官網的描述:java虛擬機
也可以參考該鏈接:Java虛擬機

JVM內存結構

在這裏插入圖片描述
**多線程共享內存區域😗*方法區、堆。
**每一個線程獨享內存😗*java棧、本地方法棧、程序計數器。

堆:
Java虛擬機具有一個在所有Java虛擬機線程之間共享的堆。堆是運行時數據區,從中分配所有類實例和數組的內存。
堆是在虛擬機啓動時創建的。對象的堆存儲由自動存儲管理系統(稱爲垃圾收集器)回收;對象永遠不會顯式釋放。Java虛擬機實現可以爲程序員或用戶提供對Java虛擬機堆棧初始大小的控制,並且在動態擴展或收縮Java虛擬機堆棧的情況下,可以控制最大和最小大小。堆的內存不必是連續的。
以下異常條件與Java虛擬機堆棧相關:
如果線程中的計算需要比允許的Java虛擬機更大的堆棧,則Java虛擬機將拋出StackOverflowError。
如果可以動態擴展Java虛擬機堆棧,並嘗試進行擴展,但是可以提供足夠的內存來實現擴展,或者如果沒有足夠的內存來爲新線程創建初始Java虛擬機堆棧,則Java虛擬機機器拋出一個OutOfMemoryError。
堆:Java堆是程序員需要重點關注的一塊區域,因爲涉及到內存的分配(new關鍵字,反射等)與回收(回收算法,收集器等);
方法區:也叫永久區,用於存儲已經被虛擬機加載的類信息,常量(“zdy”,"123"等),靜態變量(static變量)等數據。(jdk1.8已經將方法區去掉了,將方法區移動到直接內存)

java 棧:線程私有,生命週期和線程,每個方法在執行的同時都會創建一個 棧幀用於存儲局部變量表,操作數棧,動態鏈接,方法出口等
信息。方法的執行就對應着棧幀在虛擬機棧中入棧和出棧的過程;棧裏面存放着各種基本數據類型和對象的引用;

運行時常量池:運行時常量池是方法區的一部分,用於存放編譯期生成的各種字面(“zdy”,"123"等)和符號引用。
直接內存:不是虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域;
1)如果使用了NIO,這塊區域會被頻繁使用,在java堆內可以用directByteBuffer對象直接引用並操作;
2) 這塊內存不受java堆大小限制,但受本機總內存的限制,可以通過MaxDirectMemorySize來設置(默認與堆內存最大值一樣),所以也會出現OOM異常;

堆和棧的區別
1)堆和棧功能上的區別
以棧幀的方式存儲方法調用的過程,並存儲方法調用過程中基本數據類型的變
量(int、short、long、byte、float、double、boolean、char等)以及對象的引
用變量,其內存分配在棧上,變量出了作用域就會自動釋放;
而堆內存用來存儲Java中的對象(就是new出來的)。無論是成員變量,局部變量,還是類變量,
它們指向的對象都存儲在堆內存中;
2)堆和棧在線程共享和線程私有區別
棧內存歸屬於單個線程,每個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存可以理解成線程的私有內存。
堆內存中的對象對所有線程可見。堆內存中的對象可以被所有線程訪問。
3)空間大小
棧的內存要遠遠小於堆內存,棧的深度是有限制的,如果遞歸沒有及時跳出,很可能發生StackOverFlowError問題。
你可以通過-Xss選項設置棧內存的大小(這個參數是設定單個線程的棧空間)。-Xms選項可以設置堆的開始時的大小,-Xmx選項可以設置堆的最大值

根據以下代碼來具體說明jvm虛擬機如何運作的:

public class Math{
	public static final int initData = 666;
	public static User user = new User();
	
	public int compute(){ //一個方法對應一塊棧幀
		int a = 1;
		int b = 2;
		int c = (a+b) * 10;
		return c;
	}
	public static void Math(String[] args){
		Math math = new Math();
		math.compute();
		System.out.println("test");
	}
}

在這裏插入圖片描述
可以根據如上圖片的命令 javap -c xx.class, 通過JVM指令手冊進行分析。
以下是分析圖:
在這裏插入圖片描述
Java虛擬機棧描述的是Java方法執行的內存模型:每個方法被調用的時候都會創建一個棧幀,用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程就對應着一個棧幀在虛擬機中從入棧到出棧的過程。
上述main()-棧幀中局部變量表存儲的是堆中math對象的內存地址,同理方法區中存儲了堆中user的內存地址。

操作數棧:是一塊臨時的內存區域,用來存放程序在運行中臨時操作數。

動態鏈接:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。Class文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用,一部分會在類加載階段或第一次使用的時候轉化爲直接引用(如final、static域等),稱爲靜態解析,另一部分將在每一次的運行期間轉化爲直接引用,這部分稱爲動態連接。

程序計數器:較小的內存空間,當前線程執行的字節碼的行號指示器;各線程之間獨立存儲,互不影響;(簡單意思就是CPU多線程切換,記錄從哪一行繼續執行)

本地方法棧::本地方法棧保存的是native方法的信息,當一個JVM創建的線程調用native方法後,JVM不再爲其在虛擬機棧中創建棧幀,JVM只是簡單地動態鏈接並直接調用native方法;(主要是因爲之前某些方法用的是c語言,java語言與c語言進行跨源)

JVM內存分配與回收

主要詳細講解堆

在這裏插入圖片描述
對象會優先在Eden區分配:大多數情況下,對象在新生代中Eden區分配。當Eden區沒有足夠空間進行分割時,虛擬機將發起一次Minor GC。

Minor GC/Young GC:指發生新生代的垃圾手機動作,Minor GC非常頻繁,回收速度一般也比較快。
Full GC/Major GC:一般回收老年代,年輕代,方法區的垃圾,Full GC的速度一般比Minor GC的慢10倍以上。

在這裏插入圖片描述
碎碎念模式開啓:
GC roots還會引用其他的對象,在這一鏈條中都是存活的對象。不在這鏈條中的對象就是可回收對象。
凡是在這鏈條上能找到的對象就會移動到Survivor區中,垃圾對象就會一次性清理掉。當進行minor gc後,還沒被銷燬的對象就會移動到Survivor區中。對象會有個分代年齡,就是+1,
jvm調優的目的就是減少full gc。

如何判斷對象可以被回收
堆中幾乎放着所有的對象實例,對堆垃圾回收前的第一步就是 要判斷哪些對象已經死亡(即不能再被任何途徑使用的對象)。
引用計數法
給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加1;當引用失效,計數器就減1;任何時候計數器爲的對象就是不可能再被使用的。
這個方法實現簡單,效率高,但是目前主流的虛擬機中並沒有選擇這個算法來管理內存,其最主要的原因是它很難解決對象之間相互循環引用的問題。所謂對象之間的相互引用問題,如下面代碼所示:除了對象objA和objB相互引用着對方之外,這兩個對象之間再無任何引用。但是他們因爲互相引用對方,導致它們的引用計數器都不爲0,於是用計數算法無法通知GC回收器回收他們。

 public class ReferenceCountingGc {
   object instance = null;
   public static void main(String[] args) {
		ReferenceCountingGc objA = new ReferencecountingGc();
		ReferenceCountingGc objB = new ReferenceCountingGc();
		objA. instance = objB;
		objB. instance = objA;
		objA= null;0  
		objB = null;
		}
	}

長期存活的對象將進入老年代
既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。爲了做到這一點,虛擬機給每個對象一個對象年齡(Age) 計數器。
如果對象在Eden出生並經過第一-次 Minor GC後仍然能夠存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並將對象年齡設爲1。對象在Survivor中每熬過- -次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認爲15歲), 就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數-XX: MaxTenuring Threshold來設置。

對象動態年齡判斷
當前放對象的Survivor區域裏(其中一塊區域,放對象的那塊s區),一批對象的總大小大於這塊Survivor區域內存大小的50%(-XX:TargetSurivorRatio可以指定),那麼此時大於等於這批對象年齡最大值的對象,就可以直接進入老年代了,例如Survivor區域裏現在有一批對象, 年齡1+年齡2+年齡n的多個年齡對象總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代。這個規則其實是希望那些可能是長期存活的對象,儘早進入老年代。對象動態年齡判斷機制-般是在minor gc之後觸發的。
Eden與Survivor區默認8:1:1
大量的對象被分配在eden區,eden區滿了後會觸發minor gc,可能會有99%以上的對象成爲垃圾被回收掉,剩餘存活的對象會被挪到爲空的那塊survivor區,下一 次eden區滿了後又會觸發minor gc,把eden區和survivor去垃圾對象回收, 把剩餘存活的對象一次性挪動到另外一塊爲空的survivor區。因爲新生代的對象都是朝生夕死的,存活時間很短,所以JVM默認的8:1:1的比例是很合適的, 讓Eden區儘可能的大,Survivor區夠用即可。JVM默認有這個參數-XX:+UseAdaptiveSizePolicy,會導致這個比例自動變化,如果不想要這個比例變化可以設置-XX:+UseAdaptiveSizePolicy。

JVM調優案例

根據業務場景以及內存進行計算
在這裏插入圖片描述
常規想法是這樣的:
弊端:當Eden區執行了Minor gc後存活的對象會放入Survivor區,但是根據對象動態年齡判斷中,如果存入的對象大小超過Survivor的50%就會直接進入Old(老年代),那樣在三至四分鐘左右就會執行一次Full GC,執行一次Full GC就會執行STW,STW就會暫停所有的線程,從而影響性能。
在這裏插入圖片描述
優化:讓其幾乎不發生Full GC。
將Eden區內存擴大,old區內存減小,將-Xmn(新生代內存大小)參數設置大一點。
在這裏插入圖片描述
這樣做的優點:當Eden區放滿時執行Minor GC, Minor gc後存活的60M對象會挪到S0區,根據對象動態年齡判斷60M不大於S0區的50%,所以不會進入老年代;當Eden區再次放滿時會執行Minor GC, 同時會將Eden,S0區垃圾一起直接銷燬掉,這樣就基本上不會 發生Full GC。

調優參數設置:
java -Xms3072M -Xmx3072M -Xmn2048 -Xss1M -XX:MetaspaceSize=256M --XX:MaxMetaspaceSize= 256M -jar microservice-eureka-server.jar

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章