天天看點

JVM常量池、Class常量池、運作時常量池

詳解JVM常量池、Class常量池、運作時常量池、字元串常量池(心血總結)

常量池,也叫 Class 常量池(常量池==Class常量池)。Java檔案被編譯成 Class檔案,Class檔案中除了包含類的版本、字段、方法、接口等描述資訊外,還有一項就是常量池,常量池是當Class檔案被Java虛拟機加載進來後存放在方法區 各種字面量 (Literal)和 符号引用 。

在Class檔案結構中,最頭的4個位元組用于 存儲魔數 (Magic Number),,用于确定一個檔案是否能被JVM接受,再接着4個位元組用于 存儲版本号,前2個位元組存儲次版本号,後2個存儲主版本号,再接着是用于存放常量的常量池常量池主要用于存放兩大類常量:字面量和符号引用量,字面量相當于Java語言層面常量的概念,如文本字元串,聲明為final的常量值等,符号引用則屬于編譯原理方面的概念。如下

JVM常量池、Class常量池、運作時常量池

2.運作時常量池

2.1運作時常量池的簡介

運作時常量池是方法區的一部分。運作時常量池是當Class檔案被加載到記憶體後,Java虛拟機會 将Class檔案常量池裡的内容轉移到運作時常量池裡(運作時常量池也是每個類都有一個)。運作時常量池相對于Class檔案常量池的另外一個重要特征是具備動态性,Java語言并不要求常量一定隻有編譯期才能産生,也就是并非預置入Class檔案中常量池的内容才能進入方法區運作時常量池,運作期間也可能将新的常量放入池中

2.2方法區的Class檔案資訊,Class常量池和運作時常量池的三者關系

JVM常量池、Class常量池、運作時常量池

字元串常量池

3.1字元串常量池的簡介

字元串常量池又稱為:字元串池,全局字元串池,英文也叫String Pool。 在工作中,String類是我們使用頻率非常高的一種對象類型。JVM為了提升性能和減少記憶體開銷,避免字元串的重複建立,其維護了一塊特殊的記憶體空間,這就是我們今天要讨論的核心:字元串常量池。字元串常量池由String類私有的維護。

我們理清幾個概念:

在JDK7之前字元串常量池是在永久代裡邊的,但是在JDK7之後,把字元串常量池分進了堆裡邊。看下面兩張圖:

JVM常量池、Class常量池、運作時常量池
JVM常量池、Class常量池、運作時常量池

我們知道,在Java中有兩種建立字元串對象的方式:

采用字面值的方式指派

采用new關鍵字建立一個字元串對象。這兩種方式在性能和記憶體占用方面存在着差别。

3.2采用字面值的方式建立字元串對象

package Oneday;
public class a {
    public static void main(String[] args) {
        String str1="aaa";
        String str2="aaa";
        System.out.println(str1==str2);   
    }
}
運作結果:
true

           

采用字面值的方式建立一個字元串時,JVM首先會去字元串池中查找是否存在"aaa"這個對象,如果不存在,則在字元串池中建立"aaa"這個對象,然後将池中"aaa"這個對象的引用位址傳回給字元串常量str,這樣str會指向池中"aaa"這個字元串對象;如果存在,則不建立任何對象,直接将池中"aaa"這個對象的位址傳回,賦給字元串常量。

對于上述的例子:這是因為,建立字元串對象str2時,字元串池中已經存在"aaa"這個對象,直接把對象"aaa"的引用位址傳回給str2,這樣str2指向了池中"aaa"這個對象,也就是說str1和str2指向了同一個對象,是以語句System.out.println(str1== str2)輸出:true

3.3采用new關鍵字建立一個字元串對象

package Oneday;
public class a {
    public static void main(String[] args) {
        String str1=new String("aaa");
        String str2=new String("aaa");
        System.out.println(str1==str2);
    }
}
運作結果:
false

           

采用new關鍵字建立一個字元串對象時,JVM首先在字元串常量池中查找有沒有"aaa"這個字元串對象,如果有,則不在池中再去建立"aaa"這個對象了,直接在堆中建立一個"aaa"字元串對象,然後将堆中的這個"aaa"對象的位址傳回賦給引用str1,這樣,str1就指向了堆中建立的這個"aaa"字元串對象;如果沒有,則首先在字元串常量池池中建立一個"aaa"字元串對象,然後再在堆中建立一個"aaa"字元串對象,然後将堆中這個"aaa"字元串對象的位址傳回賦給str1引用,這樣,str1指向了堆中建立的這個"aaa"字元串對象。

對于上述的例子:

因為,采用new關鍵字建立對象時,每次new出來的都是一個新的對象,也即是說引用str1和str2指向的是兩個不同的對象,是以語句

System.out.println(str1 == str2)輸出:false

字元串池的實作有一個前提條件:String對象是不可變的。因為這樣可以保證多個引用可以同時指向字元串池中的同一個對象。如果字元串是可變的,那麼一個引用操作改變了對象的值,對其他引用會有影響,這樣顯然是不合理的。

