1. 程式人生 > >Java 中級 學習筆記 1 JVM的理解以及新生代GC處理流程和常量池、執行時常量池、字串常量池的理解

Java 中級 學習筆記 1 JVM的理解以及新生代GC處理流程和常量池、執行時常量池、字串常量池的理解

寫在最前

從畢業到現在已經過去了差不多一年的時間,工作還算順利,但總是離不開CRUD ,我覺得這樣下去肯定是不行的,溫水煮青蛙,勢必有一天,會昏昏沉沉的迷失在溫水裡。所以,需要將之前學習JAVA 當中一些中高階部分的知識需要進行學習和記錄,並將其整理部落格,一起成長,一起努力。

JVM

JAVA虛擬機器在執行的時候,會給所有的變數、以及例項物件等分配記憶體區域,當然這一塊記憶體區域是在Java 虛擬機器上分配的,虛擬機器的記憶體。

這裡先來了解一個區別,就是JVM 與 Hotspot

JVM 可以理解為是一種標準,就好像我們JAVA 裡面定義的介面一樣,它是一個籠統的概念。

而Hotspot則具體是JVM 的一種具體實現,它由SUN 公司開發,並且在Open-jdk 與 sun-jdk 當中包含的,都是Hotspot 虛擬機器。

特點:JIT 即時編譯、熱點探測等

  參考:https://www.cnblogs.com/baxianhua/p/9528192.html

 

先從JVM記憶體分佈區分為:執行緒共享區以及執行緒私有區

執行緒記憶體共享區

  • 堆 Heap
  • 方法區/永久帶

執行緒的記憶體共享區域包含堆和方法區,方法區也可以叫做是永久帶,其實是HotSpot VM 將堆裡面一塊關於永久代的記憶體區域用來實現方法區,為什麼要這樣做呢?其實虛擬機器的垃圾回收機制希望也控制這一塊記憶體的裝載與解除安裝,但是手頭有不想單獨給他獨立劃分一個出去,那就從堆裡面拿出用來放永久代的記憶體區域用來實現方法區即可。

 

堆 heap (執行緒共享)

堆作為JVM 虛擬機器最大的共享記憶體區域,用來存放例項物件以及陣列等,也是垃圾收集器GC 進行的重要區域。這裡一會兒會涉及到一個關於分代回收的垃圾處理演算法。

首先來了解一小部分,比如我們平時使用new 關鍵字進行物件的例項化的時候,就會在堆裡面開闢一塊記憶體,用來存放我們例項後的物件。

 

當代JVM 大都採用分代回收的演算法,按照GC的角度,又可以將堆細分為新生代和老年代

新生代佔據堆的1/3,而老年代佔據堆記憶體的2/3

 

新生代的劃分 Eden/FromSurvivor/To Survivor

新生代Eden區

這裡是每一個物件出生的地方,每當新分配一個物件的時候,若出現Eden區域記憶體不足,則會觸發MinorGC

負責清理新生代的記憶體。

Minor GC

從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC。這一定義既清晰又易於理解。但是,當發生Minor GC事件的時候,有一些有趣的地方需要注意到:

  1. 當 JVM 無法為一個新的物件分配空間時會觸發 Minor GC,比如當 Eden 區滿了。所以分配率越高,越頻繁執行 Minor GC。
  2. 記憶體池被填滿的時候,其中的內容全部會被複制,指標會從0開始跟蹤空閒記憶體。Eden 和 Survivor 區進行了標記和複製操作,取代了經典的標記、掃描、壓縮、清理操作。所以 Eden 和 Survivor 區不存在記憶體碎片。寫指標總是停留在所使用記憶體池的頂部。
  3. 執行 Minor GC 操作時,不會影響到永久代。從永久代到年輕代的引用被當成 GC roots,從年輕代到永久代的引用在標記階段被直接忽略掉。
  4. 質疑常規的認知,所有的 Minor GC 都會觸發“全世界的暫停(stop-the-world)”,停止應用程式的執行緒。對於大部分應用程式,停頓導致的延遲都是可以忽略不計的。其中的真相就 是,大部分 Eden 區中的物件都能被認為是垃圾,永遠也不會被複制到 Survivor 區或者老年代空間。如果正好相反,Eden 區大部分新生物件不符合 GC 條件,Minor GC 執行時暫停的時間將會長很多。

所以 Minor GC 的情況就相當清楚了——每次 Minor GC 會清理年輕代的記憶體。

https://www.cnblogs.com/williamjie/p/9516264.html

FromSurvivor

上一次GC 清理過後的倖存者,分配到此塊區域

