概述
Java虛擬機會自動管理內存,不容易出現內存泄漏和內存溢出問題。Java虛擬機會在執行過程中將管理的內存分為若干個不同的數據區域。
運行時數據區域
在jdk1.8之前的版本與1.8版本略有不同,在jdk1.8之前:
jdk1.8:
以上圖片來源:https://github.com/LikFre/JavaGuide
線程共享區域:
1.堆
2.方法區
3.直接內存(非運行時數據區)
線程私有區域:
1.虛擬機棧
2.本地方法棧
3.程序計數器
線程私有區域:
1.虛擬機棧
1.它的生命周期隨着線程的創建而創建,隨着線程的結束而死亡;
2.描述的是java方法執行的內存模型,java虛擬機棧是由一個個棧幀組成,每個棧幀都有局部變量表、動態鏈接、操作數棧、方法出口信息。
3.局部變量表主要存放可知的各種數據類型(8種基本數據類型變量,對象引用變量);
4.Java虛擬機棧是線程私有的,每個線程都有自己的虛擬機棧;
5.java虛擬機棧中主要存放的是一個個棧幀,每調用一次方法都會有對應的棧幀壓入虛擬機棧,每一個方法執行結束后,都會有一個棧幀彈出。
Java方法有兩種返回方式:1、return;
2、拋出異常;
這兩種方式都會導致棧幀彈出;
java虛擬機棧會出現兩種異常:StackOverFlowError和OutOfMemoryError :
1.StackOverFlowError:如果java虛擬機棧的內存大小不允許動態擴展,當虛擬機棧請求的內存超過棧的最大內存時就會出現StackOverFlowError異常;
2.OutOfMemoryError:如果java虛擬機棧的內存大小允許動態擴展,當線程請求棧的內存已用完,且無法再動態擴展時,拋出OutOfMemoryError異常;
2.本地方法棧
1.與java虛擬機棧的生命周期相同,都是線程私有,隨着線程的創建而創建,線程的結束而死亡。
2.描述的是Native方法執行的內存模型,也是由棧幀組成,每個棧幀用於存放本地方法的局部變量表,操作數棧、動態鏈接、方法出口信息;
3.也會出現兩種異常:StackOverFlowError和OutOfMemoryError
在HotSpot虛擬機中,虛擬機棧與本地方法棧合二為一;
3.程序計數器
1.程序計數器是一塊較小的內存空間,字節碼解釋器通過改變計數器的值來選取下一條字節碼指令。分支、跳轉、循環、異常處理、線程恢復都需要依賴程序計數器;
2.程序計數器也是線程私有的,每個線程都有自己的程序計數器;
3.程序計數器主要有兩個作用:
1.字節碼解釋器通過改變程序計數器的值來順序的執行字節碼指令;如:順序執行、循環、跳轉等;
2.在多線程環境下,程序計數器用於記錄當前線程執行的位置,當CPU切換再次執行當前線程時從程序計數器記錄的位置繼續執行;
注意:程序計數器是唯一一個不會出現OutOfMemoryError異常的內存區域,它的生命周期隨着線程的創建而創建,線程的結束而死亡;
線程共享區域:
1.堆
1.java虛擬機管理的最大的一塊內存區域,是所有線程共享的內存區域,隨着虛擬機的啟動而創建,主要用於存放對象實例,幾乎所有的對象實例和數組都在堆中存儲。
2.堆也是垃圾收集器主要管理的區域,因此也被稱為GC堆(Garbage Collected Heap);從垃圾回收的角度:由於現在的收集器都采用分代收集算法,堆又被分為:新生代和老年代,進一步分為Eden空間、From Survivor、ToSurvivor。進一步划分目的是為了更好的回收內存或者更好的分配內存。
3.上圖中,Eden、S0、S1是新生代,TenTired是老年代。大部分情況,一個對象實例創建后存儲在Eden區域,經歷過一次垃圾回收之后,如果對象還存活,對象的年齡加+1(由Eden區進入到Survivor區),進入S0或者S1,當對象的年齡達到一定的閾值(默認是15),這個對象才會進入老年代區域,對象的閾值可以通過參數(-XX:MaxTenuringThreshold)來設置;
2.方法區
1.與java堆一樣,是線程共享的內存區域,主要用於存儲加載過的類信息、被final修飾的常量、靜態變量以及即時編譯器編譯的代碼,雖然java虛擬機規范把方法區列為堆的一個邏輯部分,但是它卻有一個別名NON-Heap(非堆),可能就是為了和堆區分開。
2.方法區也被稱為永久代,方法區是java虛擬機中的一種規范,並沒有去實現它,在不同的JVM方法區的實現也不同,永久代是HotSpot的概念,在HotSpot虛擬機中永久代是對方法區的一種實現,其他的虛擬機並沒有永久代這一說法。
3.在jdk1.8之前,方法區還沒有被移除,通過兩個參數可以設置永久代的大小:
-XX:PermSize=N //方法區 (永久代) 初始大小
-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常
垃圾收集行為在這個區域很少出現,但並不是數據進入方法區就一直存在
4.jdk1.8版本,方法區(永久代)被移除,取而代之的時元空間,使用的是直接內存,可以通過以下兩個參數設置
-XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小
與方法區最大的的不同就是,如果不指定大小,隨着更多的類創建,虛擬機可能會耗盡所有可用的系統內存
5.為什么要用元空間替換永久代?
整個永久代有JVM設置的固定大小上限,無法進行調整,而元空間使用的是直接內存,只受本機可用內存大小的限制,如果不指定最大元空間大小,-XX:MaxMetaspaceSize默認大小是unlimited,只受到本機可用內存大小限制,如果不指定初始大小,JVM會動態的分配應用程序所需要的大小。這只是其中一個原因,有興趣的可以去查閱資料。
3.運行時常量池
1.是方法區的一部分,主要用來存放編譯器生成的各種字面量和引用。字面量(文本字符串、基本數據類型的值、聲明為final的常量值、其他)
2.常量池的大小也受到方法區大小的限制,超過常量池的大小會報OutOfMemoryError異常。
3.jdk1.7及以后JVM將運行時常量池從方法區移出,在堆中開辟了一塊區域用來存放運行時常量池。
直接內存:
不屬於運行時數據區,也不是虛擬機規范中定義的內存區域,但是被頻繁的使用,也有可能引起OutOfMemoryError異常。
jdk1.4中新加入的NIO,使用一種基於通道(Channel)和緩存(Buffer)的方式,直接使用Native方法分配堆外內存,通過在java堆中一個DirectByteBuffer對象作為這塊內存的引用來操作,避免了java堆與Native堆之間來回復制數據,提高了性能。
HotSpot虛擬機:
1.對象的創建:
Step1:類加載檢查,虛擬機遇到一條new指令時,首先檢查指令的參數是否能在常量池中定位到這個類的符號引用,然后檢查符號引用代表的類是否被加載、解析和初始化過,沒有則執行相應的類加載過程。
Step2:分配內存,類加載檢查通過后,虛擬機為新生的對象分配內存,在類加載完成后已經確定了對象的大小,分配內存實際就是在堆中分配一塊確定大小的內存,分配內存有兩種方式:指針碰撞和空閑列表,
分配內存的方式取決於java堆內存是否規整,java堆內存是否規整取決於GC收集算法是“標記-清理”還是“標記-整理”;
1.指針碰撞:堆內存規整,使用過的內存全部整合到一邊,沒有使用的內存在另一邊,中間有一個分界值指針,只需要將指針往沒有使用的內存方向移動對象內存大小即可。GC收集器:Serial、ParNew;
2.空閑列表:堆內存不規整,虛擬機會維護一個列表,列表中記錄未使用的內存大小,選擇一塊足夠的內存來為對象分配,更新列表的記錄。GC收集器:CMS
分配內存的並發問題:對象的創建是很頻繁的,虛擬機要保證線程的安全性,采用兩種方式:
1.CAS+失敗重試:CAS是樂觀鎖的一種方式,每次認為不會有沖突的去執行某項操作,遇到沖突一直重試,知道成功為止。虛擬機使用CAS加失敗重試的方式來保證更新操作的原子性。
2.TLAB:為每一個線程預先在Eden區分配一塊內存,線程為對象分配內存時,首先在TLAB中分配,TLAB中內存不夠時或者已經用盡時,再使用CAS上述方式。
Step3:初始化零值,內存分配完成后,需要將分配到的內存空間初始化為零值,這一步保證了對象在不初始化的情況下,對象的實例字段在java代碼中可以不賦初始值就使用。
Step4:設置對象頭,初始化零值完成之后,虛擬機要對對象進行必要的設置,列如:這個對象是哪個類的實例、如何才能找到對象的元數據、對象的哈希碼、對象的GC分代年齡等信息,這些信息存儲在對象頭中。
Step5:執行init方法,在new出對象之后,把對象按照程序員的意願進行初始化,執行init方法。
2.對象在內存中的布局:
對象在內存中的布局可分為三個區域:對象頭、實例數據和對齊填充;
1.對象頭分為兩部分:一部分存儲自身的運行時數據(哈希碼、GC分代年齡等),另一部分類型指針,通過指針來確定這個對象是哪個類的實例
2.實例數據:是真正存儲對象的有效信息,對象的各種類型的字段內容。
3.對齊填充:不是必然存在的只是起到占位作用。HotSpot虛擬機的自動內存管理系統要求對象的起始地址必須是8字節的整數倍,前兩部分不足8字節的整數倍時,使用對齊填充補足。
3.對象的訪問定位:
創建對象就是為了使用對象,通過在棧上存儲的引用類型變量來操作堆上的具體類型變量,對象的訪問方式由虛擬機決定,目前主流的有兩種:①使用句柄 ②直接指針
1.使用句柄:java堆中會有一塊內存作為句柄池,棧中的引用類型變量中保存的是句柄池的句柄,句柄池中有對象的實例數據和對象類型信息的地址,也就是引用變量訪問句柄池,句柄池再訪問對象。
2.直接指針:棧中的引用類型變量中直接保存的是對象的地址,可以直接訪問。
使用句柄訪問的好處是,變量中保存的是穩定的句柄池的地址,在對象被移動時改變句柄池中的地址即可,變量保存的地址不需要改變。
使用直接指針的好處是:速度快,節省了一次指針定位的時間。
內容補充:
String類與常量池
String對象的兩種創建方式:
String str1 = "Hello" ; String str2 = new String("Hello"); String str3 = new String("Hello"); System.out.println(str1 == str2);//false System.out.println(str2 == str3);//false
str1.先檢查常量池中有沒有“Hello”,沒有就在常量池中創建,人后str1指向常量池中“Hello”的地址,常量池中已經有的話,str1直接指向常量池中“Hello”的地址。
str2,在堆中重新創建一個新的對象
第一種方式:在常量池中拿對象,
第二種方式:直接在堆內存中創建新的對象
String類型的常量池有兩種使用方式
String str1 = new String("Hello" ); String str2 = str1.intern(); String str3 = "Hello"; System.out.println(str1 == str2);//true System.out.println(str2 == str3);//false
上述代碼中 str2和str3指向的都是字符串常量池中的“Hello”
第一種方式:使用雙引號聲明的String對象會直接存儲在常量池中
第二種方式:使用String提供的intern方法,intern()方法是一個本地方法,它的作用是:如果當前字符串對象的字符串已經在常量池中,那么直接返回常量池中該字符串的引用,如果當前字符串對象的字符串內容不在常量池中,那么在常量池中創建一個字符串,返回該字符串的引用。
字符串拼接:
String str1 = "hel"; String str2 = "lo"; String str3 = "hel"+"lo"; String str4 = str1 + str2; String str5 = "hello"; System.out.println(str3 == str4);//false System.out.println(str4 == str5);//false System.out.println(str3 == str5);//true
str4是在堆上新建的對象,str3與str5都是常量池中的“hello”
8種基本類型的包裝類與常量池
java基本類型的包裝類中有六種實現的常量池技術:Byte、Short、Integer、Long、Character、Boolean,前五種默認使用[-128,127]的緩存數據,超出這個范圍才會創建對象。Double和Float沒有實現常量池技術。
Integer i1 = 44; 默認使用Integer.valueOf(44),從而使用常量池中的數據,只有超過【-128,127】才會在對中創建對象。
Integer i2 = new Integer(44);這種直接在堆中創建對象,
Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); System.out.println("i1=i2 " + (i1 == i2));//true System.out.println("i1=i2+i3 " + (i1 == i2 + i3));//true System.out.println("i1=i4 " + (i1 == i4));//false System.out.println("i4=i5 " + (i4 == i5));//false System.out.println("i4=i5+i6 " + (i4 == i5 + i6));//true System.out.println("40=i5+i6 " + (40 == i5 + i6));//true
i5+i6這個操作符“+”不適用於Integer對象,所以i5與i6自動拆箱,加之后就變為:i4=40,又因為i4是Integer對象,無法與int對象比較,i4自動拆箱為int值為40,所以相等
原文地址:https://github.com/LikFre/JavaGuide/blob/master/docs/java/jvm/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F.md