[TOC]
## 內存模型

### 線程私有的數據區
線程私有的數據區?包括 **程序計數器** 、?**虛擬機棧**?和?**本地方法棧** 三個區域,它們的內涵分別如下:
### 程序計數器
我們知道,線程是CPU調度的基本單位。在多線程情況下,當線程數超過CPU數量或CPU內核數量時,線程之間就要根據?時間片輪詢搶奪CPU時間資源。也就是說,在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此,**為了線程切換后能夠恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器去記錄其正在執行的字節碼指令地址**。
因此,程序計數器是線程私有的一塊較小的內存空間,其可以看做是當前線程所執行的字節碼的行號指示器。如果線程正在執行的是一個 Java 方法,計數器記錄的是正在執行的字節碼指令的地址;如果正在執行的是 Native 方法,則計數器的值為空。
程序計數器是唯一一個沒有規定任何 OutOfMemoryError 的區域。
### 虛擬機棧
虛擬機棧描述的是Java方法執行的內存模型,是線程私有的。**每個方法在執行的時候都會創建一個棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息**,而且?每個方法從調用直至完成的過程,對應一個棧幀在虛擬機棧中入棧到出棧的過程。其中,局部變量表主要存放一些基本類型的變量(int, short, long, byte, float, double, boolean, char)和 對象句柄,它們可以是方法參數,也可以是方法的局部變量。
虛擬機棧有兩種異常情況:StackOverflowError 和 OutOfMemoryError。我們知道,一個線程擁有一個自己的棧,這個棧的大小決定了方法調用的可達深度(遞歸多少層次,或嵌套調用多少層其他方法,-Xss 參數可以設置虛擬機棧大小),若線程請求的棧深度大于虛擬機允許的深度,則拋出 StackOverFlowError 異常。此外,棧的大小可以是固定的,也可以是動態擴展的,若虛擬機棧可以動態擴展(大多數虛擬機都可以),但擴展時無法申請到足夠的內存(比如沒有足夠的內存為一個新創建的線程分配棧空間時),則拋出 OutofMemoryError 異常。下圖為棧幀結構圖:

### 本地方法棧
本地方法棧與Java虛擬機棧非常相似,也是線程私有的,區別是虛擬機棧為虛擬機執行 Java 方法服務,而本地方法棧為虛擬機執行 Native 方法服務。與虛擬機棧一樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
### 線程共享的數據區
線程共享的數據區?具體包括?Java堆?和?方法區?兩個區域,它們的內涵分別如下
### 方法區
方法區與Java堆一樣,也是線程共享的并且不需要連續的內存,**其用于存儲已被虛擬機加載的?類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據**。方法區通常和永久區(Perm)關聯在一起,但永久代與方法區不是一個概念,只是有的虛擬機用永久代來實現方法區,這樣就可以用永久代GC來管理方法區,省去專門內存管理的工作。根據Java虛擬機規范的規定,當方法區無法滿足內存分配的需求時,將拋出 OutOfMemoryError 異常。
**運行時常量池**(Runtime Constant Pool)是方法區的一部分,用于存放編譯期生成的各種?字面量?和?符號引用。其中,字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等;而符號引用則屬于編譯原理方面的概念,包括以下三類常量:類和接口的全限定名、字段的名稱和描述符?和?方法的名稱和描述符。因為運行時常量池(Runtime Constant Pool)是方法區的一部分,那么當常量池無法再申請到內存時也會拋出 OutOfMemoryError 異常。
運行時常量池相對于Class文件常量池的一個重要特征是具備動態性。Java語言并不要求常量一定只有編譯期才能產生,運行期間也可能將新的常量放入池中,比如字符串的手動入池方法intern()。
### 方法區的回收
方法區的內存回收目標主要是針對?**常量池的回收**?和?**對類型的卸載**。回收廢棄常量與回收Java堆中的對象非常類似。以常量池中字面量的回收為例,假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果在這時候發生內存回收,而且必要的話,這個“abc”常量就會被系統“請”出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。
判定一個常量是否是“廢棄常量”比較簡單,**而要判定一個類是否是“無用的類”的條件則相對苛刻許多**。類需要同時滿足下面3個條件才能算是“無用的類”:
* 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
* 加載該類的ClassLoader已經被回收;
* 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述3個條件的無用類進行回收(卸載),這里說的僅僅是“可以”,**而不是和對象一樣,不使用了就必然會回收**。特別地,在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出
### Java 堆
Java 堆的唯一目的就是存放對象實例,幾乎所有的對象實例(和數組)都在這里分配內存。Java堆是線程共享的,類的對象從中分配空間,這些對象通過new、newarray、 anewarray 和 multianewarray 等指令建立,它們不需要程序代碼來顯式的釋放。
由于Java堆唯一目的就是用來存放對象實例,因此其也是垃圾收集器管理的主要區域,故也稱為稱為?GC堆。從內存回收的角度看,由于現在的垃圾收集器基本都采用分代收集算法,所以為了方便垃圾回收Java堆還可以分為?新生代?和?老年代?。新生代用于存放剛創建的對象以及年輕的對象,如果對象一直沒有被回收,生存得足夠長,對象就會被移入老年代。新生代又可進一步細分為 eden、survivorSpace0 和 survivorSpace1。剛創建的對象都放入 eden,s0 和 s1 都至少經過一次GC并幸存。如果幸存對象經過一定時間仍存在,則進入老年代。