3.4字元串池的優缺點

字元串池的優點就是避免了相同内容的字元串的建立,節省了記憶體,省去了建立相同字元串的時間,同時提升了性能;另一方面,字元串池的缺點就是增加了JVM在常量池中周遊對象所需要的時間,不過其時間成本相比而言比較低。

4字元串常量池和運作時常量池之間的藕斷絲連

部落客為啥要把他倆放在一起講呢,主要是随着JDK的改朝換代,字元串常量池有很大的變動,和運作時常量池有關。而且網上衆說紛纭,我真的在看的時候ctm了,是以部落客花很長時間把這一塊講明白,如果有錯誤或者異議可以通知部落客。部落客一定會在第一時間參與讨論

4.1常量池和字元串常量池的版本變化

  • 在JDK1.7之前運作時常量池邏輯包含字元串常量池存放在方法區, 此時hotspot虛拟機對方法區的實作為永久代
  • 在JDK1.7 字元串常量池被從方法區拿到了堆中, 這裡沒有提到運作時常量池,也就是說 字元串常量池被單獨拿到堆,運作時常量池剩下的東西還在方法區, 也就是hotspot中的永久代
  • 在JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字元串常量池還在堆, 運作時常量池還在方法區, 隻不過方法區的實作從永久代變成了元空間(Metaspace)

4.2String.intern在JDK6和JDK7之後的差別(重點)

JDK6和JDK7中該方法的功能是一緻的,不同的是常量池位置的改變(JDK7将常量池放在了堆空間中),下面會具體說明。intern的方法傳回字元串對象的規範表示形式。其中它做的事情是:首先去判斷該字元串是否在常量池中存在,如果存在傳回常量池中的字元串,如果在字元串常量池中不存在,先在字元串常量池中添加該字元串,然後傳回引用位址
String s1 = new String("1");
s1.intern();
String s2 = "1";
System.out.println(s1 == s2);

運作結果:
JDK6運作結果:false
JDK7運作結果:false

           
方法區在邏輯上就是堆的一個組成部分,隻是各種jvm的實作不遵循而已

1、類加載過程

1.1、加載

查找和導入class檔案。

1.2、連結

驗證

檢驗載入的class檔案的正确性,完整性。

準備

給類的靜态變量配置設定存儲空間,會賦對象類型的預設值。

解析

将class常量池中的符号引用轉換成直接引用。

符号引用和直接引用的差別:

符号引用:java編譯階段不知道所引用的對象的實際位址,使用符号引用來代替

直接引用:能夠直接定位到對象的指針,或相對偏移量。能定位到一個對象的記憶體實際位址。

1.3、初始化

對類的靜态變量,代碼塊執行初始化操作,靜态變量指派順序根據代碼定義的順序執行。

2、類的加載順序

父類靜态成員變量

父類靜态代碼塊

子類靜态成員變量

子類靜态代碼塊

父類非靜态成員變量

父類非靜态代碼塊

父類構造方法

子類非靜态成員變量

子類非靜态代碼塊

子類構造方法

3、類加載時機

建立類執行個體-使用new關鍵字,反射,克隆,反序列化。

調用類的靜态變量或者靜态方法,或對靜态變量進行指派操作。

初始化子類時會先初始化父類。

虛拟機啟時,包含main方法的啟動類。

注意:

通過數組定義的引用類,不會造成類的初始化。

通路類的靜态常量是不會造成類加載的。因為在編譯時期,靜态常量已經放入類的常量池中了。通路類靜态常量其實是直接通路常量池中的常量,不需要加載類。

4、靜态常量是什麼時候指派的

靜态常量在編譯階段把初始值存入class檔案的常量池中,在類的準備階段,将值賦給靜态變量。

5、什麼是雙親委派

  1. 類加載器包括:BootstrapClassLoader、ExtensionClassLoader、 ApplicationClassLoader、自定義的類加載器。
  2. 雙親委派模型:如果一個類加載器收到了加載類的請求,首先交給父類加載器進行加載,如果父類加載器加載失敗,目前類才會自己加載類。
  3. 雙親委派的作用:避免重複加載,父類已經加載子類不用加載,防止使用者自定義加載器加載java核心的api,帶來安全隐患。
  4. 一個類是否被加載是通過全類名和命名空間确定的,命名空間是加載類的加載器名。

6、如何自定義類加載器

繼承classloader類,重寫findClass方法。

final修飾的引用類型:是在堆記憶體new出來的;(如對象)可以被指派一次,引用位址不可變,但對象裡面的内容(如屬性值)可以變。

static修飾的引用類型:是在加載類的時候,load到方法區的;是這個類的執行個體共有的類方法or屬性;引用的位址可以變,裡面具體的内容也可以變

static final修飾的引用類型:是在加載類的時候,load到方法區的(同static);可以被指派一次,引用位址不可變,但對象裡面的内容(如屬性值)可以變(同final);