學習筆記 2021.10.22
2021.10.22
JVM
常量池具體存在的位置在哪裡?
是怎麼通過常量池來減少記憶體消耗的?
字元引用在記憶體上又怎麼體現?
執行時資料區
堆
YGC OGC FGC的一些簡單理解
FGC是包括方法區在內的區域
上面注意到老年回收單獨收集是一個很少見的行為,知道有這麼個東西即可。
這裡簡單貼一張各個區轉移的示意圖
注意點:YGC觸發的條件是伊甸區滿的時候觸發,而s區滿的時候不會觸發垃圾回收。這也是觸發頻率最高的垃圾回收。
至於FGC會在後面垃圾回收時具體再講。
關於堆分代的理解
其實不分代也是完全可以的,但是分代了是對具體物件具體處理的策略,更多的是為了優化垃圾回收的效能。
記憶體分配策略(物件提升原則)
- 長期存活的物件可以理解為即age超過閾值的物件。
TLAB
堆空間中不一定都是共享的!
因此引入了TLAB這個概念:
- JVM確實是把TLAB設定為首選。
- 一般TLAB所佔空間為伊甸區的百分之一
- 在TLAB分配失敗的話,JVM會通過加鎖機制讓物件在伊甸區中分配記憶體。
基本的使用過程:
當然上圖只是一個簡圖,也有直接到老年區的情況。
堆空間的常用引數設定
- 伊甸區相對s區大的話,會導致FGC的存在意義就沒有了。
- 相對的當比例小的話,則會導致垃圾回收的頻率變大了。
- 空間分配擔保相關:
簡單理解即是看情況要不要使用FGC。
一些小拓展:
目的就是為了看能否分配物件時不在堆上面進行。在棧上進行的話,就只有入棧出棧的操作,而不存在GC之類的,就優化了很多。
具體是否發生逃逸的判斷:
如何快速的判斷是否發生了逃逸,即看new的物件實體是否在外部被使用了。
像第二種本來就是使用外面的而不是自己的,自然也是發生了逃逸。
結論:能使用區域性變數的,就不要在方法外定義了。
程式碼優化的小tips
棧上分配
同步省略
具體的例子示意:
標量替換:
此時看到由建立物件直接變成了基本資料型別的使用,就可以直接分配在棧中的區域性變量表裡面了。
總結
方法區
棧、堆、方法區的互動關係
區域性變量表中會儲存具體數值麼?
簡單理解就是,在虛擬機器棧的區域性變量表中,儲存著具體物件的名稱,如果不是基本資料型別,該欄的值是指向堆中例項化後的物件的引用。到了堆中,則是一個個具體的物件,然後至於物件中的具體資訊,則又指向了方法區中的內容。方法區中就是.class檔案本身。
突然有感:
具體互動關係以及我想這麼實現的意義。
首先第一點是類定義好後,被載入後即會被放到方法區中,包含具體的函式以及一些變數?
型別宣告的過程,只是在棧空間給了塊slot放他的引用,在沒有具體建立物件之前,這塊引用是沒有指向的位置的。所以此時對於棧中的其他基本資料型別,他們放的就是實際值,而棧只會放堆中物件的地址(建立好過後)。
然後才是物件建立,即new。這時引用會指向堆中的具體物件,而物件自己也會指向方法區中的類獲取相關的其他東西,我認為在物件建立這,也有物件自己的資料放在了這裡。
這麼設計物件建立的過程,首先在區域性變量表層面,不至於佔用太多空間,因為這裡是操作很頻繁的區域。在堆層面,不必儲存太多資訊,因為基本都有一個模板,所以又才會指向方法區中的模板。
剩下的問題:
具體物件中的屬性,是在方法區中還是堆中?public static這些定義體現在JVM中是怎麼個情況?
方法區的理解
並且實際上設定堆的大小的時候也不會影響到方法區的大小。
即前面有提到的metaspace(元空間)就是指代的方法區。在hotspot的發展過程中,經歷了從永久代和元空間的變化。
對這麼一個演變過程有個印象就可以了。
方法區大小的設定
這裡只記錄jdk8之後的吧:
初步瞭解解決OOM的思路和方法:
這裡具體在後面調優的學習中再去解決。
方法區的內部結構
大概示意圖:
方法區中載入的資訊包括:型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等。
型別資訊:
域資訊(也屬於型別資訊):
方法資訊(也屬於型別資訊):
可以看到,這裡對方法的記錄也包括其運算元棧和區域性變量表之類的。
這裡也要注意,class檔案被載入放入方法區後,對應的類載入器的資訊也放在了方法區裡面。
一些關鍵字的相關注意:
這裡即是指用static修飾的變數。可以不用例項化物件就可以直接使用。
即可以看到final修飾的,在編譯階段就已經賦值了。
而只用static修飾的,則會現在前面所說的preapare先初始化,再在後面的階段再賦值。
執行時常量池
常量池表:包含各種字面量和對型別、域和方法的符號引用。
常量池存在的意義:
這時,為了避免載入那麼多的資訊,不去直接載入這些類,而是儲存他們的符號,在具體編譯執行的時候實現引用從而達到目的。
執行時常量池:
方法區的具體執行過程:
這裡就貼一張圖,說下理解。
- 首先位元組碼指令中有很多去獲取常量池中類的資訊的字元引用。這裡就要注意的是在編譯執行的過過程中,會把字元引用轉換為真正的地址索引,去看該類是否存在或已經被載入之類的。
- 呼叫方法即是在棧中建立新棧幀的過程,此時對應的符號引用也是指向的方法,然後再去找方法的具體位置從而執行。
方法區的演進細節:
特別注意就是下面JDK8中的元空間是屬於本地記憶體的。
永久代被元空間替換的原因分析:
為什麼對字串常量池進行位置得到調整:
反正就是用的多,希望垃圾回收更頻繁一點。
靜態變數位置相關
這裡反而想說一點,即是類的成員變數,就可以理解成前面有提到過的類的自己的資訊,即成員變數是引用變數,在堆中也是以引用指向來儲存的。這個就是直接存在於堆裡面的,而方法中的區域性變數,則自然就是在棧空間中的。
至於後面的static定義的變數,就硬記為在方法區中即可。
方法區的垃圾回收
常量的判斷相對還好理解,主要是判斷某個類是否需要回收:
總結
執行時資料區就是記憶體!! 所有涉及到記憶體的都以執行時資料區為指代來考慮。
物件例項化的佈局
將物件建立的過程從記憶體層面上的分析,不論面試,至少也有助於對程式的理解。
常見的建立物件的方式:
物件建立的步驟
從位元組碼的角度看待:
- new這一步即是建立物件的過程,包括檢視class是否被載入,開闢記憶體空間等等。
- invoke這部則是呼叫了構造器方法來初始化,上面是零值初始化,這裡是顯示初始化。
從執行步驟上來分析
- 第一步判斷的具體方法即在於去看元空間的常量池中是否有該類的符號引用。有就好,沒有的話則呼叫對應的類載入器去載入,沒有的話就丟擲異常
- 指標碰撞,其實就是按著指標的順序放置物件即可。
- 不規整時維護的列表其實就可以理解為連結串列形式,在邏輯上有序,物理上無序那種。
- 而記憶體是否規整則由垃圾收集器去決定。
- 第四部即是屬性的預設初始化,下附幾個初始化的順序。
- 物件頭包括一些必要資訊,後面再說。
- 最後一步即為顯示初始化及其後面的,反正就是涉及到自己操作進行的初始化。上面的2 3 4都會在init方法中進行實現。
- 普遍還是認為init過後才算物件建立完成。
物件的記憶體佈局
- 雜湊值用來從棧到堆、GC分代年齡即前面所說的age,其他的有了解到再說。
- 型別指標指向了方法區中載入的class。
- 例項資料即是建立具體物件時自己定義的資料,注意如果物件中也有物件,同樣是指向方法區中的類。
小結的一個圖:
把這三部分一起來看再結合前面所說,也就很好理解了。
物件訪問定位
兩種主要方式的圖示:
控制代碼池
直接指標
直接指標就偏向於常理解的形式。也就是hotspot採用的方式。
直接記憶體
有點相關NIO的常識
常見的讀取記憶體的資料的過程
而直接記憶體讀取的情況
即這就是對上面最後一點的闡述。
關於直接記憶體的其他一些注意點:
- 元空間也只是屬於直接記憶體的一部分。
考慮到堆空間站執行時資料區的大頭,因此這裡是個大約的計算。