To Survivor

在Minor GC 清理過程中的倖存者,移到該區域。

 

新生代Minor GC 回收過程

回收演算法:複製演算法

回收過程:複製-》清空-》互換

  1. 首先將Eden 區和From 區域存活的物件進行復制到To區域
  2. 將原有的Eden區和From 區域進行清空
  3. 最後將From 域To 區域進行一個互換,這時候To區域將成為下一次GC過程當中的From 區域

老年代區域

從上面學習的內容瞭解,老年代的物件大多都來自於物件年齡的+1導致物件年齡超過新生代,從而防止到老年代的位置,也有一部分來自於

新生代,新生代在初始化例項的時候,若遇到記憶體不足,可直接將物件記憶體分配到老年代。

老年代的物件基本上都很穩定,因此,在老年代工作的GC : Major GC 

Major GC 不會頻繁的進行記憶體清理。在Major GC 進行前,至少有一次Minor GC 進行了新生代的清理工作,導致物件年齡增加而在老年代有了其物件。

清理演算法:標記清除法

MajorGC 採用標記清除演算法:首先掃描一次所有老年代,標記出存活的物件,然後回收沒
有標記的物件。 MajorGC 的耗時比較長,因為要掃描再回收。 MajorGC 會產生記憶體碎片,為了減
少記憶體損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的
時候,就會丟擲 OOM(Out of Memory)異常。 

方法區、永久代(執行緒共享)

方法區用來存放一些常量、靜態變數、以及JAVA 虛擬機器載入類以後類的一些資訊都是存放在方法區的。

方法區還存在一個叫做執行時常量池。

GC 不會在主程式執行期對永久區域進行清理。所以這
也導致了永久代的區域會隨著載入的 Class 的增多而脹滿,最終丟擲 OOM 異常。

 

執行時常量池

常量池作為方法區的一部分,在Class 檔案被java虛擬機器載入的時候,一個類物件包含的欄位、方法、還有一項就是常量池,常量池用於存放編譯時期產生的各種字面量和符號引用。

class 檔案被載入後,所產生的內容就會被存放到方法區的常量池。

執行緒記憶體私有區

執行緒私有區域的記憶體生命週期與執行緒的生命週期是一致的。依賴使用者執行緒啟動則分配記憶體,執行緒執行結束則回收記憶體。

在Hotspot VM 當中,每個使用者的執行緒與作業系統的執行緒直接對映,這一部分的記憶體跟隨本地執行緒的存活

 

虛擬機器棧(執行緒私有)

虛擬機器棧是描述JAVA方法在執行時候的記憶體模型,而每個執行緒在虛擬機器棧裡面都有屬於自己的棧幀,棧幀隨著方法執行被建立,

建立入棧,執行完畢方法後出棧,棧幀被銷燬。

一個棧幀包含有:方法執行過程產生的區域性變量表,以及運算元棧,動態連結,方法出口等。

隨著方法建立而建立入棧,隨著方法執行完畢則銷燬。

本地方法棧(執行緒私有)

本地方法棧主要為本地Native方法服務,而與之類似的虛擬機器棧則是為Java 方法提供服務

程式計數器(執行緒私有)

程式計數器是一塊較小的記憶體空間,

每個執行緒都有自己的程式計數器,計數器可以理解為是一個儲存每個執行緒在執行過程中記錄當前執行的行號以及

Java 方法在執行過程中虛擬機器位元組碼指令的地址。

Java 8 與元空間

現在基本上所有的開發都一般以JDK 1.8開始,我們需要了解一下JAVA8 當中的元空間。

其實元空間的理解與在JVM當中的方法區/永久代類似。永久代在JAVA8當中被移除

不同在於:元空間不在虛擬機器內,而在使用者機器的記憶體上開闢。

JVM在載入類的時候,將類的元資料(欄位、名稱、型別、長度)等放入本地記憶體。

而將字串常量以及類的靜態變數等資訊放入JVM 堆中形成字串常量池。

好處:不會再因為永久代從不會被GC進行清理導致的OOM錯誤等。

 

容易混淆的地方:

常量池、執行時常量池、字串常量池

在Class檔案中除了有類的版本【高版本可以載入低版本】、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table)【此時沒有載入進記憶體,也就是在檔案中】,用於存放編譯期生成的各種字面量和符號引用。

下面對字面量和符號引用進行說明
字面量
字面量類似與我們平常說的常量,主要包括:

  1. 文字字串:就是我們在程式碼中能夠看到的字串,例如String a = “aa”。其中”aa”就是字面量。
  2. 被final修飾的變數。