### Java堆 與 方法區的區別
Java堆是?Java代碼可及的內存,是留給開發人員使用的;而非堆(Non-Heap)是JVM留給自己用的,所以方法區、JVM內部處理或優化所需的內存 (如JIT編譯后的代碼緩存)、每個類結構 (如運行時常量池、字段和方法數據)以及方法和構造方法的代碼都在非堆內存中。
## CG
JVM 內存模型一共包括三個部分:堆 ( Java代碼可及的 Java堆 和 JVM自身使用的方法區)、棧 ( 服務Java方法的虛擬機棧 和 服務Native方法的本地方法棧 ) 和 保證程序在多線程環境下能夠連續執行的程序計數器。特別地,我們當時就提到Java堆是進行垃圾回收的主要區域,故其也被稱為GC堆;而方法區也有一個不太嚴謹的表述,就是永久代。總的來說,堆 (包括Java堆 和 方法區)是 垃圾回收的主要對象,特別是Java堆。
### 哪些內存需要回收
兩種經典算法: 引用計數法 和 可達性分析算法
#### 引用計數算法
引用計數算法是通過判斷對象的引用數量來決定對象是否可以被回收。
引用計數算法是垃圾收集器中的早期策略。在這種方法中,堆中的每個對象實例都有一個引用計數。當一個對象**被創建時**,且將該對象實例分配給一個引用變量,該對象實例的**引用計數設置為 1**。當任何其它變量被賦值為這個對象的引用時,對象實例的引用計數加 1(a = b,則b引用的對象實例的計數器加 1),但當一個對象實例的**某個引用超過了生命周期或者被設置為一個新值時**,對象實例的引用計數減 1。特別地,當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器均減 1。**任何引用計數為0的對象實例可以被當作垃圾收集**。

#### 可達性分析算法
可達性分析算法是通過判斷對象的引用鏈是否可達來決定對象是否可以被回收。
可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關系看作一張圖,通過一系列的名為 “GC Roots” 的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain)。當一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達)時,則證明此對象是不可用的,如下圖所示。在Java中,可作為 GC Root 的對象包括以下幾種:
* 虛擬機棧(棧幀中的局部變量表)中引用的對象;
* 方法區中類靜態屬性引用的對象;
* 方法區中常量引用的對象;
* 本地方法棧中Native方法引用的對象;

### 什么時候回收

對象將根據存活的時間被分為:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區)
#### 年輕代
對象被創建時,內存的分配首先發生在年輕代(大對象可以直接 被創建在年老代),大部分的對象在創建后很快就不再使用,因此很快變得不可達,于是被年輕代的GC機制清理掉
年輕代上的內存分配是這樣的,年輕代可以分為3個區域**:Eden區和兩個存活區(Survivor 0 、Survivor 1)。**
1. 絕大多數剛創建的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,因此在其上分配內存極快;
2. 當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,并將剩余的對象復制到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);
3. 此后,每次Eden區滿了,就執行一次Minor GC,并將剩余的對象都添加到Survivor0;
4. 當Survivor0也滿的時候,將其中仍然活著的對象直接復制到Survivor1,以后Eden區執行Minor GC后,就將剩余的對象添加Survivor1(此時,Survivor0是空白的)。
5. 當兩個存活區切換了幾次(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大于該值進入老年代)之后,仍然存活的對象(其實只有一小部分,比如,我們自己定義的對象),將被復制到老年代。
#### 年老代
對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次 Young GC后存活了下來),則會被復制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時, 將執行Major GC,也叫 Full GC
#### 永久代
永久代的回收有兩種:常量池中的常量,無用的類信息。
常量的回收很簡單,沒有引用了就可以被回收。對于無用的類進行回收,必須保證3點:
1. 類的所有實例都已經被回收
2. 加載類的ClassLoader已經被回收
3. 3類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)
### 如何回收
#### 標記清除算法
標記-清除算法分為標記和清除兩個階段。該算法首先從根集合進行掃描,對存活的對象對象標記,標記完畢后,再掃描整個空間中未被標記的對象并進行回收,如下圖所示。

標記-清除算法的主要不足有兩個:
* 效率問題:標記和清除兩個過程的效率都不高;
* 空間問題:標記-清除算法不需要進行對象的移動,并且僅對不存活的對象進行處理,因此標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
#### 復制算法
復制算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這種算法適用于對象存活率低的場景,比如**新生代**。

#### 標記整理算法
標記整理算法的標記過程類似標記清除算法,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存,類似于磁盤整理的過程,該垃圾回收算法適用于對象存活率高的場景(**老年代**)

