一、內(nèi)存模型
JVM的內(nèi)存模型如下圖所示,由堆、方法區(qū)、java棧、本地棧和程序計數(shù)器組成。

- 堆和方法區(qū) 是所有線程共享的。并且是歸GC 管理的內(nèi)存區(qū)域。
- 程序計數(shù)器、java棧和本地方法棧 是每個線程所獨有的。
下面逐個分析:
1、程序計數(shù)器
- Java運行時數(shù)據(jù)區(qū)中的一小塊內(nèi)存區(qū)域。在JVM中多個線程輪流獲得時間片,為了能夠使得每個線程都在線程切換后能夠恢復在切換之前的程序執(zhí)行位置,每個線程都需要有自己獨立的程序計數(shù)器,并且不能互相被干擾。程序計數(shù)器是每個線程所私有的。
- 在JVM規(guī)范中規(guī)定,如果線程執(zhí)行的是非native方法,則程序計數(shù)器中保存的是當前需要執(zhí)行的指令的地址;如果線程執(zhí)行的是native方法,則程序計數(shù)器中的值是undefined。
- 由于程序計數(shù)器中存儲的數(shù)據(jù)所占空間的大小不會隨程序的執(zhí)行而發(fā)生改變。
2、Java 棧
Java??偸桥c線程關(guān)聯(lián)在一起的,每當創(chuàng)建一個線程,JVM就會為該線程創(chuàng)建對應的Java棧,在這個Java棧中又會包含多個棧幀(Stack Frame),這些棧幀是與每個方法關(guān)聯(lián)起來的,每運行一個方法就創(chuàng)建一個棧幀,每個棧幀會含有一些局部變量、操作棧和方法返回值等信息。每當一個方法執(zhí)行完成時,該棧幀就會彈出棧幀的元素作為這個方法的返回值,并且清除這個棧幀,Java棧的棧頂?shù)臈褪钱斍罢趫?zhí)行的活動棧,也就是當前正在執(zhí)行的方法,PC寄存器也會指向該地址。只有這個活動的棧幀的本地變量可以被操作棧使用,當在這個棧幀中調(diào)用另外一個方法時,與之對應的一個新的棧幀被創(chuàng)建,這個新創(chuàng)建的棧幀被放到Java棧的棧頂,變?yōu)楫斍暗幕顒訔!M瑯蝇F(xiàn)在只有這個棧的本地變量才能被使用,當這個棧幀中所有指令都完成時,這個棧幀被移除Java棧,剛才的那個棧幀變?yōu)榛顒訔?,前面棧幀的返回值變?yōu)檫@個棧幀的操作棧的一個操作數(shù)。
在Java虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機可以動態(tài)擴展,如果擴展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。
在Hot Spot虛擬機中,可以使用-Xss參數(shù)來設置棧的大小。棧的大小直接決定了函數(shù)調(diào)用的可達深度。
3、本地方法棧
本地方法棧與Java棧的作用和原理非常相似。區(qū)別只不過是Java棧是為執(zhí)行Java方法服務的,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務的。
在JVM規(guī)范中,并沒有對本地方發(fā)展的具體實現(xiàn)方法以及數(shù)據(jù)結(jié)構(gòu)作強制規(guī)定,虛擬機可以自由實現(xiàn)它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二為一。
4、方法區(qū)
方法區(qū) 是各個線程共享的內(nèi)存區(qū)域,用于存儲被虛擬機加載的類的信息(類的全限定名、屬性的名稱和修飾符、方法的名稱和修飾符)、常量、靜態(tài)變量等。
4.1、類型信息
- 類型的全限定名
- 超類的全限定名
- 直接超接口的全限定名
- 類型標志(該類是類類型還是接口類型)
- 類的訪問描述符(public、private、default、abstract、final、static)
4.2、類型的常量池
存放該類型所用到的常量的有序集合,包括直接常量(如字符串、整數(shù)、浮點數(shù)的常量)和對其他類型、字段、方法的符號引用。常量池中每一個保存的常量都有一個索引,就像數(shù)組中的字段一樣。因為常量池中保存中所有類型使用到的類型、字段、方法的字符引用,所以它也是動態(tài)連接的主要對象(在動態(tài)鏈接中起到核心作用)。
4.3、字段信息(該類聲明的所有字段)
- 字段的類型
- 字段名稱
- 字段修飾符(public、protect、private、default)
4.4、方法信息
方法信息中包含類的所有方法,每個方法包含以下信息:
- 方法修飾符
- 方法返回類型
- 方法名
- 方法參數(shù)個數(shù)、類型、順序等
- 方法字節(jié)碼
- 操作數(shù)棧和該方法在棧幀中的局部變量區(qū)大小
異常表
4.5、類變量(靜態(tài)變量)
指該類所有對象共享的變量,即使沒有任何實例對象時,也可以訪問的類變量。它們與類進行綁定。
4.6、 指向類加載器的引用
每一個被JVM加載的類型,都保存這個類加載器的引用,類加載器動態(tài)鏈接時會用到。
7、指向Class實例的引用
類加載的過程中,虛擬機會創(chuàng)建該類型的Class實例,方法區(qū)中必須保存對該對象的引用。通過Class.forName(String className)來查找獲得該實例的引用,然后創(chuàng)建該類的對象。
4.7、方法表
為了提高訪問效率,JVM可能會對每個裝載的非抽象類,都創(chuàng)建一個數(shù)組,數(shù)組的每個元素是實例可能調(diào)用的方法的直接引用,包括父類中繼承過來的方法。這個表在抽象類或者接口中是沒有的,類似C++虛函數(shù)表vtbl。
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。
Class 文件中除了有類的版本、字段、 方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字 面量和符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。
Java 虛擬機對 Class 文件的每 一部分(自然也包括常量池)的格式都有嚴格的規(guī)定,每一個字節(jié)用于存儲哪種數(shù)據(jù)都必須符合規(guī)范上的要求, 這樣才會被虛擬機認可、裝載和執(zhí)行。但對于運行時常量池,Java 虛擬機規(guī)范沒有做任何細節(jié)的要求,不同的 提供商實現(xiàn)的虛擬機可以按照自己的需要來實現(xiàn)這個內(nèi)存區(qū)域。
運行時常量池相對于 Class 文件常量池
的另外一個重要特征是具備動態(tài)性,Java 語言并不要求常量一定只能在編譯期產(chǎn)生,也就是并非預置入 Class 文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員
利用得比較多的便是 String 類的 intern()方法。
運行時常量池 與類class文件常量池
class 文件的格式:
javac Test.java
javap -v Test.class > info.txt
總結(jié) 一
對于某個類或接口而言,其自身、父類和繼承或?qū)崿F(xiàn)的接口的信息會被直接組裝成CONSTANT_Class_info常量池項放置到常量池中;
類中或接口中使用到了其他的類,只有在類中實際使用到了該類時,該類的信息才會在常量池中有對應的CONSTANT_Class_info常量池項;
類中或接口中僅僅定義某種類型的變量,JDK只會將變量的類型描述信息以UTF-8字符串組成CONSTANT_Utf8_info常量池項放置到常量池中,上面在類中的private Date date;JDK編譯器只會將表示date的數(shù)據(jù)類型的“Ljava/util/Date”字符串放置到常量池中。
總結(jié) 二
- Int 的字面量 僅僅[-128,127] 中的值 才會緩存到運行時常量池中;
- float 和double的字面量 均未實現(xiàn)運行時常量池,所以不會保存到運行時常量池。
- String類型的字面量都會保存到運行時常量池。
https://blog.csdn.net/songwenbinasdf/article/details/79421107
5、堆內(nèi)存
Java 堆(Java Heap)是 Java 虛擬機所管理的內(nèi)存中最大的一塊。Java堆被所有線程共享。所有被new出來的對象要在堆上分配。
java堆分為新生代和老年代。
- 新生代主要存儲剛剛產(chǎn)生的對象,如果對象的生命足夠長,就把老年對象移入老年代。 新生大分為三級:eden(剛出生)、survivor space0(幸存者0)、survivor space1(幸存者1)。
新生代發(fā)生的GC 稱為 Young GC。
eden、survivor0 和 survivor1的比率是8:1:1
- 老年代:經(jīng)歷過若干次YoungGC
幸存下載的對象,會被移動到Old Generation 老年代。
當老年代區(qū)域無法為新“晉級”到Old generation的對象分配內(nèi)存空間時,就會發(fā)送GC,稱為Old GC
二、對象的加載和訪問
1、對象的創(chuàng)建過程
1.1 new的過程
- 進行類加載檢查
當遇到一個new指令,首先檢查能否在方法區(qū)的常量池中能否定位到這個類的符號引用,并且檢查類有沒有進行加載、解析和初始化; - 分配空間
有兩種常見的分配方式,一是指針碰撞,二是空閑列表,分別針對連續(xù)分配內(nèi)存和不連續(xù)的,有空隙的,取決于虛擬機是否會壓縮整理。內(nèi)存分配的大小是在類加載完成之后就已經(jīng)確定的,但是分配的時候修改指針的指向位置應該是線程安全的(棧上的Reference),第一種方式就保證原子性。 - 初始化
將分配的內(nèi)存初始化為各中數(shù)據(jù)類型的默認值,如整型數(shù)據(jù)初始化為0值(對象頭除外) - 對象初始化init<>
進行基本的設置,確定這個對象是哪個類的實例,對象的HASH碼,對象的年齡等等。
1.2 對象的內(nèi)存布局
對象在內(nèi)存中的存儲的布局可以分為3塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。
- 對象頭(Header)
對象頭包含兩個部分的信息,第一部分是對象自身的運行時數(shù)據(jù),如哈希碼、GC分代年齡、持有的鎖等等;第二部分是類型指針,指向它的類元數(shù)據(jù)的指針,通過這個虛擬機來確定這個對象是哪個類的實例。
- 實例數(shù)據(jù)(Instance Data)
對象真正存儲的數(shù)據(jù),就是程序代碼中定義的字段內(nèi)容。 - 對齊填充(Padding)
用于使對象的開頭必須是8字節(jié)的整數(shù)倍,無特殊意義。
2、對象的訪問
Java程序通過棧上的reference數(shù)據(jù)來操作堆上的具體對象,由于reference類型在Java虛擬機規(guī)范中只規(guī)定了一個指向?qū)ο笠?。而沒有規(guī)定這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,它取決于Java虛擬機實現(xiàn)。
目前主要有兩種實現(xiàn)方式:
使用句柄(類似間接指針):在Java堆中劃分出一塊內(nèi)存來作為句柄池,reference中存儲的就是對象的句柄地址,句柄中包含對象實例數(shù)據(jù)與類型各自具體地址信息。示例圖如下圖所示。
直接指針訪問:Java堆中的對象布要考慮如何放置訪問類型數(shù)據(jù)相關(guān)的信息,而reference中存儲的直接就是對象地址。示例圖如下圖所示。
句柄的好處:reference中存儲的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄中的實例數(shù)據(jù)指針,而reference本身不需要修改。
直接指針訪問的最大好處就是速度更快,節(jié)省一次指針定位時間開銷,因為對象訪問在Java中非常頻繁,這類開銷積少成多也非??捎^。目前HotSpot 虛擬機是采用的直接指針訪問策略。
https://www.cnblogs.com/chenyangyao/p/5296807.html
三、垃圾回收機制
1、垃圾回收算法
標記清除算法
標記清除算法是最基礎的垃圾收集算法。算法分為標記和清除兩個階段。
首先標記處所有可回收的對象,在編輯完成后統(tǒng)一回收掉所有被標記的對象。

