一、堆
定義: Heap,通過new關鍵字創建的對象,都存放在堆內存中。
特點
- 線程共享,堆中的對象都存在線程安全的問題
- 垃圾回收,垃圾回收機制重點區域。
jvm內存的划分:
- JVM內存划分為堆內存和非堆內存,堆內存分為年輕代(Young Generation)、老年代(Old Generation),非堆內存就一個永久代(Permanent Generation)。
- 年輕代又分為Eden和Survivor區。Survivor區由FromSpace和ToSpace組成。Eden區占大容量,Survivor兩個區占小容量,默認比例是8:1:1。
- 非堆內存用途:永久代,也稱為方法區,存儲程序運行時長期存活的對象,比如類的元數據、方法、常量、屬性等。
- 年輕代(New):年輕代用來存放JVM剛分配的Java對象
- 年老代(Tenured):年輕代中經過垃圾回收沒有回收掉的對象將被Copy到年老代
- 永久代(Perm):永久代存放Class、Method元信息,其大小跟項目的規模、類、方法的量有關,一般設置為128M就足夠,設置原則是預留30%的空間,方法區。
堆內存查看的相關指令:
- jps
查看系統有哪些進程。
- jmap
查看堆內存使用情況 jmap -heap PID
- jconsole
圖形界面,多功能檢測工具,連續監測
二、方法區
定義: 其中主要存儲class文件的信息和運行時常量池,class文件的信息包括類信息和class文件常量池。
class文件結構:
- 最頭的4個字節用於存儲魔數Magic Number,用於確定一個文件是否能被JVM接受
- 接着4個字節用於存儲版本號,前2個字節存儲次版本號,后2個存儲主版本號
- 再接着是用於存放常量的常量池,由於常量的數量是不固定的,所以常量池的入口放置一個U2類型的數據(constant_pool_count)存儲常量池容量計數值、類信息、父類與接口數組、方法信息。
三、常量池、運行時常量池、字符串池
1、常量池:就是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息,我們可以通過Javap -v 類名.class 指令反編譯一個簡單的程序看到如下的常量池信息
左邊“#1”為常量池中的符號地址。
2、運行時常量池:常量池是 class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,並把里面的符號地址變為真實地址。
3、字符串池:在JVM里實現字符串池功能的是一個StringTable類,它的底層是一個HashTable,里面存的是字符串對象的引用(而不是字符串實例本身),真正的字符串實例是存放在堆內存中的(並且字符串池在邏輯上是屬於運行時常量池的一部分)
4.常量池和字符串池的關系:
下面來看段代碼:
public static void main(String[] args) { String s1 = "b"; String s2 = "c"; String str = new String("b"); System.out.println(s1 == str); //false }
然后通過反編譯觀察字節碼文件
說明:在jdk1.8時,最開始編譯時字符串都是常量池中的符號,尚未轉化為對象,當程序執行時,常量池中的信息都會被加載到運行時常量池中,這才轉化成了對象,並且看StringTable中有沒有"b","c"對象,如果沒有則把 "b" 和 "c" 對象的引用值存入StringTable,真正的對象實例則在堆中;如果有的話則不會存入,這樣就避免了重復創建字符串對象。
再來分析String str = new String("b")這行代碼:
可以看出,這行代碼創建的對象個數因StringTable中有沒有“b”對象而異,如果字符串池有“b”,則此時只會創建一個對象:也就是new的一個字符串對象,存放在堆中;如果沒有就會創建兩個對象,一個是new的對象存放在堆中,一個是“b”字符串常量對象,存放在StringTable中。
下面我們再看一個例子:
public class HelloWorld { public static void main(String []args) { String str1 = "abc"; String str2 = new String("def"); String str3 = "abc"; String str4 = str2.intern(); String str5 = "def"; System.out.println(str1 == str3);//true System.out.println(str2 == str4);//false System.out.println(str4 == str5);//true } }
看到String str3 = "abc"; 解析str3時,在StringTable中尋找“abc”,會發現str1的值已經在stringTable中,所以str3的引用地址和str1相同,不會創建不同的對象,即str1==str3為true;
看到String str4 = str2.intern();我們可以知道,intern()函數返回StringTable中”def”的引用值。因為StringTable中已經有“def”引用值,即返回str2中new出來的“def”在StringTable中的引用值。
StringTable 的位置
jdk6(永久代實現)和jdk8(元空間實現)中方法區的區別,其中最主要的區別是將方法區轉移到本地內存中,且常量池分為運行時常量池和字符串常量池;且字符串常量池被留在內存中的堆中。
原因:
- StringTable中存在大量的字符串對象,運行時間增長永久代內存占用過多,且永久代只有在觸發FULL GC時才進行垃圾回收,回收頻率過慢。
- 轉移到堆中可以利用虛擬機在堆內存中頻繁的垃圾回收,處理StringTable中對象過多情況。
永久代和元空間內存溢出的區別:
- jdk1.6
- jdk1.8
jdk1.8和jdk1.6中intern()方法的運用
- 1.8 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有則將該字符串的引用放入串池
- 1.6 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有會把此對象復制一份,放入串池
總結
- 全局字符串池每個虛擬機只有一個,存儲字符串常量的引用值;
- class常量池是java程序編譯之后才有的,每個類都有,存放字面值和符號引用常量;
- 運行時常量池是在類加載完之后,常量池內容存儲在運行時常量池中,每個類都有一個,且常量池中符號引用轉換為直接引用,與全局字符串池中保持一致。
StringTable調優:
- 調整hash表中桶子個數,-XX:StringTableSize=桶個數
- 考慮字符串是否入池
四、直接內存
- 常見於NIO操作中,用於數據緩沖
- 分配回收成本高,但讀寫能力強
- 不受JVM內存回收管理
直接內存使用前后的對比:
使用前:
說明:
- 因為java無法操作本地文件,在java堆內存中划出java緩沖區;
- 從用戶態轉移到內核態,本地方法在系統內存中划出一段系統緩沖區,將磁盤文件分部分緩沖到系統緩沖區中,間接的將系統緩沖區中數據傳輸到java緩沖區中;
- 內核態轉到用戶態,調用輸出流寫入操作,將文件copy到另一個位置,循環copy,直到全部復制完成。
使用后:
說明:
- ByteBuffer.allocateDirect(_size),在系統內存中分配直接內存;
- 系統方法和java方法都可以訪問直接內存;
- 與不使用直接內存相比,減少了一次從系統緩存區向java緩沖區復制的操作,復制效率成倍上升。
直接內存的回收:
- 使用Unsafe對象實現直接內存的分配回收,回收主要使用的是freeMemory方法
- ByteBuffer類內部,使用了Cleaner(虛引用)來檢測ByteBuffer對象,一旦對象被回收,就會由ReferenceHandler線程通過Cleaner的clean對象調用freeMenory來釋放直接內存。
- -XX:+DisableExplicitGC 顯式的System.gc()顯式的垃圾回收 FULL GC,被禁用。
- 因為考慮到系統性能,FULL GC時間夠長,會嚴重影響性能。所以涉及到直接內存的使用,釋放內存使用Unsafe.freeMemory,不建議使用System.gc()。