一、虛擬機簡介
邏輯上,可看作一臺虛擬的計算機,實際上,一個軟件,能夠執行一系列虛擬的計算指令。
可分爲系統虛擬機和軟件虛擬機。
系統虛擬機:對物理計算機的仿真,如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核心技術》