#### 分代收集算法
**不同的對象的生命周期(存活情況)是不一樣的,而不同生命周期的對象位于堆中不同的區域,因此對堆內存不同區域采用不同的策略進行回收可以提高 JVM 的執行效率**。當代商用虛擬機使用的都是分代收集算法:新生代對象存活率低,就采用復制算法;老年代存活率高,就用標記清除算法或者標記整理算法。
### 不同引用類型的回收狀態
#### 強引用
~~~
Object?strongReference?=?newObject()
~~~
如果一個對象具有強引用,那垃圾回收器絕不會回收它,當內存空間不足, Java 虛擬機寧愿拋出 OOM 錯誤,使程序異常 Crash ,也不會靠隨意回收具有強引用的對象來解決內存不足的問題.如果強引用對象不再使用時,需要弱化從而使 GC 能夠回收,需要:
~~~
strongReference?=?null;?//等?GC?來回收
~~~
還有一種情況,如果:
~~~
public?void?onStrongReference(){
????Object?strongReference?=?new?Object()
}
~~~
在 onStrongReference() 內部有一個強引用,這個引用保存在 java 棧 中,而真正的引用內容 (Object)保存在 java 堆中。當這個方法運行完成后,就會退出方法棧,則引用對象的引用數為 0 ,這個對象會被回收。
但是如果 mStrongReference 引用是全局時,就需要在不用這個對象時賦值為 null ,因為 強引用 不會被 GC 回收。
#### 軟引用 (SoftReference)
如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存,只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
**軟引用可以和一個引用隊列ReferenceQueue**聯合使用,如果軟引用所引用的對象被垃圾回收器回收, java 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。


注意: 軟引用對象是在 jvm 內存不夠的時候才會被回收,我們調用 System.gc() 方法只是起通知作用, JVM 什么時候掃描回收對象是 JVM 自己的狀態決定的。就算掃描到了 str 這個對象也不會回收,只有內存不足才會回收。
#### 弱引用 (WeakReference)
弱引用與軟引用的區別在于: 只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,**不管當前內存空間足夠與否,都會回收它的內存**。不過由于垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。
**弱引用可以和一個引用隊列聯合使用**,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。


可見 weakReference 對象的生命周期基本由 GC 決定,一旦 GC 線程發現了弱引用就標記下來,第二次掃描到就直接回收了。
注意這里的 referenceQueuee 是裝的被回收的對象。
#### 虛引用 (PhantomReference)
~~~
?@Test
????public?void?onPhantomReference()throws?InterruptedException{
????????String?str?=?new?String("123456");
????????ReferenceQueue?queue?=?new?ReferenceQueue();
//?創建虛引用,要求必須與一個引用隊列關聯
????????PhantomReference?pr?=?new?PhantomReference(str,?queue);
????????System.out.println("PhantomReference:"?+?pr.get());
????????System.out.printf("ReferenceQueue:"?+?queue.poll());
????}
~~~
虛引用顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在于: 虛引用必須和引用隊列 (ReferenceQueue) 聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。
#### 總結
| 引用類型 | 調用方式 | GC | 是否內存泄漏 |
| --- | --- | --- | --- |
| 強引用 | 直接調用 | 不回收 | 是 |
| 軟引用 | .get() | 視內存情況回收 | 否 |
| 弱引用 | .get() | 回收 | 不可能 |
| 虛引用 | null | 任何時候都可能被回收,相當于沒有引用一樣 | 否 |
## 番外:不通過 GC ROOT,仍使用引用計數方式,怎么解決它的循環引用問題?
**智能指針**:智能指針以引用計數的方式來標識無用對象,使用智能指針的對象需繼承自 RefBase,RefBase 中維護了此對象的強引用數量和弱引用數量。
**若 A 強引用了 B,那 B 引用 A 時就需使用弱引用,當判斷是否為無用對象時僅考慮強引用計數是否為 0,不關心弱引用計數的數量**
## 推薦閱讀
[學習JVM是如何從入門到放棄的?](https://www.jianshu.com/p/904b15a8281f)
[JVM 內存模型概述](https://blog.csdn.net/xinzhou201/article/details/81981282)
[圖解Java 垃圾回收機制](https://blog.csdn.net/justloveyou_/article/details/71216049)
- Java
- Object
- 內部類
- 異常
- 注解
- 反射
- 靜態代理與動態代理
- 泛型
- 繼承
- JVM
- ClassLoader
- String
- 數據結構
- Java集合類
- ArrayList
- LinkedList
- HashSet
- TreeSet
- HashMap
- TreeMap
- HashTable
- 并發集合類
- Collections
- CopyOnWriteArrayList
- ConcurrentHashMap
- Android集合類
- SparseArray
- ArrayMap
- 算法
- 排序
- 常用算法
- LeetCode
- 二叉樹遍歷
- 劍指
- 數據結構、算法和數據操作
- 高質量的代碼
- 解決問題的思路
- 優化時間和空間效率
- 面試中的各項能力
- 算法心得
- 并發
- Thread
- 鎖
- java內存模型
- CAS
- 原子類Atomic
- volatile
- synchronized
- Object.wait-notify
- Lock
- Lock之AQS
- Lock子類
- 鎖小結
- 堵塞隊列
- 生產者消費者模型
- 線程池