JVM虛擬機詳解——java

一、虛擬機簡介

邏輯上,可看作一臺虛擬的計算機,實際上,一個軟件,能夠執行一系列虛擬的計算指令。

可分爲系統虛擬機和軟件虛擬機。

系統虛擬機:對物理計算機的仿真,如VMWare

軟件虛擬機:專門爲單個計算程序而設計的,如JVM

二、Java內存分類

java自動內存管理,程序員只需要申請使用,系統會檢查無用的對象並回收內存,系統統一管理內存,內存使用相對高效,但也會出現異常。

線程私有內存:

  • 程序計數器:一塊小內存,每一個線程都有,存儲線程正在執行的方法,方法爲本地(native)時則值未定義,當前方法爲非本地方法時,則包含了當前正在執行指令的地址。當前唯一一塊不會引起OutOfMemoryError異常。
  • java虛擬機棧:每個線程有自己的獨立java虛擬機棧。私有的。每個方法從調用到完成對應一個棧幀在棧中入棧、出棧的過程。棧幀存儲局部變量表,操作數棧等。局部變量表存放方法中存在“棧”裏面的東西。
  • 本地方法棧:存儲native方法的執行信息,線程私有,VM規範沒有對本地方法棧做出明顯規定。

線程共享內存:

  • 堆:所有線程共享,最大的空間。對象實例和數組都是在堆上分配內存,垃圾回收主要區域,設置大小通過-Xms初始堆值,-Xmx最大堆值來設置。
  • 方法區:存儲JVM已經加載類的結構,所有線程共享。比如運行時的常量池,類信息】常量、靜態變量等。JVM啓動時,邏輯上屬於堆的一部分。很少做垃圾回收。
  • 運行時的常量池:Class文件中常量池的運行時表示,屬於方法區的一部分。java語言並不要求常量一定只有在編譯期產生。

 三、JVM內存參數

此圖爲eclipse2019中運行類時配置參數的圖:

上面爲程序參數,下面爲虛擬機參數。

-X參數:不標準,不在所有的VM通用,即一定要注意jdk版本是否支持該參數;-XX參數,不穩定,容易變更,隨着版本更新可能會淘汰。所以使用參數時一定要注意。

設置參數-Xmx20M則設置堆最大20M,

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
	public static void main(String[] args) {
		
		List<HeapObject> list = new ArrayList<>();

		while (true) {
			list.add(new HeapObject());
			System.out.println(list.size());
		}
		
		//System.out.println(Runtime.getRuntime().maxMemory()/1024/1024 + "M");
	}
}

class HeapObject {
}


輸出:
.....
810324
810325
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
	at java.base/java.util.ArrayList.grow(ArrayList.java:237)
	at java.base/java.util.ArrayList.grow(ArrayList.java:242)
	at java.base/java.util.ArrayList.add(ArrayList.java:485)
	at java.base/java.util.ArrayList.add(ArrayList.java:498)
	at HeapOOM.main(HeapOOM.java:10)

 jvm棧:

主要存儲方法,且和方法中的變量有關,所以JvmStackSOF更容易耗光內存。

 方法區:

jdk7及以前參數爲:-XX:PermSize , -XX:MaxPermSize

jak8及以後就參數更改爲 -XX:MetaspaceSize,-XX:MaxMetaspaceSize

四、對象引用判斷無用對象

準備知識:

java語言含有內存自動管理,系統會檢查無用得對象並收回內存。JVM內置了垃圾收集器用於回收。

回收時需要做到:需要判定無用得的對象,何時啓動回收,並且需要不影響程序的正常運行,回收過程需要速度快時間短影響小。

java對象的生命週期:對象通過構造函數創建,但是沒有析構函數回收內存。對象只能存在離它最近的一對大括號中。

java中有內存回收的API:

  • 如:Object的finalize方法,垃圾收集器在回收對象時調用,有且僅唄調用一次。備註:但是此方法不靠譜,因爲無法預測什麼時候被調用。
  • 如:System的gc方法,運行垃圾收集器。但是也不靠譜,還是需要虛擬機做出判斷是否釋放。

對象引用鏈:

基於對象引用判斷無用對象。零引用、互引用等。

通過一系列的"GC Roots"對象作爲起始點,從這些節點開始向下搜索。

利用對象引用鏈來判斷:

“GC Roots"對象包括

  • 虛擬機棧中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中引用的對象

引用的分類:

 強引用

設置-Xmx4M,運行後報錯,正式內存並未釋放。


public class StrongReferenceTest {

