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.html


免責聲明!

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



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