本章主要介紹jvm,dvm,art。通過對這三個虛擬機的介紹讓學生明白,android虛擬機是一步步從dvm發展到今天的art,以及在發展的過程中,android操作系統對虛擬機主要做了那些方面的優化并詳細的講解了java虛擬機在結構,編譯流程,類加載以及內存管理等方面的知識。
### **JVM結構詳解**
* JVM整體結構詳解
* Java代碼的編譯和執行過程
* 內存管理和垃圾回收
#### **JVM整體結構詳解**


**Class文件的生成**
* **編譯流程**

> **備注**:
> 1. JVM字節碼就是Class字節碼
> 2. javac就是一個編譯器程序
**類加載器**

**加載流程**

* **Loading**:類的信息從文件中獲取并且載入到JVM的內存里
* **Verifying**:檢查讀入的結構是否符合JVM規范的描述
* **Preparing**:分配一個結構來存儲類信息
* **Resolving**:把這個類的常量池中的所有的符號引用改變成直接引用
* **Initializing**:執行靜態初始化程序,把靜態變量初始化成指定的值
#### **JVM內存管理**
內存分配是一款虛擬機產品的核心內容,良好的內存分配策略可以提高虛擬機的處理效率。Dalvik虛擬機的內容分配策略和Java 虛擬機的內存分配策略類似。
Java 內存分配與管理是Java 的核心技術之一, 一般來說, Java 在內存分配時會涉及到以
下區域。
* 寄存器: 我們在程序中無法控制。
* 棧: 存放基本類型的數據和對象的引用,但**對象本身不存放在棧中,而是存放在堆中**。
* 堆: 存放用new 產生的數據。
* 靜態域: 存放在對象中用static 定義的靜態成員。
* 常量池: 存放常量。
* 非RAM 存儲: 硬盤等永久存儲空間
**Java棧區**
* 作用:它存放的是Java方法執行時的所有數據
* 組成:由棧幀組成,一個棧幀代表一個方法的執行
> **備注**:
> 一般我們類中的方法都是被嵌套調用的,A調用B,B調用C,C調用D,以此類推,一直嵌套或者返回。棧區如何描述這個過程,這時就需要棧幀這個概念,一個棧幀代表一個方法的執行,因此Java棧可以完整描述Java中方法嵌套調用,同時得知棧中主要核心就是棧幀,搞清楚棧幀自然就明白Java棧區。
在函數中定義的一些基本類型的變量數據,還有對象的引用變量都在函數的戰內存中分配。當在一段代碼塊中定義一個變量時, Java 就在校中為這個變量分配內存空間,當該變量退出該作用域后, Java 會自動釋放掉為該變量所分配的內存空間,該內存空間可以立即被另作他用。
棧也叫棧內存,是Java 程序的運行區,是在線程創建時創建的。它的生命期是跟隨錢程的生命期,線程結束找內存也就釋放。
棧中的數據都是以棧幀(Stack Frame)的格式存在的。棧幀是一個內存區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集。
**Java棧幀**
* 作用:每個方法從調用到執行完成就對應一個棧幀在虛擬機中入棧到出棧。
舉例:A方法運行時調用到了B方法,在A方法執行調用到B方法的那行代碼時,JVM就會創建一個保存B方法的棧幀,然后把這棧幀壓入到Java棧區;到B方法執行完要返回到A方法時,這個棧幀就會隨之被彈出Java棧區。
* 組成:**局部變量表**、**棧操作數**、**動態鏈接**、**方法出口**
局部變量表中存放了編譯期可知的各種基本數據類型(boolean 、byte 、char 、short 、mt 、float 、long 、double)、對象引用(reference 類型,它不等同于對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress 類型(指向了一條字節碼指令的地址) 。
棧幀中到底存在著什么數據呢?在棧幀中主要保存如下3 類數據。
* 本地變量(Local Variabl es):包括輸入參數和輸出參數以及方法內的變量。
* 棧操作(Operand Stack) : 記錄出棧、入棧的操作。
* 棧幀數據(Frame Data) : 包括類文件、方法等。
當一個方法A被調用時,就產生了一個棧幀F1,并被壓入棧中,A方法又調用B方法,于是產生了棧幀F2,F2也被壓入棧,執行完畢后,先彈出F2棧幀,再彈出F1棧幀,遵循“先進后出”原則。如下圖所示

圖中,在一個棧中有兩個棧幀,棧幀2是最先被調用的方法,先入棧,然后方法2 又調用了方法1,棧幀1處于棧頂的位置, 棧幀2 處于棧底,執行完畢后,依次彈出棧幀l 和棧幀2,線程結束,棧釋放。
**本地方法棧**
作用:本地方法棧是專門為native方法服務的。
> **備注**:
> 1. 本地方法棧也是通過棧幀來記錄每個方法的調用
> 2. 在Java系統中,在本地方法棧執行的是非Java語言編寫的代碼,比如C/C++
> 3. JVM規范對本地方法棧中的方法使用的語言、使用方式與數據結構并沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(Sun HotSpot虛擬機)直接把Java棧和本地方法棧合二為一
虛擬機棧描述的是Java方法執行的內存模型: 每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程, 就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。對于活動線程中棧頂的棧幀,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法,正在執行的字節碼指令都只針對當前有效棧幀進行操作。
在棧幀的基礎上,不難理解虛擬機棧的內存結構。Java 虛擬機規范規定虛擬機棧的大小是可以固定的或者動態分配大小。Java 虛擬機實現可以向程序員提供對Java 棧的初始大小的控制,以及在動態擴展或者收縮Java 棧的情況下, 控制Java 棧的最大值和最小值。
下面列出的兩種異常情況與Java 棧相關。
* 如果線程請求的枝深度大于虛擬機所允許的深度,則Java 虛擬機將拋出StackOverflowError異常。
* 如果虛擬機棧可以動態擴展((當前大部分的Java 虛擬機都可動態擴展, 只不過Java 虛擬機規范中也允許固定長度的虛擬機棧),但是無法申請到足夠的內存來實現擴展,或者不能得到足夠的內存為一個新線程創建初始Java 棧,則Java 虛擬機將拋出OutOfMemoryError異常。
有人通常把Java 內存區分為堆內存(Heap)和棧內存(Stack),其中所指的“棧”就是現在講的虛擬機棧, 或者說是虛擬機棧中的局部變量表部分。
**方法區**
作用:存儲虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后等數據
方法區在虛擬機啟動時創建,是一塊所有**線程共享的內存區域**。方法區(Method Area)與Java 堆一樣, 是各個線程共享的內存區域。雖然Java 虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫作Non-Heap(非堆)。
Java 虛擬機規范對這個區域的限制非常寬松,除了**和Java 堆一樣不需要連續的內存和可以選擇固定大小或者可擴展**外,還可以選擇不實現垃圾收集。但是,垃圾回收在方法區還是必須有的,只是回收效果不是很明顯。這個區域的**回收目標主要針對的是常量池的回收和對類型的卸載。**
方法區的**大小也可以控制**,以下異常與方法區相關:**如果方法區無法滿足內存分配需求時命,將會拋OutOfMemoryError 異常**。
**堆區**
**作用:所有通過new創建的對象的內存都在堆中分配
特點:是虛擬機中最大的一塊內存,是GC要回收的部分**

堆內存用來存放由關鍵字new 創建的對象和數組。在堆中分配的內存,由Java 虛擬機的自動垃圾回收器來管理。
數組和對象本身在堆中分配,即使程序運行到使用new 產生數組或者對象的語句所在的代碼塊之外,數組和對象本身占據的內存不會被釋放,**數組和對象在沒有引用變量指向它時,才變為垃圾,不能再被使用,但仍然占據內存空間不放,在隨后的一個不確定的時間被垃圾回收器收走(釋放掉)** 。這也**是Java 比較占內存的原因**。
實際上, **棧中的變量指向堆內存中的變量,這就是Java 中的指針**。
**分為三部分**:
* ( 1) **Pemanent Space 永久存儲區**
永久存儲區是一個常駐內存區域,用于存放JDK 自身所攜帶的Class Interface 的元數據。也就是說,**它存儲的是運行環境必需的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉Java 虛擬機才會釋放此區域所占用的內存**。
* (2) **Young Generation Space 新生區**
**新生區是類的誕生、成長、消亡的區域, 一個類在這里產生、應用,最后被垃圾回收器收集,結束生命**。新生區又分為兩部分:伊甸區(Eden space)和幸存者區(Survivor pace),所有的類都是在伊甸區被new(新建)出來的。幸存區有兩個: 0 區(Survivor 0 space)和l 區(Survivor 1 space)。當伊甸園的空間用完時,程序又需要創建對象, Java 虛擬機的垃圾回收器將對伊甸園區進行垃圾回收,將伊甸園區中的不再被其他對象所引用的對象進行銷毀。然后將伊甸園中的剩余對象移動到幸存0 區。若幸存0 區也滿了,再對該區進行垃圾回收,然后移動到1 區。那如果l 區也滿了呢?再移動到養老區。
* (3) **Tenure generation space 養老區**
養老區用于保存**從新生區篩選出來的Java 對**象, **一般池對象都在這個區域活躍**。

**注意**:但是無論如何劃分,都與存放內容無關,無論哪個區域,存儲的都仍然是對象實例。**進一步劃分的目的是為了更好地回收內存,或者更快地分配內存**。
Java 堆是類實例和數組的分配空間, 是**一塊所有線程共享的內存區域**。堆在虛擬機啟動時創建, 是Java 虛擬機所管理的內存中最大的一塊。**內存泄漏和溢出問題大都發生在堆區域**。所以對于大多數應用來說, Java 堆(Java Heap)是Java 虛擬機所管理的內存中最大的一塊。
Java 堆內存區域的唯一目的就是存放對象實例,**幾乎所有的對象實例都在這里分配內存**。如果堆中沒有可用內存完成類實例或者數組的分配,在對象數量達到最大堆的容量限制后將拋出OutOfMemoryError(**OOM**) 異常。
Java 堆也是垃圾收集器管理的主要區域,因此很多時候也被稱作“ GC 堆”。‘
> **引用**:
> Java 虛擬機規范:
> 1. 所有的對象實例以及數組都要在堆上分配,但是隨著JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。
> 2. 規定堆在內存單元中只要在邏輯上是連續的, Java 堆是可以是固定大小的,或者按照需求做動態擴展, 并且可以在一個大的堆變的不必要時收縮。Java 虛擬機的實現向程序員或者用戶提供了對堆初始化大小的控制,以及對堆動態擴展和收縮的最大值和最小值的控制。
#### **運行時的數據區域**
Java 通過**自身的動態內存分配和垃圾回收機制**,可以便Java 程序員不用像C++程序員那么頭疼內存的分配與回收。
通過**Java 虛擬機的自動內存管理機制**,不僅降低了編碼的難度,而且不容易出現內存泄漏和內存溢出的問題。但是這過于理想的愿望正是由于把內存的控制權交給了Java 虛擬機, **一旦出現內存泄漏和溢出,我們就必須翻過Java 虛擬機自動內存管理這堵高墻去排查錯誤。**
《Java 虛擬機規范》的規定, Java 虛擬機在執行Java 程序時,即運行時環境下會把其所管理的內存劃分為幾個不同的數據區域。有的區域伴隨虛擬機進程的啟動而創建,死亡而銷毀:有些區域則是依賴用戶線程的啟動時創建, 結束時銷毀。
所有線程共享:方法區和堆, 虛擬機棧、本地方法棧和程序計數器是線程隔離的數據區,是每個線程稀有的。Java 虛擬機運行時的數據區結構如下圖所示。

Java 虛擬機內存模型中定義的訪問操作與物理計算機處理的基本一致。Java 通過多線程機制使得多個任務同時執行處理,所有的線程共享Java 虛擬機內存區域main memory ,而每個線程又單獨的有自己的工作內存, 當線程與內存區域進行交互時, 數據從主存復制到工作內存,進而交由線程處理(操作碼+操作數) 。
**程序計數器( Program Counter Register)**
程序計數器是一塊較小的內存空間, 其作用相當于當前線程所執行的字節碼的行號指示器。在Java 虛擬機的概念模型里, 字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令,很多基礎功能都需要依賴這個計數器來完成。例如分支、循環、跳轉、異常處理、線程恢復等。
由于在Java 虛擬機的多線程應用中, 是通過線程輪流切換并分配處理器執行時間的方式來實現的,所以在任何一個確定的時刻的處理器(對于多核處理器來說是一個內核),只會執行一條線程中的指令。為了在線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器, 并且各條線程之間的計數器互不影響,能夠獨立存儲,我們稱這類內存區域為“線程私有”的內存。
如果線程正在執行的是一個Java 方法, 這個計數器記錄的是正在執行的虛擬機字節碼指令的地址。如果正在執行的是Native(本地)方法,那么這個計數器值為空( Undefined) 。**此內存區域是唯一一個在Java 虛擬機規范中沒有規定任何
OutOfM emoryError 情況的區域**。
因為操作系統使用的是時間片輪流的多線程并發方式,所以在任何時刻,處理器只會處理當前線程的指令。**線程間切換的并發要求每個線程都需要有一個私有的程序計數器,并且程序計數器問互不影響。**
當程序計數器存儲當前線程下一條要執行的字節碼的地址時, 會占用較小的內存空間。所有的控制執行流程(例如分支、循環、返回、異常等)功能都在程序計數器的指示范圍之內, 字節碼解釋器通過改變程序計數器值的方式來獲取下一條要執行的字節碼的指令。
**運行時常量池**
**運行時常量池( Runtime Constant Pool )是方法區的一部分**。在Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于**存放編譯期生成的各種字面量和符號引用**,這部分內容將**在類加載后存放到方法區的運行時常量池**中。
常量池是每個類的Class 文件中存儲編譯期生成的各種字面量和符號引用的運行期表示,其**數據結構是一種由無符號數和表組長的類似于C 語言結構體的偽結構**。另外,常量池也是方法區的一部分, **類的常量池在該類的Java class 文件被Java 虛擬機成功地裝載時創建,這部分內容在類加載后存放到方法區的運行時常量池中**。
**Java 虛擬機對Class 文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用于存儲哪種數據都必須符合規范上的要求,這樣才會被虛擬機認可、裝載和執行**。但**對于運行時常量池來說**, Java 虛擬機規范**沒有做任何細節的要求**,不同的提供商實現的虛擬機可以按照自己的需要來實現這個內存區域。不過,**一般來說,除了保存Class 文件中描述的符號引用外, 還會把翻譯出來的直接引用也存儲在運行時常量池中**。
**運行時常量池相對于Class 文件常量池的另外一個重要特征是具備動態性**, **Java 并不要求常量一定只能在編譯期產生**,也就是并非預置入Class 文件中常量池的內容才能進入方法區運行時常量池, **運行期間也可能將新的常量放入池中**,這種特性被開發人員利用得**比較多的便是String類的intern()方法**。
既然運行時常量池是方法區的一部分,自然**會受到方法區內存的限制**,當常量池無法再申請到內存時會拋出OutOfMemoryError 異常。以下異常與常量池有關:
在裝載Class 文件時,如果常量池的創建需要比Java 虛擬機的方法區中需求更多的內存時,將會拋出OutOfMemoryError異常。
#### **為什么要把Java 虛擬機堆和Java 虛擬機棧區分出來呢? Java 虛擬機棧中不是也可以存儲數據嗎?**
* (1)從**軟件設計的角度**看, **JVM棧**代表了**處理邏輯**,而**JVM堆**代表了**數據**。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、模塊化的思想在軟件設計的方方面面都有體現。
* (2) JVM堆與JVM棧的分離,使得JVM堆中的內容可以被多個JVM棧共享(也可以理解為多個線程訪問同一個對象) 。這種共享的收益是很多的。一方面這種共享提供了一種有效的數據交互方式(如:共享內存),另一方面, JVM堆中的共享常量和緩存可以被所有JVM棧訪問,節省了空間。
* (3) JVM棧因為運行時的需要,比如保存系統運行的上下文,需要進行地址段的劃分。但是由于JVM棧只能向上增長,就會限制住JVM棧存儲內容的能力。而JVM堆不同, JVM堆中的對象是可以根據需要動態增長的,因此JVM棧和JVM堆的拆分,使得動態增長成為可能,相應JVM棧中只需記錄JVM堆中的一個地址即可。
* (4) 面向對象就是JVM堆和JVM棧的完美結合。其實,面向對象方式的程序與以前結構化的程序在執行上沒有任何區別。但是, 面向對象的引入,使得對待問題的思考方式發生了改變,而更接近于自然方式的思考。當我們把對象拆開,你會發現,對象的屬性其實就是數據,存放在JVM堆中;而對象的行為(方法),就是運行邏輯,放在JVM棧中。我們在編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。
#### **堆和棧的合作**
Java 的**堆是一個運行時數據區**, 類的對象從中分配空間。這些對象通過new 、newarray 、anew aπay 和mu ltianewarray 等指令建立,它們**不需要程序代間來顯式的釋放**。堆是**由垃圾回收來負責的**。
優勢:可以**動態地分配內存大小**,生存期也不必事先告訴編譯器,因為**它是在運行時動態分配內存的**, Java 的垃圾收集器會自動收走這些不再使用的數據。
缺點:由于要在運行時動態分配內存,**存取速度較慢**。
棧的優勢:**存取速度比堆要快**,僅次于寄存器, 棧數據可以共享。
缺點:**存在棧中的數據大小與生存期必須是確定的**,缺乏靈活性。
棧中主要存放一些基本類型的變量數據(int,short, long, byte, flo時, double, boolean, char)和對象句柄(引用) 。
棧有一個很重要的特殊性, 就是存在校中的數據可以共享。假設我們同時定義:
~~~
int a = 3;
int b = 3;
~~~
編譯器先處理int a= 3 : 首先它會在棧中創建一個變量為a 的引用,然后**查找棧中是否有3這個值**, 如果沒找到, 就將3 存放進來, 然后將a 指向3 。接著處理int b = 3 : 在創建完b 的引用變量后, 因為在棧中已經有3 這個值, 便將b 直接指向3 。這樣,就出現了a 與b 同時均指向3 的情況。如果這時再令a=4 : 那么編譯器會重新搜索棧中是否有4 值,如果沒有,則將4 存放進來, 并令a 指向4 : 如果已經有了, 則直接將a 指向這個地址。因此a 值的改變不會影響到b 的值。
**要注意這種數據的共享與兩個對象的引用同時指向一個對象的這種共享是不同的**,因為這種情況a 的修改并不會影響到b ,它是由編譯器完成的, 有利于節省空間。但是如果一個對象引用變量修改了這個對象的內部狀態, 就會影響到另一個對象引用變量。
**重點**:
**※※※※※ String**
在Java 中, String 是一個特殊的包裝類數據。可以用如下兩種的形式來創建。
~~~
String str1= new String("abc");
String str2="abc";
~~~
其中第一種是用new ()來新建對象的,它會在存放于堆中。每調用一次就會創建一個新的對
象。而第二種是先在棧中創建一個對String 類的對象引用變量str2,然后通過符號引用去**字符串常量池里**找有沒有“abc”,如果沒有,則將“ abc ”存放進字符串常量池, 并令str2指向“ abc ”,如果已經有“ abc ”則直接令str2 指向“abc”。
**比較類里面的數值是否相等時使用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時, 用==**
下面用例子說明上面的理論。
~~~
String str1= new String("abc");
String str2= new String("abc");
String str3="abc";//單獨執行該行代碼,不一定保證創建了String類的對象str3,很可能已經存在該對象,str3只需要指向它就行
String str4="abc";
System.out.println(str1==str2);//false
System.out.println(str3==str4);//true
~~~
看出str3 和str4 是指向同一個對象的.
用new 的方式的功能是生成不同的對象, 每一次生成一個。因此用第二種方式創建多個“abc”字符串, 在內存中其實只存在一個對象而已。這種寫法有利于節省內存空間,同時它可以在一定程度上提高程序的運行速度, 因為JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對于代碼`String str = new String(“abc”);`,則**一概在堆中創建新對象**,而不管其字符串值是否相等, 是否有必要創建新對象,從而加重了程序的負擔。
另一方而,要注意在使用諸如`String str = "abc" ; `的格式定義類時,總是想當然地認為,創建了String類的對象str 。此時會擔心對象可能并沒有被創建,而可能只是指向一個先前已經創建的對象。只有通過方法new()才能保證每次都創建一個新的對象。
由于String類的immutable性質, **當String變量需要經常變換其值時,應該考慮使用StringBuffer類以提高程序效率**。因為String 不屬于8 種基本數據類型,String是一個對象。所以對象的默認值是null,所以String的默認值也是null;但它又是一種特殊的對象,有其他對象沒有的一些特性。由此可見,**new String()和new String("")都是申明一個新的空字符串,是空串不是null** 。
**示例1**:
~~~
String s0 = "kvill";
String s1 = "kvill";
String s2 = "kv" + "ill";
System.out.println(s0 == s1);//true
System.out.println(s0 == s2);//true
~~~
**Java 會確保一個字符串常量只有一個復本**。因為上述例子中的s0 和s1中的“kvill”都是字符串常量,它們在編譯期就被確定了,所以s0==sl 為true;而“ kv ”和“ill ”也都是字符串常量,當一個字符串由多個字符串常量連接而成時,它自己肯定也是字符串常量,所以s2 也同樣在編譯期就被解析為一個字符串常量,所以s2也是常量池中“ kvill ”的一個引用。所以我們得出`s0==s1==s2` ;用new String()創建的字符串不是常量,不能在編譯期就確定,所以new String()創建的字符串不放入常量池中,它們有自己的地址空間。
再看下面的代碼:
~~~
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = "kv" +new String("ill");
System.out.println(s0 == s1);//false
System.out.println(s0 == s2);//false
System.out.println(s1 == s2);//false
~~~
運行結果如下。
~~~
false false false
~~~
在上述代碼中, s0還是常量池中"kvill"的應用, s1 因為無法在編譯期確定,所以是運行時創建的新對象"kvill"的引用,s2 因為有后半部分new String("ill")所以也無法在編譯期確定,所以也是一個新創建對象"kvill"的應用,明白了這些也就知道為何會得出此結果了。
另外,存在于.class 文件中的常量池,在運行時被Java 虛擬機裝載,并且可以擴充。String的intern()方法就是擴充常量池的一個方法; 當一個String 實例str 調用intern()方法時, Java 查找常量池中是否有相同Unicode的字符串常量,如果有則返回其的引用,如果沒有則在常量池中增加一個Unicode等于str的字符串并返回它的引用。
請看下面的演示示例
~~~
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = new String("kvill");
System.out.println(s0 == s1);//false
System.out.println("**********");
s1.intern();
s2 = s2.intern();//把常量池中"kvill"的引用賦給s2
System.out.println(s0 == s1);//false,雖然執行了s1.intern(),但它的返回值沒有賦給sl
System.out.println(s0 == s1.intern());//true,說明s1.intern()返回的是常量池中"kvill"的引用
System.out.println(s0 == s2);//true
~~~
另外,很多人認為使用String.intern()方法可以將一個String 類保存到一個全局String 表中,如果具有相同值的Unicode 字符串己經在這個表中,那么該方法返回表中已有字符串的地址,如果在表中沒有相同值的字符串,則將自己的地址注冊到表中,這是錯的。也就是說如果將這個全局的String 表理解為常量池的話,如果在表中沒有相同值的字符串,則不能將自己的地址注冊到表中。
請看下面的演示示例。
~~~
String s1 = new String("kvill");
String s2 = s1.intern();
System.out.println(s1==s1.intern());
System.out.println(s1+"////"+s2);
System.out.println(s2 == s1.intern());
~~~
運行結果如下。
~~~
false
kvill////kvill
true
~~~
在這個類中我們沒有聲名一個"kvill"常量,所以常量池中一開始是沒有"kvill"的,當我們調用**s1.intern**()后就在常量池中新添加了一個"kvill"常量,原來的不在常量池中的"kvill"仍然存在,也就不是“將自己的地址注冊到常量池中”了。
`s1==s1.intern()`為false 說明原來的"kvill"仍然存在; s2現在為常量池中"kvill"的地址,所以有`s2 == s1.intern()`為true 。
**通過使用equals(), String 可以比較兩字符串的(內容)Unicode 序列是否相當,如果相等返回true 。而“==”是比較兩字符串的地址是否相同,也就是是否是同一個字符串的引用。**
String的實例一旦生成就不會再改變了,比如下面的語句。
~~~
String str="kv"+"ill"+" "+"ans";
~~~
上述str有4 個字符串常量, 首先"kv"和"ill"生成了"kvill"存在內存中,然后"kvill"又和" "生成“ kvill ”存在內存中, 最后又和生成了“ kvill ans ” 。并把這個字符串的地址賦給了str, 就是**因為String 的“ 不可變”產生了很多臨時變量,這也就是為什么建議用StringBuffer的原因了**,因為StringBuffer 是可改變的。
#### **垃圾收集**
參考[Java虛擬機二:垃圾回收機制](http://blog.csdn.net/yulong0809/article/details/77421615)
**何謂垃圾收集**
垃圾收集(Garbage Collection, GC)提供了內存管理的機制,使得應用程序不需要關注內存是如何釋放的,內存用完后, 垃圾收集會進行收集,這樣就減輕了因人為管理內存而造成的錯誤,比如在C++語言里, 出現內存泄漏是很常見的。
Java 是目前使用最多的依賴于垃圾收集器的語言,在堆里面存放著Java 肚界中幾乎所有的對象,在垃圾回收前首先要確定這些對象之中哪些還在存活, 哪些己經“死”了,即不可能再被任何途徑使用的對象。
**常見的垃圾收集策略**
所有的垃圾收集算法都面臨同一個問題,那就是**找出應用程序不可到達的內存塊,并將其釋放**。這里的**不可到達主要是指應用程序已經沒有內存塊的引用了**,而在Java 中,某個對象對應用程序是可到達的是指這個對象被根(根主要是指類的靜態變量,或者活躍在所有線程攏的對象的引用)引用或者對象被另一個可到達的對象引用。
* **Reference Counting (引用計數)**
引用計數是最簡單直接的一種方式, 這種方式**在每一個對象中增加一個引用的計數, 這個計數代表當前程序有多少個引用引用了此對象, 如果此對象的引用計數變為0 , 那么此對象就可以作為垃圾收集器的目標對象來收集**。
這種策略的**優點是簡單、直接,不需要暫停整個應用。**其**缺點是需要編譯器的配合,編譯器要生成特殊的指令來進行引用計數的操作**,引用計數會影響執行效率,每引用一次都需要更新引用計數,比如每次將對象賦值給新的引用,或者對象的引用超出了作用域等, 并**且不能處理循環引用的問題**,即另外一個問題就是**引用計數不能解決交叉引用,或者環形引用的問題**。比如在一個環形鏈表里, 每一個元素都引用前面的元素,這樣首尾相連的鏈表, 當所有元素都變成不需要時, 就沒有辦法識別出來,并進行內存回收。如下圖所示:

A引用B,B又引用A,A和B都是不可達的,這時他們已經成為垃圾對象了,但是在引用計數的場景下,A和B并不能被GC回收。
* **可達性算法**

在主流的商用程序語言的主流實現中,都是稱通過可達性分析來判定對象是否存活的。
基本思想:通過一系列的稱為“GC Roots”的對象作為起點,從這些節點開始向下探索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話,就是從GC Roots到找個對象不可達)時,則證明此對象是不可用的。如上圖中的ObjD、ObjE雖然關聯,但是他們到GC Roots是不可達的,所以它們將會被判定為可回收的對象。
在Java語言中,可作為GC Roots的對象包括以下幾種:
* 虛擬機棧(棧幀中的本地變量表)中引用的對象;
* 方法區中類靜態屬性引用的對象;
* 方法區中常量引用的對象;
* 本地方法棧中JNI(即一般說的Native方法)引用的對象;
總結就是,方法運行時,方法中引用的對象;類的靜態變量引用的對象;類中常量引用的對象;Native方法中引用的對象。
**引用的類型**
* 強引用、軟引用、弱引用、虛引用
* 最常使用的就是強引用和弱引用
~~~
Object obj =new Object();
WeakReference<Object> wf=new WeakReference<Object>(obj);
obj=null;
wf.get();
~~~
上面的obj就是強引用。wf就是弱引用。
Java在1.2以后將引用分為了4種類型,即強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)。
* **強引用**:一般就是我們平常寫代碼觸發new的動作產生的對象。例如 `Object obj = new Object();` obj即為強應用,**只要強引用還引用著對象,GC就不會回收被引用的對象。**
* **軟引用(SoftReference)** : 一般就是我們目前還有點用,但是也不是必須的對象,**軟引用所關聯的對象在系統要發生內存溢出的時候,將會對這類對象進行二次回收,如果還是回收失敗才會拋出oom**;
* **弱引用(WeakReference)** : 和軟引用的意義類似,但是比軟引用更弱,**被軟引用關聯的對象,只能生存到下一次GC發生之前,當GC觸發時無論內存是否足夠都會回收掉該類對象**。
* **虛引用(PhantomReference)**:任何時候都可以被GC回收,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作為GC回收Object的標志。
**垃圾回收算法**
* **標記-清除**算法

是**最基礎的收集算法**,是因為后續的收集算法都是基于這種思路并對其不足進行改進而得到的
**思想**
先標記可以被清除的對象,然后統一回收被標記要清除的對象,標記過程中會對被標記的對象進行篩選,篩選就是判斷是否有必要執行執行對象的finalize()方法,當對象沒有重寫finalize()方法或者finalize已經被虛擬機調用過的話,就被判定為不需要執行。
如果對象需要執行finalize方法的話那么這個對象將會加入到一個叫F-Queue的隊列當中,然后會在一個優先級較低的Finalizer的線程中去執行finalize方法,但是虛擬機并不保證這個對象finalize的方法會完全執行完,因為可能在這個方法中你干了很耗時的操作的時候會導致整個F-Queue隊列的其他對象都在等待或者導致系統崩潰。如果對象在finalize方法中將自己賦值給某個類的變量或者對象的成員變量,那么這個對象將逃脫這次的GC。對象的finalize方法只會被虛擬機調用一次,第二次再觸發GC的時候不會在去調用finalize方法。
**缺點**
標記清除算法最大的缺點是空間問題,在垃圾回收之后會產生大量的內存碎片,而如果內存碎片多了,當我們再創建一個占用內存比較大的對象時就沒有足夠的內存來分配,那么這個時候虛擬機就還要再次觸發GC來清理內存后來給新的對象分配內存。
缺點是效率問題,標記和清除兩個過程的效率都不高;

* **復制算法**

復制算法會將內存空間平均分為大小相等的兩塊,每次只使用其中的一快,當觸發GC操作的時候,會將存活的對象復制到另一個區域當中,然后將整塊區域情況,這種算法最大的缺點是將原有內存分成了兩塊,每次只能使用區中一塊,也就是損失了50%的內存空間,代價有點大。

* **標記整理算法**

**背景**:
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
**思路**
標記整理算法一般用在老年代中,因為在老年代如果也采用復制算法的話,第一會浪費一部分內存,第二是當存活對象較多的時候會進行大量的復制,這樣會影響效率。所以提出了標記整理算法,標記過程還和標記清除一樣,然后在清理的時候是先將存活的對象全部像一邊移動,然后再清理掉邊界以外的內存。

三種算法相對各有優劣,在JVM三者結合使用,并不是只使用某一種算法。
* **分代收集算法** :
目前大部分商業虛擬機都使用了分代收集算法,它的思想是根據對象的存活周期將內存分為了新生代和老年代,在每個年代中采用合適的收集算法,這樣更能提升效率。例如新生代中的對象大多都是很快就會死去,只有少量的存活,那么就采用復制算法,這樣可以付出少量復制的成本就可以完成收集,而且可以解決內存碎片的問題。而老年代一般對象的存活率較高,不能浪費內存空間,所有一般采用標記清除或者標記整理算法。
**在什么時候進行垃圾回收**
* Java虛擬機無法在為新的對象分配內存空間了
* 手動調用System.gc()方法(強烈不推薦)
* 低優先級的GC線程,被運行時就會執行GC
#### **DVM和JVM的不同**
* **DVM**
執行的文件不同,一個是class,一個是dex
類加載的系統與JVM卻別較大
JVM只能存在一個,可以同時存在多個DVM
DVM基于寄存器,而JVM基于棧的

#### **DVM和ART區別**
* DVM使用JIT來將字節碼轉換成機器碼,效率低
* ART采用了AOT預編譯技術,執行速度更快
* ART會占用更多的應用安裝時間是存儲空間
### **參考鏈接**:
關于JVM、DVM、ART三者的區別,可參考以下文章:
[Android系統的體系結構](http://www.hmoore.net/alex_wsc/android/344866)
[Android運行時ART簡要介紹和學習計劃](http://blog.csdn.net/luoshengyang/article/details/39256813)
[Android ART運行時無縫替換Dalvik虛擬機的過程分析](http://blog.csdn.net/luoshengyang/article/details/39256813)
[Android運行時ART執行類方法的過程分析](http://blog.csdn.net/luoshengyang/article/details/40289405)
[JAVA虛擬機、Dalvik虛擬機和ART虛擬機簡要對比](http://blog.csdn.net/jason0539/article/details/50440669)
[ Android開發——JVM、Dalvik以及ART的區別]( http://blog.csdn.net/SEU_Calvin/article/details/52354964)
- 前言
- Android 熱補丁技術——資源的熱修復
- 插件化系列詳解
- Dex分包——MultiDex
- Google官網——配置方法數超過 64K 的應用
- IMOOC熱修復與插件化筆記
- 第1章 class文件與dex文件解析
- Class文件解析
- dex文件解析
- class與dex對比
- 第2章 虛擬機深入講解
- 第3章 ClassLoader原理講解
- 類的加載過程
- ClassLoade源碼分析
- Android中的動態加載
- 第4章 熱修復簡單講解
- 第5章 熱修復AndFix詳解
- 第6章 熱修復Tinker詳解及兩種方式接入
- 第7章 引入熱修復后代碼及版本管理
- 第8章 插件化原理深入講解
- 第9章 使用Small完成插件化
- 第10章 使用Atlas完成插件化
- 第11章 課程整體總結
- DN學院熱修復插件化筆錄
- 插件化
- 熱修復
- Android APP開發應掌握的底層知識
- 概述
- Binder
- AIDL
- AMS
- Activity的啟動和通信原理
- App啟動流程第2篇
- App內部的頁面跳轉
- Context家族史
- Service
- BroadcastReceiver
- ContentProvider
- PMS及App安裝過程