相對於C、C++等語言來說,Java語言一個很美好的特性就是自動內存管理機制。C語言等在申請堆內存時,需要malloc內存,用完還有手動進行free操作,若程序員忘記回收內存,那這塊內存就只能在進程退出時,由操作系統來釋放了。而Java程序員(初級)則基本上不需要對內存分配、回收做過多的關注,完全由Java虛擬機來管理。不過,一旦出現內存泄漏或者溢出,如果不理解JVM管理內存的機制,又如何排除錯誤、調優系統呢?
1. 運行時區域
1.1程序計數器
Java程序最終編譯成字節碼運行在JVM之上,程序計數器可以看做時當前線程執行的字節碼的行號指示器。字節碼解釋器在工作的時候就是通過這個計數器來選擇下一條要執行的字節碼指令,分支、循環、異常處理等都需要依賴該計數器。
另外,在多線程的場景下,一個CPU(或者一個核)在一個確定的時刻,只能執行一個線程的一條字節碼指令,多線程的實現是由CUP在不同線程間切換來完成的。而CPU在線程間切換所依賴的也是程序計數器(CPU跳來跳去要確定調到某個線程的某一行上,從這一點可以看出,程序計數器是線程私有的,線程間互不影響)。
注意,在JVM規范中,程序計數器不會發生OOM(就記個數,能用多少內存)。
1.2虛擬機棧
線程私有,與線程生命周期相同。
棧描述的是Java執行方法的內存模型。線程是進程創造的(例如服務器的每個請求可以看做是一個線程,舉例ThreadLocal),由多個方法間的調用組成,每個方法在執行時會創建一個棧幀,棧幀內存儲的是局部變量表,操作數棧,動態鏈接,方法出口等信息。每個方法從調用直到執行完成,就是一個棧幀入棧到出棧的過程。
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。類型:boolean、byte、char、short、int、float、reference(對象起始地址的指針或者句柄)、returnAddress(指向了一條字節碼指令的地址)八種。在編譯時,每個方法所需的局部變量表大小就固定下來了。(疑問:若在循環體中定義變量,JVM如何取得的局部變量表的大小? 在內層循環中定義變量到底會不會存在重復分配的問題,這涉及到編譯器的優化,不過主流編譯器(如vs和gcc)這一塊優化都比較好,不會反復分配變量。棧中的空間在編譯這個代碼的時候大小就確定下來了,運行這個方法時空間就已經分配好了,不要想當然的以為聲明一次就要分配一次空間,那是c語言,java可以重用這些超出作用域的空間。)
虛擬機棧這塊區域規定了兩種異常:StackOverflowError,線程請求的棧深度超過一定量(比如遞歸層級過多,大概幾千(與分配給jvm的內存有關)就報錯);OutOfMemoryError,無法申請到足夠的內存。
1.3本地方法棧
本地方法棧與虛擬機方法棧作用相似,區別為虛擬機棧為虛擬機執行Java方法服務,本地方法棧為虛擬機使用的native方法服務。很多虛擬機在實現時已經將二者合二為一。拋錯相同。
Java native 方法:一個Native Method就是一個java調用非java代碼的接口。大多數應用場景為java需要與一些底層系統如操作系統、某些硬件交換信息時的情況。
1.4Java堆
所有線程共享,用於存放所有線程產生的對象實例(還有數組)。
堆是垃圾收集器管理的主要區域。為了更好的回收或者分配內存,堆可能會被分為多個區域,例如分代收集算法的垃圾回收器會將堆分為新生代和老年代(當然還可以繼續細分:Eden、From Survivor、To Survivor等)。但不管如何划分,每個區間存儲的內容是不變的,都是對象實例。
另外,堆在內存中並不是物理連續的,只要邏輯連續即可。當向堆申請內存(實例化對象),而堆中找不到這么大的空間時)會拋出OutOfMemoryError(最新虛擬機都可動態擴展,但擴無可擴時也會拋錯)。
1.5方法區
線程共享,方法區內存儲的是已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。一些虛擬機實現上,將方法區作為堆上的“永久代”,意味着垃圾回收器可以向管理堆一樣來管理這塊內存(但本質上,方法區和永久代是不等價的,會產生一些問題,官方已經不推薦這么使用。例如,String.intern()方法在不同的虛擬上會因為該機制而表現不同)。
當然,方法區也確實有一些“永久”的意思,進入到該區域的數據,例如類信息,基本上就不會被卸載了。但其實也會被卸載,只是卸載的條件相當的苛刻,導致很多垃圾回收器在這部分起到的作用並不大
當方法區無法滿足內存分配要求時,將拋出OutOfMemoryError異常。
1.6運行時常量池
是上邊1.5里講的方法區中的一部分。Class文件在編譯期會生成各種字面量和符號引用,這部分內容將在類加載后,放入到方法區的常量池中存放。另外,並非只有預置入Class文件中的常量池的部分才能進入方法區的運行時常量池,運行期間也可能將新的常量放入池中,例如String類的intern()方法。
運行時常量池屬於方法區的一部分,所以當申請不到內存的時候,會拋出OutOfMemoryError異常。
1.7直接內存
一些native函數庫可以直接分配堆外內存,例如NIO,它可以通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。由於直接內存是受機器總內存限制的,當申請不到內存的時候,同樣會拋出OutOfMemoryError異常。
2. 對象的創建
2.1對象的創建
創建對象的幾種方式:1)使用new 關鍵字;2)使用反射的newInstance()方法,newInstance方法通過調用無參的構造函數創建對象;3)clone,調用clone時,jvm會創建一個新的對象,將前面對象的內容全部拷貝進去。用clone方法創建對象並不會調用任何構造函數;4)反序列化,jvm會給我們創建一個單獨的對象。在反序列化時,jvm創建對象並不會調用任何構造函數。 我們在着重談一談new時都發生了什么。
當jvm遇到new指令時,第一步要做的是去常量池中找一找,看是否能找到對應類的符號引用,並且檢查該符號引用代表的類是否被加載、解析、初始化過(檢查類是否被加載)。
類加載檢查通過之后,接下來就是分配內存,對象所需內存大小在類加載完成之后就完全確定了,所以分配對象的工作其實就是把一塊確定的內存從Java堆中划出來。
堆內存是規整的時候——用過的在一邊、沒用過的在另一邊,中間用一個指針標記,內存分配就是指針向沒用過的方向挪動一下,這種方式叫做指針碰撞。這個時候若多個線程一起申請內存,就會沖突。對應的解決方法:1)加同步,采用CAS加失敗重試策略;2)為每個線程預分配一小塊內存(Thread Local Allocation Buffer,TLAB),哪個線程需要內存,就在自己的TLAB上進行分配,而只在創建線程為線程分配TLAB是用同步鎖定。
堆內存不是規整的時候——用過和沒用過的亂糟糟的放在一起,內存分配就需要記住哪些地方被分配了,哪些地方還是空閑的,這種分配方式叫做分配列表。在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表。
對內存是否規整,是由使用的垃圾回收機制是否帶有壓縮整理功能決定的。
內存分配完成之后,虛擬機需要設置一下對象的數據:非對象頭部分,會被初始化為零值,這個操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用;對象頭部分,進行必要的設置,例如:對象類的元數據信息、哈希碼、GC分代信息、鎖信息等。
一個新的對象產生了,后續就在java語言層面,按照程序員的想法,執行init函數了。
2.2對象的內存布局
一個對象在內存中由三部分組成:對象頭,實例數據,對齊填充。
對象頭由兩部分組成:一部分存儲運行時數據:哈希碼、GC分代、鎖狀態等等;另一部分是指向類元數據的指針,說明該對象是由哪個類實例化來的。
實例數據存放的是對象真正存儲的有效信息,也就是程序員自己定義的各種類型字段內容。需要注意的時,為了節省內存,相同類型的字段總是被放在一起存放的,而且子類較窄的變量有可能會插入到父類變量的空隙中。
由於對象大小必須是8字節的整數倍,所以對齊填充,就是湊整用的,可有可無。
2.3對象的訪問定位
兩種定位方式:句柄、直接指針。貼兩個圖,分別說一下他們的優缺點。
句柄訪問,堆內划分出一塊內存來作為句柄池,對象引用存儲的是句柄地址,句柄中包含了對象的真實地址信息。有點:對象被移動時,無需通知引用這個它的對象,只需要更改句柄池就行了;缺點:增加了一層尋址,會慢一些。
直接指針訪問:對象引用的就是真實的地址信息。優點:快,節省一次指針定位時間;缺點:對象被移動時,引用它的對象也要跟着修改。
3. 關於內存溢出
3.1棧溢出
不斷遞歸,超過棧允許的最大深度時,就可以觸發StackOverflowError。看一個棧深度超限引發StackOverflowError的示例,代碼及錯誤信息如下:
1 public class Stack_StackOverflowError { 2 private Integer stackLength = 1; 3 4 public void stackLoop() { 5 stackLength++; 6 stackLoop(); 7 } 8 9 public static void main(String[] args) { 10 Stack_StackOverflowError a = new Stack_StackOverflowError(); 11 try { 12 a.stackLoop(); 13 } catch (Throwable e) { 14 System.out.println("stack length: " + a.stackLength); 15 throw e; 16 } 17 } 18 }
Exception in thread "main" stack length: 9651(本人機器64位,12G內存,未對jvm系統做任何參數修改) java.lang.StackOverflowError at java.lang.Number.<init>(Number.java:55) at java.lang.Integer.<init>(Integer.java:849) at java.lang.Integer.valueOf(Integer.java:832) at com.star.ott.scriptsTranslation.api.business.test.Stack_StackOverflowError.stackLoop(Stack_StackOverflowError.java:10) at com.star.ott.scriptsTranslation.api.business.test.Stack_StackOverflowError.stackLoop(Stack_StackOverflowError.java:11) at com.star.ott.scriptsTranslation.api.business.test.Stack_StackOverflowError.stackLoop(Stack_StackOverflowError.java:11)
3.2堆溢出
堆是用來存放對象示例的,只要不斷創建對象,並且保證垃圾回收器無法回收這些對象,就能產生堆的OutOfMemoryError異常。看一個不斷創建對象引發OutOfMemoryError的示例,代碼及錯誤信息如下:首先將idea中的堆大小限制為20M。
import java.util.ArrayList; import java.util.List; /** * Created by laizy on 2018/7/30. */ public class Heap_OutOfMemoryError { static class OOMTestObject { } public static void main(String[] args) { //保證創建出來的對象不被回收 List<Heap_OutOfMemoryError.OOMTestObject> list = new ArrayList<Heap_OutOfMemoryError.OOMTestObject>(); //不斷創建對象 while (true) { list.add(new Heap_OutOfMemoryError.OOMTestObject()); System.out.println(list.size()); } } }
540213 540214 540215 540216 540217(向隊列中插入這么多對象之后,崩了) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.star.ott.aaa.Heap_OutOfMemoryError.main(Heap_OutOfMemoryError.java:20) Process finished with exit code 1
另外,在jdk1.8中,String常量池已經從方法區中的運行時常量池分離到堆中了(划重點),也就是說不斷的創建String常量,也能夠將堆撐爆,代碼及錯誤信息如下:
import java.util.ArrayList;
import java.util.List;
/**
* Created by laizy on 2018/7/31.
*/
// -Xms20m -Xmx20m
public class Heap_StringConstantOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("0");
int i = 1;
try {
while (true) {
list.add(list.get(i - 1) + String.valueOf(i++).intern());
if (list.size() % 100 == 0) {
System.out.println(list.size());
}
}
} catch (Throwable e) {
System.out.print(list.size());
throw e;
}
}
}
1900
2000
2100
2200
2201
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.star.ott.aaa.Heap_StringConstantOOM.main(Heap_StringConstantOOM.java:15)
3.3方法區溢出
運行時常量池屬於方法區的一部分,首先我們通過將常量池撐爆的方式,制造方法區溢出。首先還是限制jvm的參數,設置方法區大小為5m,不限制的話,程序得跑到地老天荒。參照3.2中設置jvm的方式設置方法區大小。在jdk8之前,方法區放到了永久代中,對應參數為:-XX: PermSize=5m -XX:MaxPermSize=5m;在jdk8以后,方法區放到的元數據里,對應參數為:-XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5m。代碼及錯誤信息如下:
好吧,讓你失望了,我的環境是jdk8,在jdk8中我做不到(捂臉),希望大家指點一下,如何在jdk8中實現常量池的溢出。
另外,在之前的jdk中,要實現常量池的溢出是通過不斷創建String來實現的,對,就是上邊3.2中的用String.intern()撐爆堆的那種做法。
接下來我們通過CGLib技術,不斷創建動態類,將方法區撐爆。代碼及異常如下:
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * Created by laizy on 2018/7/31. */ // -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m public class RunTime_ObjectOOM { public static void main(String[] args) { int i = 0; while (true) { i++; System.out.println(i); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } static class OOMObject { } }
328 329 330 331(331次循環之后,方法區崩了) Exception in thread "main" java.lang.OutOfMemoryError: Metaspace at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:386) at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:219) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377) at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285) at com.star.ott.aaa.RunTime_ObjectOOM.main(RunTime_ObjectOOM.java:28)
最后,棧中的OOM、直接內存OOM並未做驗證。