	public static void main(String[] args) {
		StringBuilder s1 = new StringBuilder();
		for(int i=0;i<10000;i++)
		{
			s1.append("00000000000000000000");
		}
		
		StringBuilder s2 = s1;
		s1 = null; //s1 爲null, 但是s2依舊佔據內存
		//s2 = null;
		System.gc(); 
		//垃圾回收, 無法對強類型引用回收, 內存被佔用, 引發異常
		
		byte[] b = new byte[1024*1024*3];
		
	}

}
輸出:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at StrongReferenceTest.main(StrongReferenceTest.java:17)

軟引用:

設置-Xmx5M,運行後內存足夠是則不釋放,內存不夠則優先釋放。

import java.lang.ref.SoftReference;

public class SoftReferenceTest {

	public static void main(String[] args) {
		StringBuilder s1 = new StringBuilder();
		for(int i=0;i<100000;i++)
		{
			s1.append("0000000000");
		}
		
		SoftReference<StringBuilder> s2 = new SoftReference<StringBuilder>(s1);
		s1 = null;
		
		System.out.println(s2.get().length()); //not null
		
		System.gc();
		//軟引用, 內存不緊張, 沒有回收
		System.out.println(s2.get().length()); //not null
		
		byte[] b = new byte[(int)(1024*1024*3.5)];
		
		System.gc();
		//內存緊張, 軟引用被回收
		System.out.println(s2.get()); //null
		
	}

}

弱引用:

WeakReference<StringBuilder> s2 = new WeakReference<StringBuilder>(s1);

虛引用:一般程序員不常用,因爲不好控制。

PhantomReference<StringBuilder> s2 = new PhantomReference<StringBuilder>(s1,queue);

五、垃圾收集算法

引用計數法:

有引用加一,引用失效減一,計數器爲0的對象則回收

優點:簡單,高效。缺點:無法識別對象之間的循環引用

標記-清除法:

標記所有需要回收的對象,統一回收所有被標記的對象

優點:簡單。缺點:效率不高,內存碎片。

複製算法:

優點:簡單、高效。缺點:可用內存減少,對象存活率高時賦值操作較多。

標記-整理算法

標記需待回收的對象,整理時將所有存活的對象都向一端移動,然後直接清理端編輯以外的內存。

優點:比賣你碎片產生,無需兩塊相同的內存。缺點:計算代價大,標記+整理,更新引用地址。

分代收集:

一般都會採用此方法,分爲新生代和老年代。

新生代:存放短暫生命週期的對象,新創建的對象都先放入新生代。

老年代:一個對象經過幾次gc仍然存活則放入老年代。這些對象可以活很長時間。

新生代:採用複製算法

老年代:採用標記清除或者標記整理。

六、堆內存參數和GC跟蹤。


/**
 * 來自於《實戰Java虛擬機》
 * -Xms5M -Xmx20M -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
 * @author Tom
 *
 */
public class HeapAlloc {

	public static void main(String[] args) {
		
		printMemoryInfo();
		byte[] b = new byte[1*1024*1024];
		System.out.println("分配1MB空間");
		
		printMemoryInfo();
		b = new byte[4*1024*1024];
		System.out.println("分配4MB空間");

		printMemoryInfo();
	}
	
	public static void printMemoryInfo()
	{
		System.out.print("maxMemory=");
		System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024.0 + " MB");
		System.out.print("freeMemory=");
		System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024.0 + " MB");
		System.out.print("totalMemory=");
		System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024.0 + " MB");
	}

}

 


/**
 * 來自於《實戰Java虛擬機》
 * -Xmx20m -Xms20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
 * 新生代1M,eden/s0=2, eden 512KB, s0=s1=256KB 
 * 新生代無法容納1M,所以直接放老年代
 * 
 * -Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
 * 新生代7M,eden/s0=2, eden=3.5M, s0=s1=1.75M
 * 所以可以容納幾個數組,但是無法容納所有,因此發生GC
 * 
 * -Xmx20m -Xms20m -Xmn15m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC
 * 新生代15M,eden/s0=8, eden=12M, s0=s1=1.5M
 * 
 * -Xmx20m -Xms20m -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
 * 新生代是6.6M,老年代13.3M
 * @author Tom
 *
 */
public class NewSizeDemo {

	public static void main(String[] args) {
		
		byte[] b = null;
		for(int i=0;i<10;i++)
		{
			b = new byte[1*1024*1024];
		}		
	}
}

收集器還有很多種,性能也都不一樣。可以後續瞭解。

 參考中國大學mooc《Java核心技術》

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