它有兩個缺點:一個是效率問題,標記和清除過程效率都不高。另一個是空間問題:標記清除之后會差生大量不連續(xù)的內(nèi)存碎片??臻g碎片太多可能會導致,當程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。
復制算法
“復制”(Copying)的收集算法,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
這樣使得每次都是對其中的一塊進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可。
只是這種算法的代價是將內(nèi)存縮小為原來的一半,持續(xù)復制長生存期的對象則導致效率降低。

標記整理算法
復制收集算法在對象存活率較高時就要執(zhí)行較多的復制操作,效率將會變低。更關(guān)鍵的是,如果不想浪費50%的空間。所以在老年代一般不能直接選用這種算法。
根據(jù)老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法。
標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存
分代收集算法
GC分代的基本假設:絕大部分對象的生命周期都非常短暫,存活時間短。
“分代收集”(Generational Collection)算法,把Java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴ā?br>
在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-整理”算法來進行回收。
2、垃圾回收器
如果說收集算法是內(nèi)存回收的方法論,垃圾收集器就是內(nèi)存回收的具體實現(xiàn)。
jvm將內(nèi)存空間(堆)分為老年代和新生代,然后垃圾收器是針對不同年代作用的。
除了G1收集器外,其他收集器都是只服務于新生代和老年代中的一個。
連線表示新生代的垃圾收集器和老年代的垃圾收集器可以協(xié)同工作。
Serial 收集器:新生代收集器。
串行收集器是最古老,最穩(wěn)定以及效率高的收集器,可能會產(chǎn)生較長的停頓,只使用一個線程去回收。垃圾收集的過程中會Stop The World(服務暫停)。
- 采用“復制”算法
- 單線程,即只會使用一個CPU或一條收集線程去完成垃圾收集的工作。
- 在垃圾回收時,必須暫停其他所有線程的工作線程,即所謂的“Stop The World。
參數(shù)控制:-XX:+UseSerialGC 串行收集器

Serial old 收集器:屬于老年代收集器
- 采用“標記-整理”算法
- 單線程,即只會使用一個CPU或一條收集線程去完成垃圾收集的工作
-
Serial old收集器可以和Serial收集器協(xié)同工作,實現(xiàn)”分代回收“。
image
ParNew收集器
ParNew收集器 屬于新生代收集器,Serical收集器的多線程版本
- 采用”復制“算法
- 多條線程進行垃圾回收
- 需要停止所有的用戶線程
- 多CPU模式下,ParNew搭配CMS將是很好的選擇。
參數(shù)控制:
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制線程數(shù)量

Parallel Scavenge收集器
Parallel Scavenge可控制的吞吐量、新生代收集器。
所謂吞吐量就是CPU用于運行代碼的時間和CPU總消耗時間的比值,即吞吐量=運行用戶代碼的時間/(運行用戶代碼時間+垃圾收集時間)
- 采用”復制“算法,并行收集。
- 精確控制吞吐量:XX:MaxGCPauseMillis控制最大垃圾收集停頓時間;XX:GCTimeRatio直接設置吞吐量
- 高吞吐量可以有效的利用CPU,盡可能完成程序的任務,也就是越適合在后臺運算而不需要太多交互任務。
Parallel Old
Parallel Old 屬于老年代收集器,使用“標記-整理”算法,只能和Parallel Scavenge配合使用。
Parallel Scavenge 和 Parallel Old 組合常用于注重吞吐量以及CPU資源敏感的場合。
CMS
Concurrent Mark Sweep - 并發(fā)的垃圾收集器,且采用標記-清除算法,屬于老年代收集器。
CMS的垃圾清理分為四個階段:
- 初始標記(CMS initial mark):這一階段仍然需要Stop The World,該階段的任務僅僅是標記一下GC Roots能直接關(guān)聯(lián)到的對象。這一階段速度是很快的。
- 并發(fā)標記:這一階段是GC Roots后的延伸,即找出GC Roots能關(guān)聯(lián)到的對象,即GC Roots Tracing的過程。該階段是和用戶線程并發(fā)執(zhí)行的
- 重新標記(CMS remark) :由于上一階段并發(fā)標記是并發(fā)的,這意味著在進行GC Roots Tracing時,用戶進行仍會改變已標記的對象的狀態(tài)。故該階段重新標記也是Stop The World,是為了修正并發(fā)標記期間因用戶程序繼續(xù)運行而導致標記產(chǎn)生變動的那一部分,該階段的時間比初始標記略長,但比并發(fā)標記時間短。
- 并發(fā)清除(CMS concurrent sweep):和用戶程序一起運行。清除上述標記的垃圾。
CMS的四個階段中耗時最長的并發(fā)標記和并發(fā)清除,他們是和用戶線程一起執(zhí)行的,而耗時較短的初始標記和重新標記則耗時較短。
缺點:
- 對CPU資源非常敏感。并發(fā)導致垃圾收集線程和用戶線程競爭資源, 當CPU數(shù)量少時,垃圾收集線程將和用戶線程搶占CPU。因此,當CPU數(shù)較高時,才建議使用CMS。
- 無法有效處理浮動垃圾。
由于并發(fā)標記和并發(fā)清除階段仍有用戶線程,因此會導致不斷有垃圾產(chǎn)生,這部分垃圾無法在當次階段被清理,這部分垃圾就被稱為“浮動垃圾”。
由于在清理的同時伴隨著用戶線程,因此堆上還需要預留空間給用戶線程使用。這導致CMS運行期間,可能預留的空間無法滿足程序的需要,此時就會出現(xiàn)“Concurrent Mode Failure”。CMS垃圾收集器失效,將會啟用Serial Old。 - 垃圾碎片 。
“標記-清除” 會產(chǎn)生垃圾碎片。當無法容納大對象時,就會觸發(fā)一次Full GC,會在Full GC發(fā)生前啟用一次內(nèi)存整理合并。
G1 收集器
Garbage-First G1既可以作用于新生代又可以作用于老年代。采用”標記-整理“算法
工作原理:
對于G1來說,java堆被劃分為多個大小相等的獨立區(qū)域(Region),并跟蹤每個Region里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收時所需要的經(jīng)驗值),在后臺維護一個優(yōu)先列表,并根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region。
G1的垃圾回收分為四個步驟:
- 初始標記(Initial Marking) :僅僅是標記GC Roots能直接關(guān)聯(lián)到的對象。需要Stop The World。
- 并發(fā)標記(Concurrent Marking):同CMS的并發(fā)標記,耗時較長,但可以與用戶線程并發(fā)執(zhí)行。
- 最終標記(Final Marking):
同樣是為了修正在并發(fā)標記期間引起的變動。該階段是需要Stop The World的,但垃圾回收線程卻可以并行執(zhí)行。 - 篩選回收(Live Data Counting and Evacuation):
對各個Region的回收價值和成本進行排序,根據(jù)用戶所希望的GC停頓時間來執(zhí)行回收計劃。
G1 的特點:
- 并行與并發(fā)
并發(fā)標記階段,用戶線程和垃圾回收線程并發(fā)執(zhí)行,最終標記和篩選回收階段是垃圾回收線程并行執(zhí)行。 - 空間整合
與CMS的“標記-清理”不同,G1從整體來看是基于“標記-整理”的,從局部(兩個Region之間)來看是基于“復制”算法的。所以G1不會產(chǎn)生垃圾碎片。 - 可預測的停頓
能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒。
如何查看虛擬機的使用的垃圾收集器 和 配置參數(shù):
https://blog.csdn.net/earthhour/article/details/76468084
參考鏈接
https://blog.csdn.net/u012882134/article/details/78422258
https://www.cnblogs.com/xiaoxi/p/6486852.html
https://baijiahao.baidu.com/s?id=1565631804713416&wfr=spider&for=pc