1.運行時常量池:方法區的一部分,存放編譯器生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池。通常來講,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲到運行時常量池中。運行時常量池具有動態性,也就是並不是預置入Class文件的內容才能進入方法區的運行時常量池,運行期間也可能將新的常量放入池中。java
jvm在執行某個類的時候,必須通過加載、鏈接、初始化,而鏈接又包括驗證、準備、解析三個階段。app
而當類加載到內存中後,jvm就會將靜態常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每一個類都有一個。dom
靜態常量池中存的是字面量和符號引用,也就是說它們存的並非對象的實例,而是對象的符號引用值。而通過解析(resolve)以後,也就是把符號引用替換爲直接引用,解析的過程會去查詢字符串常量池,也就是咱們上面所說的StringTable,以保證運行時常量池所引用的字符串與字符串常量池中所引用的是一致的。jvm
咱們看一個例子佈局
import java.util.UUID;ui
public class Test {翻譯
public static void main(String[] args) {code
System.out.println(TestValue.str);對象
}接口
}
class TestValue{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("TestValue static code");
}
}
結果:
從聲明自己str都是常量,關鍵的是這個常量的值可否在編譯時期肯定下來,顯然這裏的例子在編譯期的時候顯然是肯定不下來的。須要在運行期才能可以肯定下來,這要求目標類要進行初始化
當常量的值並不是編譯期間能夠肯定的,那麼其值不會被放到調用類的常量池中
這時在程序運行時,會致使主動使用這個常量所在的類,顯然會致使這個類被初始化。
(這個涉及到類的加載機制,後面會寫這裏作個標記)
反編譯探究一下:
Compiled from "Test.java"
class com.leetcodePractise.tstudy.TestValue {
public static final java.lang.String str;
com.leetcodePractise.tstudy.TestValue();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static {};
Code:
0: invokestatic #2 // Method java/util/UUID.randomUUID:()Ljava/util/UUID;
3: invokevirtual #3 // Method java/util/UUID.toString:()Ljava/lang/String;
6: putstatic #4 // Field str:Ljava/lang/String;
9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
12: ldc #6 // String TestValue static code
14: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: return
}
很明顯TestValue類會初始化出來
常量介紹完以後 這裏記錄一下反編譯及助記符的筆記
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
System.out.println(Father.s);
}
}
class Father{
public static final String str = "Hello,world";
public static final short s = 6;
static {
System.out.println("Father static block");
}
}
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: bipush 6
13: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
16: return
}
bipush 表示將單字節(-128-127)的常量值推送至棧頂
再加入
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
System.out.println(Father.s);
System.out.println(Father.t);
}
}
class Father{
public static final String str = "Hello,world";
public static final short s = 6;
public static final int t = 128;
static {
System.out.println("Father static block");
}
}
進行反編譯
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: bipush 6
13: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
19: sipush 128
22: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
25: return
}
sipush表示將一個短整型常量值(-32768~32767)推送至棧頂
再進行更改
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
System.out.println(Father.t);
}
}
class Father{
public static final String str = "Hello,world";
public static final int t = 1;
static {
System.out.println("Father static block");
}
}
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: bipush 6
13: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
19: sipush 128
22: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
25: return
}
D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class
Compiled from "Main.java"
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iconst_1
12: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
15: return
}
這裏變成了 iconst_1
2.字符串常量池:本質是一個HashSet<String>,這是一個純運行時的結構,並且是惰性維護的。注意它只存儲String對象的引用,而不存儲String對象的內容,根據這個引用能夠獲得具體的String對象。
3.Class常量池:主要存放兩大類常量:字面量和符號引用。加載Class文件時,Class文件中String對象會進入字符串常量池(這裏的進入是指 放入字符串的引用,字符串自己仍是在堆中),別的大都會進入運行時常量池。
字面量比較接近Java語言層面常量的概念,如文本字符串、聲明爲final的常量值
符號引用屬於編譯原理的概念:
類和接口的全定限名
字段的名稱和描述符
方法的名稱和描述符
符號引用將在解析階段被替換爲直接引用。由於Java代碼在進行編譯時,並不像C那樣有"鏈接"這一步驟,而是在虛擬機加載Class文件的時候進行動態鏈接。也就是說,Class文件不會保存外匯返傭http://www.kaifx.cn/各個方法、字段的最終內存佈局信息,所以這些字段、方法的符號引用不通過運行期間 轉換的話沒法獲得真正的內存入口地址,也就沒法直接被虛擬機使用。當虛擬機運行時,須要從常量池得到對應的符號引用,再在類建立時或運行時解析、翻譯到具體的內存地址之中。
而運行時常量池,則是jvm虛擬機在完成類裝載操做後,將class文件中的常量池載入到內存中,並保存在方法區中,咱們常說的常量池,就是指方法區中的運行時常量池。
運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,Java語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。
String的intern()方法會查找在常量池中是否存在一份equal相等的字符串,若是有則返回該字符串的引用,若是沒有則添加本身的字符串進入常量池。
那這樣來看,經過靜態常量池,即*.class文件中的常量池 更可以探究常量的含義了
下面看一段代碼
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
}
}
class Father{
public static String str = "Hello,world";
static {
System.out.println("Father static block");
}
}
輸出結果爲
再看另外一個:
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
}
}
class Father{
public static final String str = "Hello,world";
static {
System.out.println("Father static block");
}
}
結果:
只有一個
是否是發現很吃驚啊
咱們對第二個演示的代碼塊進行反編譯一下
D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class
Compiled from "Main.java"
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
這裏有一個Main()是構造方法 下面的是main方法
0: getstatic # 2 對應的是System.out
3: ldc #4 對應的值 直接是 Hello,world 了 肯定的值 沒有從Father類中取出
ldc表示將int,float或是String類型的常量值從常量池中推送至棧頂
居然沒有!!! 即便刪除Father.class文件 這段代碼照樣能夠運行 它和Father類 沒有半毛錢的關係了
實際上,在編譯階段 常量就會被存入到調用這個常量的方法所在的類的常量池當中
從這個例子中 能夠看出 這裏的str 是一個常量 調用這個常量的方法是main方法 main方法所在的類是Main ,也就是說編譯以後str被放在了該類的常量池中
本質上,調用類並無直接引用到定義常量的類,所以並不會觸發定義常量的類的初始化
4.String的intern方法:
JDK7中,若是字符串常量池中已經有了這個字符串,那麼直接返回常量池中的它的引用,若是沒有,那就將它的引用保存一份到字符串常量池,而後直接返回這個引用。
5.字面量進入字符串常量池的時機:
就HotSpot VM的實現來講,加載類的時候,那些字符串字面 量會進入當前類的運行時常量池,不會進入全局字符串常量池(即在字符串常量池中沒有相應的引用,在堆中也沒有生成對應的對象)。加載類的時,沒有解析字符串字面量,等到執行ldc指令的時候就會觸發這個解析的動做。ldc指令的語義是:到當前類的運行時常量池區查找該index對應的項,若是該項沒有解析就解析,並返回解析後的內容。在遇到String類型常量時,解析的過程是若是發現字符串常量池中已經有了內容匹配的String類型的引用,就直接返回這個引用,若是沒有內容匹配的String實例的引用,就會在Java堆中建立一個對應內容的String對象,而後在字符串常量池中記錄下這個引用。
說明:本身的一點理解,上面說的時對字符串的解析,其實對方法解析也是相似,有些方法也是lazy resolve,有一部分符號引用是在類加載階段或者第一次使用的時候就轉化爲直接引用,被稱爲靜態解析(例如靜態方法、私有方法等非虛方法),另外一部分將在每一次運行期間轉換爲直接引用,被稱爲動態鏈接(例如靜態分派),這部分也是lazy resolve。
6.例題分析:
例1:
class Test{
public static String s1 = "static";
public static void main(String[] args) {
String s2 = new String("he")+new String("llo");
s2.intern();
String s3 = "hello";
System.out.println(s2==s3); //true
}
}
"static" "he" "llo" "hello"都會進入Class常量池,類加載階段因爲解析階段時lazy的,因此不會建立實例,更不會駐留字符串常量池。但要注意這個「static"和其餘三個不同,它是靜態的,在加載階段的初始化階段,會爲靜態遍歷執行初始值,也就是將"static"賦值給s1,因此會建立"static"字符串對象, 而且會保存一個指向它的引用到字符串常量池。
運行main方法後,執行String s2 = new String("he")+new String("llo")語句,建立"he"和"llo"的對象,並會保存引用到字符串常量池中,而後內部建立一個StringBuilder對象,一路append,最後調用toString()方法獲得一個String對象(內容時hello,注意這個toString方法會new一個String對象),並把它賦值給s2(注意這裏沒有把hello的引用放入字符串常量池)。
而後執行語句:s1.intern(),此時字符串常量池中沒有,它會將上面的這個hello對象的引用保存到字符串常量池,而後返回這個引用,可是這個返回的引用沒有變量區接收,因此沒用。
而後執行:String s3 = "hello"由於字符串常量池中已經有了,因此直接指向堆中"hello"對象
而後執行:System.out.println(s2==s3),此時返回true。
例題2:
class JianZhiOffer{
public static void main(String[] args) {
String s1 = new String("he")+new String("llo"); //第一句
String s2 = new String("h")+new String("ello"); //第二句
String s3 = s1.intern(); //第三句
String s4 = s2.intern(); //第四句
System.out.println(s1==s3); //第五句
System.out.println(s1==s4); //第六句
}
}
類加載階段,什麼都沒幹。
第一句:建立"he"和"llo"對象,並放入字符串常量池,而後建立"hello"對象,沒有放入字符串常量池,s2指向這個"hello"對象。
第二句:建立了"h"和"ello"對象,並放入字符串常量池,而後建立"hello"對象,沒有放入字符串常量池,s3指向這個"hello"對象。
第三句:字符串常量池中沒有"hello",因此會把s1指向String對象的引用放入字符串常量池,而後將這個引用返回給了s3,因此s1==s3是true
第四句:字符串常量池中有了"hello",因此將s4指向的s3指向的對象"hello",因此第六句s4==s1是true。