符號引用
主要包括以下常量:

  1. 類和介面和全限定名:例如對於String這個類,它的全限定名就是java/lang/String。
  2. 欄位的名稱和描述符:所謂欄位就是類或者介面中宣告的變數,包括類級別變數(static)和例項級的變數。
  3. 方法的名稱和描述符。所謂描述符就相當於方法的引數型別+返回值型別。

2.2 執行時常量池

我們知道類載入器會載入對應的Class檔案,而上面的class檔案中的常量池,會在類載入後進入方法區中的執行時常量池【此時存在在記憶體中】。並且需要的注意的是,執行時常量池是全域性共享的,多個類共用一個執行時常量池。並且class檔案中常量池多個相同的字串在執行時常量池只會存在一份。
注意執行時常量池存在於方法區中。

2.3 字串常量池

  看名字我們就可以知道字串常量池會用來存放字串,也就是說常量池中的文字字串會在類載入時進入字串常量池。
那字串常量池和執行時常量池是什麼關係呢?上面我們說常量池中的字面量會在類載入後進入執行時常量池,其中字面量中有包括文字字串,顯然從這段文字我們可以知道字串常量池存在於執行時常量池中。也就存在於方法區中。
不過在周志明那本深入java虛擬機器中有說到,到了JDK1.7時,字串常量池就被移出了方法區,轉移到了堆裡了。
那麼我們可以推斷,到了JDK1.7以及之後的版本中,執行時常量池並沒有包含字串常量池,執行時常量池存在於方法區中,而字串常量池存在於堆中。

引用:https://www.cnblogs.com/gxyandwmm/p/9495923.html

 

學以致用

通過上面的瞭解。已經大致瞭解到常量池的一些相關內容了。最後再提一下。以及新手很難立即的String 這個引用型別的一些操作中涉及到的內容

String intern() 

Intern() 方法算是很常見但卻很容易忽略的一個關鍵方法,在JDK的文件中,它是這樣定義的。

若池裡面存在與之內容相同的字串,則返回常量池那個物件的引用,若不存在,則建立一個,並返回此物件在池裡面的引用地址。

 1 String a = new String("ab");
 2 String b = new String("ab");
 3 String c = "ab";
 4 String d = "a" + "b";
 5 String e = "b";
 6 String f = "a" + e;
 7 
 8 System.out.println(b.intern() == a);//false
 9 System.out.println(b.intern() == c);//true
10 System.out.println(b.intern() == d);//true
11 System.out.println(b.intern() == f);//false
12 System.out.println(b.intern() == a.intern());//true

就按照這個例子,和大家簡單的聊一聊。

1、2行分別用new 關鍵字建立了物件,此時的物件存在於堆中

3行直接用雙引號宣告的物件“ab” 則首先會新增到常量池裡面。String 型別的c變數指向位於字串常量池的"ab";

4行通過+號將直接宣告的"a"和“b”進行了一個拼接,這裡需要著重說明一下:

JAVA 在編譯的時候就會把類似“aaa”+"bbb"的程式碼直接優化成:“aaabbb”

所以4行這裡進行了所謂的拼接,其實編譯後還是“ab”,當然,第三行執行完後,常量池已存在“ab” 那麼String 型別的變數d 指向常量池“ab”

5、6行通過先定義一個“b”存放到字串常量池後,通過拼接變數的方式,其實這個物件最後是建立在了堆裡面而不會進入常量池。

 

答案解析

8行通過b變數執行intern() 方法後,去常量池找,找到返回的其實是c的記憶體地址。則肯定和a(new 出來的)記憶體地址不相等。false

9行就不用說了,c與c比較,肯定true

10行 d其實指向的本來就是c的記憶體地址。true

11我們知道一個字串常量+一個字串變數得到的一個新物件其實是在堆裡面出現的,肯定不會相同。false

12最後一個想必不用多說,兩個都拿出的是c的地址。true

 

小結

通過今天的學習,掌握JVM 當中記憶體的分佈關係以及堆這個最重要的記憶體共享區域內的物件迭代過程,以及從Class 檔案被編譯,編譯後形成常量池,再到Class檔案被

JVM載入到記憶體後,將物件的資訊存入方法區,而方法區域存在的執行時常量池。也就為了存放物件的字面量、以及符號引用

再到JDK8以後,將執行時常量池就放到堆裡面了。

 

參考:

常量池相關內容: https://www.cnblogs.com/gxyandwmm/p/9495923.html

字串的拼接:https://www.cnblogs.com/nianzhilian/p/8810966.html

intern() https://www.runoob.com/java/java-string-intern.h