相對於C、C++等語言來講,Java語言一個很美好的特性就是自動內存管理機制。C語言等在申請堆內存時,須要malloc內存,用完還有手動進行free操做,若程序員忘記回收內存,那這塊內存就只能在進程退出時,由操做系統來釋放了。而Java程序員(初級)則基本上不須要對內存分配、回收作過多的關注,徹底由Java虛擬機來管理。不過,一旦出現內存泄漏或者溢出,若是不理解JVM管理內存的機制,又如何排除錯誤、調優系統呢?java
Java程序最終編譯成字節碼運行在JVM之上,程序計數器能夠看作時當前線程執行的字節碼的行號指示器。字節碼解釋器在工做的時候就是經過這個計數器來選擇下一條要執行的字節碼指令,分支、循環、異常處理等都須要依賴該計數器。程序員
另外,在多線程的場景下,一個CPU(或者一個核)在一個肯定的時刻,只能執行一個線程的一條字節碼指令,多線程的實現是由CUP在不一樣線程間切換來完成的。而CPU在線程間切換所依賴的也是程序計數器(CPU跳來跳去要肯定調到某個線程的某一行上,從這一點能夠看出,程序計數器是線程私有的,線程間互不影響)。算法
注意,在JVM規範中,程序計數器不會發生OOM(就記個數,能用多少內存)。api
線程私有,與線程生命週期相同。數組
棧描述的是Java執行方法的內存模型。線程是進程創造的(例如服務器的每一個請求能夠看作是一個線程,舉例ThreadLocal),由多個方法間的調用組成,每一個方法在執行時會建立一個棧幀,棧幀內存儲的是局部變量表,操做數棧,動態連接,方法出口等信息。每一個方法從調用直到執行完成,就是一個棧幀入棧到出棧的過程。服務器
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。類型:boolean、byte、char、short、int、float、reference(對象起始地址的指針或者句柄)、returnAddress(指向了一條字節碼指令的地址)八種。在編譯時,每一個方法所需的局部變量表大小就固定下來了。(疑問:若在循環體中定義變量,JVM如何取得的局部變量表的大小? 在內層循環中定義變量到底會不會存在重複分配的問題,這涉及到編譯器的優化,不過主流編譯器(如vs和gcc)這一塊優化都比較好,不會反覆分配變量。棧中的空間在編譯這個代碼的時候大小就肯定下來了,運行這個方法時空間就已經分配好了,不要想固然的覺得聲明一次就要分配一次空間,那是c語言,java能夠重用這些超出做用域的空間。)多線程
虛擬機棧這塊區域規定了兩種異常:StackOverflowError,線程請求的棧深度超過必定量(好比遞歸層級過多,大概幾千(與分配給jvm的內存有關)就報錯);OutOfMemoryError,沒法申請到足夠的內存。app
本地方法棧與虛擬機方法棧做用類似,區別爲虛擬機棧爲虛擬機執行Java方法服務,本地方法棧爲虛擬機使用的native方法服務。不少虛擬機在實現時已經將兩者合二爲一。拋錯相同。jvm
Java native 方法:一個Native Method就是一個java調用非java代碼的接口。大多數應用場景爲java須要與一些底層系統如操做系統、某些硬件交換信息時的狀況。ide
全部線程共享,用於存放全部線程產生的對象實例(還有數組)。
堆是垃圾收集器管理的主要區域。爲了更好的回收或者分配內存,堆可能會被分爲多個區域,例如分代收集算法的垃圾回收器會將堆分爲新生代和老年代(固然還能夠繼續細分:Eden、From Survivor、To Survivor等)。但無論如何劃分,每一個區間存儲的內容是不變的,都是對象實例。
另外,堆在內存中並非物理連續的,只要邏輯連續便可。當向堆申請內存(實例化對象),而堆中找不到這麼大的空間時)會拋出OutOfMemoryError(最新虛擬機均可動態擴展,但擴無可擴時也會拋錯)。
線程共享,方法區內存儲的是已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。一些虛擬機實現上,將方法區做爲堆上的「永久代」,意味着垃圾回收器能夠向管理堆同樣來管理這塊內存(但本質上,方法區和永久代是不等價的,會產生一些問題,官方已經不推薦這麼使用。例如,String.intern()方法在不一樣的虛擬上會由於該機制而表現不一樣)。
固然,方法區也確實有一些「永久」的意思,進入到該區域的數據,例如類信息,基本上就不會被卸載了。但其實也會被卸載,只是卸載的條件至關的苛刻,致使不少垃圾回收器在這部分起到的做用並不大
當方法區沒法知足內存分配要求時,將拋出OutOfMemoryError異常。
是上邊1.5裏講的方法區中的一部分。Class文件在編譯期會生成各類字面量和符號引用,這部份內容將在類加載後,放入到方法區的常量池中存放。另外,並不是只有預置入Class文件中的常量池的部分才能進入方法區的運行時常量池,運行期間也可能將新的常量放入池中,例如String類的intern()方法。
運行時常量池屬於方法區的一部分,因此當申請不到內存的時候,會拋出OutOfMemoryError異常。
一些native函數庫能夠直接分配堆外內存,例如NIO,它能夠經過一個存儲在Java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做。因爲直接內存是受機器總內存限制的,當申請不到內存的時候,一樣會拋出OutOfMemoryError異常。
建立對象的幾種方式: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函數了。
一個對象在內存中由三部分組成:對象頭,實例數據,對齊填充。
對象頭由兩部分組成:一部分存儲運行時數據:哈希碼、GC分代、鎖狀態等等;另外一部分是指向類元數據的指針,說明該對象是由哪一個類實例化來的。
實例數據存放的是對象真正存儲的有效信息,也就是程序員本身定義的各類類型字段內容。須要注意的時,爲了節省內存,相同類型的字段老是被放在一塊兒存放的,並且子類較窄的變量有可能會插入到父類變量的空隙中。
因爲對象大小必須是8字節的整數倍,因此對齊填充,就是湊整用的,無關緊要。
兩種定位方式:句柄、直接指針。貼兩個圖,分別說一下他們的優缺點。
句柄訪問,堆內劃分出一塊內存來做爲句柄池,對象引用存儲的是句柄地址,句柄中包含了對象的真實地址信息。有點:對象被移動時,無需通知引用這個它的對象,只須要更改句柄池就好了;缺點:增長了一層尋址,會慢一些。
直接指針訪問:對象引用的就是真實的地址信息。優勢:快,節省一次指針定位時間;缺點:對象被移動時,引用它的對象也要跟着修改。
不斷遞歸,超過棧容許的最大深度時,就能夠觸發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)
堆是用來存放對象示例的,只要不斷建立對象,而且保證垃圾回收器沒法回收這些對象,就能產生堆的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)
運行時常量池屬於方法區的一部分,首先咱們經過將常量池撐爆的方式,製造方法區溢出。首先仍是限制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並未作驗證。