這篇文章解釋了Java 虛擬機(JVM)的內部架構。下圖顯示了遵照 Java SE 7 規範的典型的 JVM 核心內部組件。 html
上圖顯示的組件分兩個章節解釋。第一章討論針對每一個線程建立的組件,第二章節討論了線程無關組件。 java
這裏所說的線程指程序執行過程當中的一個線程實體。JVM 容許一個應用併發執行多個線程。Hotspot JVM 中的 Java 線程與原生操做系統線程有直接的映射關係。當線程本地存儲、緩衝區分配、同步對象、棧、程序計數器等準備好之後,就會建立一個操做系統原生線程。Java 線程結束,原生線程隨之被回收。操做系統負責調度全部線程,並把它們分配到任何可用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。run() 返回時,被處理未捕獲異常,原生線程將確認因爲它的結束是否要終止 JVM 進程(好比這個線程是最後一個非守護線程)。當線程結束時,會釋放原生線程和 Java 線程的全部資源。 web
若是使用 jconsole 或者其它調試器,你會看到不少線程在後臺運行。這些後臺線程與觸發 public static void main(String[]) 函數的主線程以及主線程建立的其餘線程一塊兒運行。Hotspot JVM 後臺運行的系統線程主要有下面幾個: bootstrap
虛擬機線程(VM thread) | 這個線程等待 JVM 到達安全點操做出現。這些操做必需要在獨立的線程裏執行,由於當堆修改沒法進行時,線程都須要 JVM 位於安全點。這些操做的類型有:stop-the-world 垃圾回收、線程棧 dump、線程暫停、線程偏向鎖(biased locking)解除。 |
週期性任務線程 | 這線程負責定時器事件(也就是中斷),用來調度週期性操做的執行。 |
GC 線程 | 這些線程支持 JVM 中不一樣的垃圾回收活動。 |
編譯器線程 | 這些線程在運行時將字節碼動態編譯成本地平臺相關的機器碼。 |
信號分發線程 | 這個線程接收發送到 JVM 的信號並調用適當的 JVM 方法處理。 |
每一個運行的線程都包含下面這些組件: 數組
PC 指當前指令(或操做碼)的地址,本地指令除外。若是當前方法是 native 方法,那麼PC 的值爲 undefined。全部的 CPU 都有一個 PC,典型狀態下,每執行一條指令 PC 都會自增,所以 PC 存儲了指向下一條要被執行的指令地址。JVM 用 PC 來跟蹤指令執行的位置,PC 將其實是指向方法區(Method Area)的一個內存地址。 緩存
每一個線程擁有本身的棧,棧包含每一個方法執行的棧幀。棧是一個後進先出(LIFO)的數據結構,所以當前執行的方法在棧的頂部。每次方法調用時,一個新的棧幀建立並壓棧到棧頂。當方法正常返回或拋出未捕獲的異常時,棧幀就會出棧。除了棧幀的壓棧和出棧,棧不能被直接操做。因此能夠在堆上分配棧幀,而且不須要連續內存。 安全
並不是全部的 JVM 實現都支持本地(native)方法,那些提供支持的 JVM 通常都會爲每一個線程建立本地方法棧。若是 JVM 用 C-linkage 模型實現 JNI(Java Native Invocation),那麼本地棧就是一個 C 的棧。在這種狀況下,本地方法棧的參數順序、返回值和典型的 C 程序相同。本地方法通常來講能夠(依賴 JVM 的實現)反過來調用 JVM 中的 Java 方法。這種 native 方法調用 Java 會發生在棧(通常是 Java 棧)上;線程將離開本地方法棧,並在 Java 棧上開闢一個新的棧幀。 服務器
棧能夠是動態分配也能夠固定大小。若是線程請求一個超過容許範圍的空間,就會拋出一個StackOverflowError。若是線程須要一個新的棧幀,可是沒有足夠的內存能夠分配,就會拋出一個 OutOfMemoryError。 數據結構
每次方法調用都會新建一個新的棧幀並把它壓棧到棧頂。當方法正常返回或者調用過程當中拋出未捕獲的異常時,棧幀將出棧。更多關於異常處理的細節,能夠參考下面的異常信息表章節。 多線程
每一個棧幀包含:
局部變量數組包含了方法執行過程當中的全部變量,包括 this 引用、全部方法參數、其餘局部變量。對於類方法(也就是靜態方法),方法參數從下標 0 開始,對於對象方法,位置0保留爲 this。
有下面這些局部變量:
除了 long 和 double 類型之外,全部的變量類型都佔用局部變量數組的一個位置。long 和 double 須要佔用局部變量數組兩個連續的位置,由於它們是 64 位雙精度,其它類型都是 32 位單精度。
操做數棧在執行字節碼指令過程當中被用到,這種方式相似於原生 CPU 寄存器。大部分 JVM 字節碼把時間花費在操做數棧的操做上:入棧、出棧、複製、交換、產生消費變量的操做。所以,局部變量數組和操做數棧之間的交換變量指令操做經過字節碼頻繁執行。好比,一個簡單的變量初始化語句將產生兩條跟操做數棧交互的字節碼。
1
|
inti;
|
被編譯成下面的字節碼:
1
2
|
0: iconst_0 // Push 0 to top of the operand stack
1: istore_1 // Pop value from top of operand stack and store as local variable 1
|
更多關於局部變量數組、操做數棧和運行時常量池之間交互的詳細信息,能夠在類文件結構部分找到。
每一個棧幀都有一個運行時常量池的引用。這個引用指向棧幀當前運行方法所在類的常量池。經過這個引用支持動態連接(dynamic linking)。
C/C++ 代碼通常被編譯成對象文件,而後多個對象文件被連接到一塊兒產生可執行文件或者 dll。在連接階段,每一個對象文件的符號引用被替換成了最終執行文件的相對偏移內存地址。在 Java中,連接階段是運行時動態完成的。
當 Java 類文件編譯時,全部變量和方法的引用都被當作符號引用存儲在這個類的常量池中。符號引用是一個邏輯引用,實際上並不指向物理內存地址。JVM 能夠選擇符號引用解析的時機,一種是當類文件加載並校驗經過後,這種解析方式被稱爲飢餓方式。另一種是符號引用在第一次使用的時候被解析,這種解析方式稱爲惰性方式。不管如何 ,JVM 必需要在第一次使用符號引用時完成解析並拋出可能發生的解析錯誤。綁定是將對象域、方法、類的符號引用替換爲直接引用的過程。綁定只會發生一次。一旦綁定,符號引用會被徹底替換。若是一個類的符號引用尚未被解析,那麼就會載入這個類。每一個直接引用都被存儲爲相對於存儲結構(與運行時變量或方法的位置相關聯的)偏移量。
堆被用來在運行時分配類實例、數組。不能在棧上存儲數組和對象。由於棧幀被設計爲建立之後沒法調整大小。棧幀只存儲指向堆中對象或數組的引用。與局部變量數組(每一個棧幀中的)中的原始類型和引用類型不一樣,對象老是存儲在堆上以便在方法結束時不會被移除。對象只能由垃圾回收器移除。
爲了支持垃圾回收機制,堆被分爲了下面三個區域:
對象和數組永遠不會顯式回收,而是由垃圾回收器自動回收。一般,過程是這樣的:
非堆內存指的是那些邏輯上屬於 JVM 一部分對象,但實際上不在堆上建立。
非堆內存包括:
Java 字節碼是解釋執行的,可是沒有直接在 JVM 宿主執行原生代碼快。爲了提升性能,Oracle Hotspot 虛擬機會找到執行最頻繁的字節碼片斷並把它們編譯成原生機器碼。編譯出的原生機器碼被存儲在非堆內存的代碼緩存中。經過這種方法,Hotspot 虛擬機將權衡下面兩種時間消耗:將字節碼編譯成本地代碼須要的額外時間和解釋執行字節碼消耗更多的時間。
方法區存儲了每一個類的信息,好比:
全部線程共享同一個方法區,所以訪問方法區數據的和動態連接的進程必須線程安全。若是兩個線程試圖訪問一個還未加載的類的字段或方法,必須只加載一次,並且兩個線程必須等它加載完畢才能繼續執行。
一個編譯後的類文件包含下面的結構:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_pool[constant_pool_count – 1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
|
magic, minor_version, major_version | 類文件的版本信息和用於編譯這個類的 JDK 版本。 |
constant_pool | 相似於符號表,儘管它包含更多數據。下面有更多的詳細描述。 |
access_flags | 提供這個類的描述符列表。 |
this_class | 提供這個類全名的常量池(constant_pool)索引,好比org/jamesdbloom/foo/Bar。 |
super_class | 提供這個類的父類符號引用的常量池索引。 |
interfaces | 指向常量池的索引數組,提供那些被實現的接口的符號引用。 |
fields | 提供每一個字段完整描述的常量池索引數組。 |
methods | 指向constant_pool的索引數組,用於表示每一個方法簽名的完整描述。若是這個方法不是抽象方法也不是 native 方法,那麼就會顯示這個函數的字節碼。 |
attributes | 不一樣值的數組,表示這個類的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 註解。 |
能夠用 javap 查看編譯後的 java class 文件字節碼。
若是你編譯下面這個簡單的類:
1
2
3
4
5
6
|
packageorg.jvminternals;
publicclassSimpleClass {
publicvoidsayHello() {
System.out.println("Hello");
}
}
|
運行下面的命令,就能夠獲得下面的結果輸出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
public class org.jvminternals.SimpleClass
SourceFile: "SimpleClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // "Hello"
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // org/jvminternals/SimpleClass
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/jvminternals/SimpleClass;
#14 = Utf8 sayHello
#15 = Utf8 SourceFile
#16 = Utf8 SimpleClass.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 Hello
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 org/jvminternals/SimpleClass
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
{
public org.jvminternals.SimpleClass();
Signature: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/jvminternals/SimpleClass;
public void sayHello();
Signature: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String "Hello"
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/jvminternals/SimpleClass;
}
|
這個 class 文件展現了三個主要部分:常量池、構造器方法和 sayHello 方法。
這個 class 文件用到下面這些字節碼操做符:
aload0 | 這個操做碼是aload格式操做碼中的一個。它們用來把對象引用加載到操做碼棧。 表示正在被訪問的局部變量數組的位置,但只能是0、一、二、3 中的一個。還有一些其它相似的操做碼用來載入非對象引用的數據,如iload, lload, float 和 dload。其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double。局部變量數組位置大於 3 的局部變量能夠用 iload, lload, float, dload 和 aload 載入。這些操做碼都只須要一個操做數,即數組中的位置 |
ldc | 這個操做碼用來將常量從運行時常量池壓棧到操做數棧 |
getstatic | 這個操做碼用來把一個靜態變量從運行時常量池的靜態變量列表中壓棧到操做數棧 |
invokespecial, invokevirtual | 這些操做碼屬於一組函數調用的操做碼,包括:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual。在這個 class 文件中,invokespecial 和 invokevirutal 兩個指令都用到了,二者的區別是,invokevirutal 指令調用一個對象的實例方法,invokespecial 指令調用實例初始化方法、私有方法、父類方法。 |
return | 這個操做碼屬於ireturn、lreturn、freturn、dreturn、areturn 和 return 操做碼組。每一個操做碼返回一種類型的返回值,其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double,a 表示 對象引用。沒有前綴類型字母的 return 表示返回 void |
跟任何典型的字節碼同樣,操做數與局部變量、操做數棧、運行時常量池的主要交互以下所示。
構造器函數包含兩個指令。首先,this 變量被壓棧到操做數棧,而後父類的構造器函數被調用,而這個構造器會消費 this,以後 this 被彈出操做數棧。
sayHello() 方法更加複雜,正如以前解釋的那樣,由於它須要用運行時常量池中的指向符號引用的真實引用。第一個操做碼 getstatic 從System類中將out靜態變量壓到操做數棧。下一個操做碼 ldc 把字符串 「Hello」 壓棧到操做數棧。最後 invokevirtual 操做符會調用 System.out 變量的 println 方法,從操做數棧做彈出」Hello」 變量做爲 println 的一個參數,並在當前線程開闢一個新棧幀。
JVM 啓動時會用 bootstrap 類加載器加載一個初始化類,而後這個類會在public static void main(String[])調用以前完成連接和初始化。執行這個方法會執行加載、連接、初始化須要的額外類和接口。
加載(Loading)是這樣一個過程,找到表明這個類的 class 文件或根據特定的名字找到接口類型,而後讀取到一個字節數組中。接着,這些字節會被解析檢驗它們是否表明一個 Class 對象幷包含正確的 major、minor 版本信息。直接父類的類和接口也會被加載進來。這些操做一旦完成,類或者接口對象就從二進制表示中建立出來了。
連接(Linking)是校驗類或接口並準備類型和父類父接口的過程。連接過程包含三步:校驗(verifying)、準備(preparing)、部分解析(optionally resolving)。
校驗會確認類或者接口表示是否結構正確,以及是否遵循 Java 語言和 JVM 的語義要求,好比會進行下面的檢查:
在驗證階段作這些檢查意味着不須要在運行階段作這些檢查。連接階段的檢查減慢了類加載的速度,可是它避免了執行這些字節碼時的屢次檢查。
準備過程包括爲靜態存儲和 JVM 使用的數據結構(好比方法表)分配內存空間。靜態變量建立並初始化爲默認值,可是初始化代碼不在這個階段執行,由於這是初始化過程的一部分。
解析是可選的階段。它包括經過加載引用的類和接口來檢查這些符號引用是否正確。若是不是發生在這個階段,符號引用的解析要等到字節碼指令使用這個引用的時候纔會進行。
類或者接口初始化由類或接口初始化方法<clinit>的執行組成。
JVM 中有多個類加載器,分飾不一樣的角色。每一個類加載器由它的父加載器加載。bootstrap 加載器除外,它是全部最頂層的類加載器。
共享類數據(CDS)是Hotspot JVM 5.0 的時候引入的新特性。在 JVM 安裝過程當中,安裝進程會加載一系列核心 JVM 類(好比 rt.jar)到一個共享的內存映射區域。CDS 減小了加載這些類須要的時間,提升了 JVM 啓動的速度,容許這些類被不一樣的 JVM 實例共享,同時也減小了內存消耗。
The Java Virtual Machine Specification Java SE 7 Edition 中寫得很清楚:「儘管方法區邏輯上屬於堆的一部分,簡單的實現能夠選擇不對它進行回收和壓縮。」。Oracle JVM 的 jconsle 顯示方法區和 code cache 區被當作爲非堆內存,而 OpenJDK 則顯示 CodeCache 被當作 VM 中對象堆(ObjectHeap)的一個獨立的域。
全部的類加載以後都包含一個加載自身的加載器的引用,反過來每一個類加載器都包含它們加載的全部類的引用。
JVM 維護了一個按類型區分的常量池,一個相似於符號表的運行時數據結構。儘管它包含更多數據。Java 字節碼須要數據。這個數據常常由於太大不能直接存儲在字節碼中,取而代之的是存儲在常量池中,字節碼包含這個常量池的引用。運行時常量池被用來上面介紹過的動態連接。
常量池中能夠存儲多種類型的數據:
示例代碼以下:
1
|
Object foo =newObject();
|
寫成字節碼將是下面這樣:
1
2
3
|
0: new #2 // Class java/lang/Object
1: dup
2: invokespecial #3 // Method java/ lang/Object "<init>"( ) V
|
new 操做碼的後面緊跟着操做數 #2 。這個操做數是常量池的一個索引,表示它指向常量池的第二個實體。第二個實體是一個類的引用,這個實體反過來引用了另外一個在常量池中包含 UTF8 編碼的字符串類名的實體(// Class java/lang/Object)。而後,這個符號引用被用來尋找 java.lang.Object 類。new 操做碼建立一個類實例並初始化變量。新類實例的引用則被添加到操做數棧。dup 操做碼建立一個操做數棧頂元素引用的額外拷貝。最後用 invokespecial 來調用第 2 行的實例初始化方法。操做碼也包含一個指向常量池的引用。初始化方法把操做數棧出棧的頂部引用當作此方法的一個參數。最後這個新對象只有一個引用,這個對象已經完成了建立及初始化。
若是你編譯下面的類:
1
2
3
4
5
6
7
8
|
packageorg.jvminternals;
publicclassSimpleClass {
publicvoidsayHello() {
System.out.println("Hello");
}
}
|
生成的類文件常量池將是這個樣子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // "Hello"
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // org/jvminternals/SimpleClass
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/jvminternals/SimpleClass;
#14 = Utf8 sayHello
#15 = Utf8 SourceFile
#16 = Utf8 SimpleClass.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 Hello
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 org/jvminternals/SimpleClass
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
|
這個常量池包含了下面的類型:
Integer | 4 字節常量 |
Long | 8 字節常量 |
Float | 4 字節常量 |
Double | 8 字節常量 |
String | 字符串常量指向常量池的另一個包含真正字節 Utf8 編碼的實體 |
Utf8 | Utf8 編碼的字符序列字節流 |
Class | 一個 Class 常量,指向常量池的另外一個 Utf8 實體,這個實體包含了符合 JVM 內部格式的類的全名(動態連接過程須要用到) |
NameAndType | 冒號(:)分隔的一組值,這些值都指向常量池中的其它實體。第一個值(「:」以前的)指向一個 Utf8 字符串實體,它是一個方法名或者字段名。第二個值指向表示類型的 Utf8 實體。對於字段類型,這個值是類的全名,對於方法類型,這個值是每一個參數類型類的類全名的列表。 |
Fieldref, Methodref, InterfaceMethodref | 點號(.)分隔的一組值,每一個值都指向常量池中的其它的實體。第一個值(「.」號以前的)指向類實體,第二個值指向 NameAndType 實體。 |
異常表像這樣存儲每一個異常處理信息:
若是一個方法有定義 try-catch 或者 try-finally 異常處理器,那麼就會建立一個異常表。它爲每一個異常處理器和 finally 代碼塊存儲必要的信息,包括處理器覆蓋的代碼塊區域和處理異常的類型。
當方法拋出異常時,JVM 會尋找匹配的異常處理器。若是沒有找到,那麼方法會當即結束並彈出當前棧幀,這個異常會被從新拋到調用這個方法的方法中(在新的棧幀中)。若是全部的棧幀都被彈出尚未找到匹配的異常處理器,那麼這個線程就會終止。若是這個異常在最後一個非守護進程拋出(好比這個線程是主線程),那麼也有會致使 JVM 進程終止。
Finally 異常處理器匹配全部的異常類型,且無論什麼異常拋出 finally 代碼塊都會執行。在這種狀況下,當沒有異常拋出時,finally 代碼塊仍是會在方法最後執行。這種靠在代碼 return 以前跳轉到 finally 代碼塊來實現。
除了按類型來分的運行時常量池,Hotspot JVM 在永久代還包含一個符號表。這個符號表是一個哈希表,保存了符號指針到符號的映射關係(也就是 Hashtable<Symbol*, Symbol>),它擁有指向全部符號(包括在每一個類運行時常量池中的符號)的指針。
引用計數被用來控制一個符號從符號表從移除的過程。好比當一個類被卸載時,它擁有的在常量池中全部符號的引用計數將減小。當符號表中的符號引用計數爲 0 時,符號表會認爲這個符號再也不被引用,將從符號表中卸載。符號表和後面介紹的字符串表都被保存在一個規範化的結構中,以便提升效率並保證每一個實例只出現一次。
Java 語言規範要求相同的(即包含相同序列的 Unicode 指針序列)字符串字面量必須指向相同的 String 實例。除此以外,在一個字符串實例上調用 String.intern() 方法的返回引用必須與字符串是字面量時的同樣。所以,下面的代碼返回 true:
1
|
("j"+"v"+"m").intern() =="jvm"
|
Hotspot JVM 中 interned 字符串保存在字符串表中。字符串表是一個哈希表,保存着對象指針到符號的映射關係(也就是Hashtable<oop, Symbol>),它被保存到永久代中。符號表和字符串表的實體都以規範的格式保存,保證每一個實體都只出現一次。
當類加載時,字符串字面量被編譯器自動 intern 並加入到符號表。除此以外,String 類的實例能夠調用 String.intern() 顯式地 intern。當調用 String.intern() 方法時,若是符號表已經包含了這個字符串,那麼就會返回符號表裏的這個引用,若是不是,那麼這個字符串就被加入到字符串表中同時返回這個引用。
原文連接: jamesdbloom 翻譯: ImportNew.com - 挖坑的張師傅