jvm入門及理解(四)——運行時數據區(堆+方法區)


一、堆

定義: 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 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有會把此對象復制一份,放入串池

 

總結

  1. 全局字符串池每個虛擬機只有一個,存儲字符串常量的引用值;
  2. class常量池是java程序編譯之后才有的,每個類都有,存放字面值和符號引用常量;
  3. 運行時常量池是在類加載完之后,常量池內容存儲在運行時常量池中,每個類都有一個,且常量池中符號引用轉換為直接引用,與全局字符串池中保持一致。

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()。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM