Java開發有個很基礎的問題,雖然我們平時接觸的不多,但是了解它卻成為Java開發的必備基礎——這就是JVM。在C++中我們需要手動申請內存然后釋放內存,否則就會出現對象已經不再使用內存卻仍被占用的情況。在Java中JVM內置了垃圾回收的機制,幫助開發者承擔對象的創建和釋放的工作,極大的減輕了開發的負擔。那是不是我們就不需要了解JVM了,顯然在做一些優化或者深入研究應用性能的時候,JVM還是起了很關鍵的作用的。因此本篇就總結性的描述下JVM的內存模型與垃圾回收相關的知識。
本文的主要內容如下:
- 內存模型
- 垃圾回收
- 參考文章
內存模型
各部分的功能
這幾個存儲區最主要的就是棧區和堆區,那么什么是棧什么是堆呢?說的簡單點,棧里面存放的是基本的數據類型和引用,而堆里面則是存放各種對象實例的。
堆與棧分開設計是為什么呢?
- 棧存儲了處理邏輯、堆存儲了具體的數據,這樣隔離設計更為清晰
- 堆與棧分離,使得堆可以被多個棧共享。
- 棧保存了上下文的信息,因此只能向上增長;而堆是動態分配
棧的大小可以通過-XSs設置,如果不足的話,會引起java.lang.StackOverflowError的異常
棧區
線程私有,生命周期與線程相同。每個方法執行的時候都會創建一個棧幀(stack frame)用于存放 局部變量表、操作棧、動態鏈接、方法出口。
堆
存放對象實例,所有的對象的內存都在這里分配。垃圾回收主要就是作用于這里的。
- 堆得內存由-Xms指定,默認是物理內存的1/64;最大的內存由-Xmx指定,默認是物理內存的1/4。
- 默認空余的堆內存小于40%時,就會增大,直到-Xmx設置的內存。具體的比例可以由-XX:MinHeapFreeRatio指定
- 空余的內存大于70%時,就會減少內存,直到-Xms設置的大小。具體由-XX:MaxHeapFreeRatio指定。
因此一般都建議把這兩個參數設置成一樣大,可以避免JVM在不斷調整大小。
程序計數器
這里記錄了線程執行的字節碼的行號,在分支、循環、跳轉、異常、線程恢復等都依賴這個計數器。
方法區
類型信息、字段信息、方法信息、其他信息
總結
名稱 | 特征 | 作用 | 配置 | 異常 |
---|---|---|---|---|
棧區 | 線程私有,使用一段連續的內存空間 | 存放局部變量表、操作棧、動態鏈接、方法出口 | -XSs | StackOverflowError OutOfMemoryError |
堆 | 線程共享,生命周期與虛擬機相同 | 保存對象實例 | -Xms -Xmx -Xmn | OutOfMemoryError |
程序計數器 | 線程私有、占用內存小 | 字節碼行號 | 無 | 無 |
方法區 | 線程共享 | 存儲類加載信息、常量、靜態變量等 | -XX:PermSize -XX:MaxPermSize | OutOfMemoryError |
垃圾回收
如何定義垃圾
有兩種方式,一種是引用計數(但是無法解決循環引用的問題);另一種就是可達性分析。
判斷對象可以回收的情況:
- 顯示的把某個引用置位NULL或者指向別的對象
- 局部引用指向的對象
- 弱引用關聯的對象
垃圾回收的方法
Mark-Sweep標記-清除算法
這種方法優點就是減少停頓時間,但是缺點是會造成內存碎片。
Copying復制算法
這種方法不涉及到對象的刪除,只是把可用的對象從一個地方拷貝到另一個地方,因此適合大量對象回收的場景,比如新生代的回收。
Mark-Compact標記-整理算法
這種方法可以解決內存碎片問題,但是會增加停頓時間。
Generational Collection 分代收集
最后的這種方法是前面幾種的合體,即目前JVM主要采取的一種方法,思想就是把JVM分成不同的區域。每種區域使用不同的垃圾回收方法。
上面可以看到堆分成三個區域:
- 新生代(Young Generation):用于存放新創建的對象,采用復制回收方法,如果在s0和s1之間復制一定次數后,轉移到年老代中。這里的垃圾回收叫做minor GC;
- 年老代(Old Generation):這些對象垃圾回收的頻率較低,采用的標記整理方法,這里的垃圾回收叫做 major GC。
- 永久代(Permanent Generation):存放Java本身的一些數據,當類不再使用時,也會被回收。
這里可以詳細的說一下新生代復制回收的算法流程:
在新生代中,分為三個區:Eden, from survivor, to survior。
- 當觸發minor GC時,會先把Eden中存活的對象復制到to Survivor中;
- 然后再看from survivor,如果次數達到年老代的標準,就復制到年老代中;如果沒有達到則復制到to survivor中,如果to survivor滿了,則復制到年老代中。
- 然后調換from survivor 和 to survivor的名字,保證每次to survivor都是空的等待對象復制到那里的。
垃圾回收器
串行收集器 Serial
這種收集器就是以單線程的方式收集,垃圾回收的時候其他線程也不能工作。
并行收集器 Parallel
以多線程的方式進行收集
并發標記清除收集器 Concurrent Mark Sweep Collector, CMS
大致的流程為:初始標記--并發標記--重新標記--并發清除
G1收集器 Garbage First Collector
大致的流程為:初始標記--并發標記--最終標記--篩選回收
參考
- JVM內存模型:http://developer.51cto.com/art/200911/165015.htm
- 垃圾回收:http://www.importnew.com/19085.html
- JVM垃圾回收器:http://www.cnblogs.com/chengxuyuanzhilu/p/7088316.html
- 內存模型:?http://blog.csdn.net/u012152619/article/details/46968883
-
JVM初探 -JVM內存模型
標簽 : JVM
JVM是每個Java開發每天都會接觸到的東西, 其相關知識也應該是每個人都要深入了解的. 但接觸了很多人發現: 或了解片面或知識體系陳舊. 因此最近抽時間研讀了幾本評價較高的JVM入門書籍, 算是總結于此. 本系列博客的主體來自?深入理解Java虛擬機(第二版)?和?實戰Java虛擬機?兩部書, 部分內容參考?HotSpot實戰?和?深入理解計算機系統?以及網上大量的文章. 若文內有引文未注明出處的, 還請聯系作者修改.
?
JVM 虛擬機架構(圖片來源:?淺析Java虛擬機結構與機制)JVM 內存區域
JVM會將Java進程所管理的內存劃分為若干不同的數據區域. 這些區域有各自的用途、創建/銷毀時間:
(圖片來源:?JAVA的內存模型及結構)一. 線程私有區域
線程私有數據區域生命周期與線程相同, 依賴用戶線程的啟動/結束而創建/銷毀(在Hotspot VM內, 每個線程都與操作系統的本地線程直接映射, 因此這部分內存區域的存/否跟隨本地線程的生/死).
1. Program Counter Register(程序計數器):
一塊較小的內存空間, 作用是當前線程所執行字節碼的行號指示器(類似于傳統CPU模型中的PC), PC在每次指令執行后自增, 維護下一個將要執行指令的地址. 在JVM模型中, 字節碼解釋器就是通過改變PC值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴PC完成(僅限于Java方法, Native方法該計數器值為
undefined
).?
不同于OS以進程為單位調度, JVM中的并發是通過線程切換并分配時間片執行來實現的. 在任何一個時刻, 一個處理器內核只會執行一條線程中的指令. 因此, 為了線程切換后能恢復到正確的執行位置, 每條線程都需要有一個獨立的程序計數器, 這類內存被稱為“線程私有”內存.2. Java Stack(虛擬機棧)
虛擬機棧描述的是Java方法執行的內存模型: 每個方法被執行時會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息. 每個方法被調用至返回的過程, 就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程(VM提供了
-Xss
來指定線程的最大棧空間, 該參數也直接決定了函數調用的最大深度).- 局部變量表(對應我們常說的‘堆棧’中的‘棧’)存放了編譯期可知的各種基本數據類型(如boolean、int、double等) 、對象引用(reference : 不等同于對象本身, 可能是一個指向對象起始地址的指針, 也可能指向一個代表對象的句柄或其他與此對象相關的位置, 見下: HotSpot對象定位方式) 和?returnAddress類型(指向一條字節碼指令的地址). 其中
long
和double
占用2個局部變量空間(Slot), 其余只占用1個. 如下Java方法代碼可以使用javap命令或javassist等字節碼工具讀到:
public String test(int a, long b, float c, double d, Date date, List<String> list) { StringBuilder sb = new StringBuilder().append(a).append(b).append(c).append(d).append(date); for (String str : list) { sb.append(str); } return sb.toString(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
注: javap/javassist讀到的其實是靜態數據, 而局部變量表內存儲的卻是運行時動態加載的動態數據, 但因為局部變量表所需的內存空間在編譯期間即可完成分配, 當進入一個方法時, 這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間大小不會改變, 因此可以在概念上認定這兩部分內容存儲的數據格式相同.
3. Native Method Stack(本地方法棧)
與Java Stack作用類似, 區別是Java Stack為執行Java方法服務, 而本地方法棧則為Native方法服務, 如果一個VM實現使用C-linkage模型來支持Native調用, 那么該棧將會是一個C棧(詳見:?JVM學習筆記-本地方法棧(Native Method Stacks)), 但HotSpot VM直接就把本地方法棧和虛擬機棧合二為一.
二. 線程共享區域
隨虛擬機的啟動/關閉而創建/銷毀.
1. Heap(Java堆)
幾乎所有對象實例和數組都要在堆上分配(棧上分配、標量替換除外), 因此是VM管理的最大一塊內存, 也是垃圾收集器的主要活動區域. 由于現代VM采用分代收集算法, 因此Java堆從GC的角度還可以細分為:?新生代(Eden區、From Survivor區和To Survivor區)和老年代; 而從內存分配的角度來看, 線程共享的Java堆還還可以劃分出多個線程私有的分配緩沖區(TLAB). 而進一步劃分的目的是為了更好地回收內存和更快地分配內存.
2. Method Area(方法區)
即我們常說的永久代(Permanent Generation), 用于存儲被JVM加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據. HotSpot VM把GC分代收集擴展至方法區, 即使用Java堆的永久代來實現方法區, 這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分內存, 而不必為方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 因此收益一般很小)
不過在1.7的HotSpot已經將原本放在永久代的字符串常量池移出:?
而在1.8中, 永久區已經被徹底移除, 取而代之的是元數據區Metaspace(這一點在查看GC日志和使用jstat -gcutil查看GC情況時可以觀察到),與永久代不同, 如果不指定Metaspace大小, 如果方法區持續增長, VM會默認耗盡所有系統內存.- 運行時常量池?
方法區的一部分. Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項常量池(Constant Pool Table)用于存放編譯期生成的各種字面量和符號引用, 這部分內容會存放到方法區的運行時常量池中(如前面從test
方法中讀到的signature
信息). 但Java語言并不要求常量一定只能在編譯期產生, 即并非預置入Class文件中常量池的內容才能進入方法區運行時常量池, 運行期間也可能將新的常量放入池中, 如String
的intern()
方法.
三. 直接內存
直接內存并不是JVM運行時數據區的一部分, 但也會被頻繁的使用: 在JDK 1.4引入的NIO提供了基于Channel與Buffer的IO方式, 它可以使用Native函數庫直接分配堆外內存, 然后使用
DirectByteBuffer
對象作為這塊內存的引用進行操作(詳見:?Java I/O 擴展), 這樣就避免了在Java堆和Native堆中來回復制數據, 因此在一些場景中可以顯著提高性能.?
顯然, 本機直接內存的分配不會受到Java堆大小的限制(即不會遵守-Xms、-Xmx等設置), 但既然是內存, 則肯定還是會受到本機總內存大小及處理器尋址空間的限制, 因此動態擴展時也會出現OutOfMemoryError
異常.HotSpot對象
對象新建
-
new
一個Java Object(包括數組和Class對象), 在JVM會發生如下步驟:- VM遇到
new
指令: 首先去檢查該指令的參數是否能在常量池中定位到一個類的符號引用, 并檢查這個符號引用代表的類是否已被加載、解析和初始化過. 如果沒有, 必須先執行相應的類加載過程. - 類加載檢查通過后: VM將為新生對象分配內存(對象所需內存的大小在類加載完成后便可完全確定), VM采用指針碰撞(內存規整: Serial、ParNew等有內存壓縮整理功能的收集器)或空閑鏈表(內存不規整: CMS這種基于Mark-Sweep算法的收集器)方式將一塊確定大小的內存從Java堆中劃分出來.
- 除了考慮如何劃分可用空間外, 由于在VM上創建對象的行為非常頻繁, 因此需要考慮內存分配的并發問題. 解決方案有兩個:?
- 對分配內存空間的動作進行同步?-采用?CAS配上失敗重試?方式保證更新操作的原子性;
- 把內存分配的動作按照線程劃分在不同的空間之中進行?-每個線程在Java堆中預先分配一小塊內存, 稱為本地線程分配緩沖TLAB, 各線程首先在TLAB上分配, 只有TLAB用完, 分配新的TLAB時才需要同步鎖定(使用
-XX:+/-UseTLAB
參數設定).
- 接下來將分配到的內存空間初始化為零值(不包括對象頭, 且如果使用TLAB這一個工作也可以提前至TLAB分配時進行). 這一步保證了對象的實例字段可以不賦初始值就直接使用(訪問到這些字段的數據類型所對應的零值).
- 然后要對對象進行必要的設置: 如該對象所屬的類實例、如何能訪問到類的元數據信息、對象的哈希碼、對象的GC分代年齡等, 這部分息放在對象頭中(詳見下).
- 上面工作都完成之后, 在虛擬機角度一個新對象已經產生, 但在Java視角對象的創建才剛剛開始(
<init>
方法尚未執行, 所有字段還都為零). 所以new
指令之后一般會(由字節碼中是否跟隨有invokespecial
指令所決定-Interface一般不會有, 而Class一般會有)接著執行<init>
方法, 把對象按照程序員的意愿進行初始化, 這樣一個真正可用的對象才算完全產生出來.
- VM遇到
對象存儲布局
HotSpot VM內, 對象在內存中的存儲布局可以分為三塊區域:對象頭、實例數據和對齊填充:
- 對象頭包括兩部分:?
- 一部分是類型指針, 即是對象指向它的類元數據的指針: VM通過該指針確定該對象屬于哪個類實例. 另外, 如果對象是一個數組, 那在對象頭中還必須有一塊數據用于記錄數組長度.?
注意: 并非所有VM實現都必須在對象數據上保留類型指針, 也就是說查找對象的元數據并非一定要經過對象本身(詳見下面句柄定位對象方式).
- 一部分用于存儲對象自身的運行時數據:?HashCode、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等, 這部分數據的長度在32位和64位的VM(暫不考慮開啟壓縮指針)中分別為32bit和64bit, 官方稱之為“Mark Word”; 其存儲格式如下:
- 一部分是類型指針, 即是對象指向它的類元數據的指針: VM通過該指針確定該對象屬于哪個類實例. 另外, 如果對象是一個數組, 那在對象頭中還必須有一塊數據用于記錄數組長度.?
狀態 標志位 存儲內容 未鎖定 01 對象哈希碼、對象分代年齡 輕量級鎖定 00 指向鎖記錄的指針 膨脹(重量級鎖定) 10 執行重量級鎖定的指針 GC標記 11 空(不需要記錄信息) 可偏向 01 偏向線程ID、偏向時間戳、對象分代年齡 - 實例數據部分是對象真正存儲的有效信息, 也就是我們在代碼里所定義的各種類型的字段內容(無論是從父類繼承下來的, 還是在子類中定義的都需要記錄下來). 這部分的存儲順序會受到虛擬機分配策略參數和字段在Java源碼中定義順序的影響. HotSpot默認的分配策略為
longs
/doubles
、ints
、shorts
/chars
、bytes
/booleans
、oops
(Ordinary Object Pointers),?相同寬度的字段總是被分配到一起, 在滿足這個前提條件下, 在父類中定義的變量會出現在子類之前. 如果CompactFields
參數值為true
(默認), 那子類中較窄的變量也可能會插入到父類變量的空隙中. - 對齊填充部分并不是必然存在的, 僅起到占位符的作用, 原因是HotSpot自動內存管理系統要求對象起始地址必須是8字節的整數倍, 即對象的大小必須是8字節的整數倍.
對象定位
建立對象是為了使用對象, Java程序需要通過棧上的reference來操作堆上的具體對象. 主流的有句柄和直接指針兩種方式去定位和訪問堆上的對象:
-
句柄: Java堆中將會劃分出一塊內存來作為句柄池, reference中存儲對象的句柄地址, 而句柄中包含了對象實例數據與類型數據的具體各自的地址信息:?
-
直接指針(HotSpot使用): 該方式Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息, reference中存儲的直接就是對象地址:?
這兩種對象訪問方式各有優勢: 使用句柄來訪問的最大好處是reference中存儲的是穩定句柄地址, 在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不變. 而使用直接指針最大的好處就是速度更快, 它節省了一次指針定位的時間開銷,由于對象訪問非常頻繁, 因此這類開銷積小成多也是一項非常可觀的執行成本.
- 參考 & 拓展
- 深入理解Java虛擬機
- 實戰Java虛擬機
- HotSpot實戰
- 深入理解計算機系統
- JVM內幕:Java虛擬機詳解?(薦)
- Java內存管理:深入Java內存區域
- JAVA的內存模型及結構
- Memory Management in the Java HotSpot Virtual Machine
- Java HotSpot VM Options
- JVM實用參數(一)JVM類型以及編譯器模式
- HotSpot虛擬機對象探秘
- 局部變量表(對應我們常說的‘堆棧’中的‘棧’)存放了編譯期可知的各種基本數據類型(如boolean、int、double等) 、對象引用(reference : 不等同于對象本身, 可能是一個指向對象起始地址的指針, 也可能指向一個代表對象的句柄或其他與此對象相關的位置, 見下: HotSpot對象定位方式) 和?returnAddress類型(指向一條字節碼指令的地址). 其中