JVM和GC爷辱,是Java的底層虛擬機和垃圾回收器,理解JVM和GC,對于我們理解Java很有幫助饭弓。在面試的過程中双饥,虛擬機和垃圾回收器也是常常被問及的內(nèi)容,故而了解JVM結(jié)構(gòu)和GC工作機制是很有必要的弟断。在網(wǎng)上找了相關(guān)的資料后咏花,這邊參照文章的內(nèi)容對于JVM結(jié)構(gòu)、GC工作機制解釋的很詳細阀趴,因此轉(zhuǎn)載過來昏翰,一方面是自己記憶,另一方面是方便大家刘急。
本文中主要分為四個部分的內(nèi)容:
- JVM結(jié)構(gòu)
- 內(nèi)存分配
- 垃圾回收算法
- 垃圾收集器
下面我們對上面四個部分的內(nèi)容棚菊,逐個了解。
一叔汁、JVM結(jié)構(gòu)
根據(jù)《java虛擬機規(guī)范》規(guī)定统求,JVM的基本結(jié)構(gòu)一般如下圖所示:
從JVM基本結(jié)構(gòu)圖可知,JVM主要包括四個部分:
1. 類加載器(ClassLoader)
在JVM啟動時或者在類運行時將需要的class加載到JVM中据块。(Java文件運行解析流程圖表示了從java源文件到JVM的整個過程码邻,可配合理解。 關(guān)于類的加載機制另假,可以參考深入分析ClassLoader)
2. 執(zhí)行引擎
負責(zé)執(zhí)行class文件中包含的字節(jié)碼指令(執(zhí)行引擎的工作機制像屋,這里也不細說了,這里主要介紹JVM結(jié)構(gòu))浪谴;
3. 內(nèi)存區(qū)(也叫運行時數(shù)據(jù)區(qū))
在JVM運行的時候操作所分配的內(nèi)存區(qū)开睡。
運行時內(nèi)存區(qū)主要可以劃分為5個區(qū)域因苹,如圖:
-
方法區(qū)(Method Area):
用于存儲類結(jié)構(gòu)信息的地方苟耻,包括常量池、靜態(tài)變量扶檐、構(gòu)造函數(shù)等凶杖。雖然JVM規(guī)范把方法區(qū)描述為堆的一個邏輯部分, 但它卻有個別名non-heap(非堆)款筑,所以大家不要搞混淆了智蝠。方法區(qū)還包含一個運行時常量池。 -
java堆(Heap):
存儲java實例或者對象的地方奈梳。這塊是GC的主要區(qū)域(后面解釋)杈湾。從存儲的內(nèi)容我們可以很容易知道,方法區(qū)和堆是被所有java線程共享的攘须。 -
java棧(JVM Stack):
java椘嶙玻總是和線程關(guān)聯(lián)在一起,每當(dāng)創(chuàng)建一個線程時,JVM就會為這個線程創(chuàng)建一個對應(yīng)的java棧浮驳。在這個java棧中又會包含多個棧幀悍汛,每運行一個方法就創(chuàng)建一個棧幀,用于存儲局部變量表至会、操作棧离咐、方法返回值等。每一個方法從調(diào)用直至執(zhí)行完成的過程奉件,就對應(yīng)一個棧幀在java棧中入棧到出棧的過程宵蛀。所以java棧是現(xiàn)成私有的。 -
程序計數(shù)器(PC Register):
用于保存當(dāng)前線程執(zhí)行的內(nèi)存地址县貌。由于JVM程序是多線程執(zhí)行的(線程輪流切換)糖埋,所以為了保證線程切換回來后,還能恢復(fù)到原先狀態(tài)窃这,就需要一個獨立的計數(shù)器瞳别,記錄之前中斷的地方,可見程序計數(shù)器也是線程私有的杭攻。 -
本地方法棧(Native Method Stack):
和java棧的作用差不多祟敛,只不過是為JVM使用到的native方法服務(wù)的。
4. 本地方法接口
主要是調(diào)用C或C++實現(xiàn)的本地方法及返回結(jié)果兆解。
二馆铁、 內(nèi)存分配
在了解垃圾回收之前,得先了解JVM是怎么分配內(nèi)存的锅睛,然后識別哪些內(nèi)存是垃圾需要回收埠巨,最后才是用什么方式回收。
Java的內(nèi)存分配原理與C/C++不同现拒,C/C++每次申請內(nèi)存時都要malloc進行系統(tǒng)調(diào)用辣垒,而系統(tǒng)調(diào)用發(fā)生在內(nèi)核空間,每次都要中斷進行切換印蔬,這需要一定的開銷勋桶,而Java虛擬機是先一次性分配一塊較大的空間,然后每次new時都在該空間上進行分配和釋放侥猬,減少了系統(tǒng)調(diào)用的次數(shù)例驹,節(jié)省了一定的開銷,這有點類似于內(nèi)存池的概念退唠;二是有了這塊空間過后鹃锈,如何進行分配和回收就跟GC機制有關(guān)了。
java一般內(nèi)存申請有兩種:靜態(tài)內(nèi)存瞧预、動態(tài)內(nèi)存屎债。
靜態(tài)內(nèi)存
編譯時就能夠確定的內(nèi)存就是靜態(tài)內(nèi)存寨蹋,即內(nèi)存是固定的,系統(tǒng)一次性分配扔茅,比如int類型變量已旧;
動態(tài)內(nèi)存
動態(tài)內(nèi)存分配就是在程序執(zhí)行時才知道要分配的存儲空間大小,比如java對象的內(nèi)存空間召娜。
根據(jù)上面我們知道运褪,java棧、程序計數(shù)器玖瘸、本地方法棧都是線程私有的秸讹,線程生就生,線程滅就滅雅倒,棧中的棧幀隨著方法的結(jié)束也會撤銷璃诀,內(nèi)存自然就跟著回收了。所以這幾個區(qū)域的內(nèi)存分配與回收是確定的蔑匣,我們不需要管的劣欢。但是java堆和方法區(qū)則不一樣,我們只有在程序運行期間才知道會創(chuàng)建哪些對象裁良,所以這部分內(nèi)存的分配和回收都是動態(tài)的凿将。一般我們所說的垃圾回收也是針對的這一部分。
總之Stack的內(nèi)存管理是順序分配的价脾,而且定長牧抵,不存在內(nèi)存回收問題;而Heap 則是為java對象的實例隨機分配內(nèi)存侨把,不定長度犀变,所以存在內(nèi)存分配和回收的問題;
三秋柄、 垃圾檢測获枝、回收算法
垃圾收集器一般必須完成兩件事:檢測出垃圾;回收垃圾华匾。怎么檢測出垃圾映琳?一般有以下幾種方法:
引用計數(shù)法
給一個對象添加引用計數(shù)器,每當(dāng)有個地方引用它蜘拉,計數(shù)器就加1;引用失效就減1有鹿。
好了旭旭,問題來了,如果我有兩個對象A和B葱跋,互相引用持寄,除此之外源梭,沒有其他任何對象引用它們,實際上這兩個對象已經(jīng)無法訪問稍味,即是我們說的垃圾對象废麻。但是互相引用,計數(shù)不為0模庐,導(dǎo)致無法回收烛愧,所以還有另一種方法:
可達性分析算法:
以根集對象為起始點進行搜索,如果有對象不可達的話掂碱,即是垃圾對象怜姿。這里的根集一般包括java棧中引用的對象、方法區(qū)常量池中引用的對象疼燥、本地方法中引用的對象等沧卢。
總之,JVM在做垃圾回收的時候醉者,會檢查堆中的所有對象是否會被這些根集對象引用但狭,不能夠被引用的對象就會被垃圾收集器回收。一般回收算法也有如下幾種:
1.標記-清除(Mark-sweep)
算法和名字一樣,分為兩個階段:標記和清除胸蛛。標記所有需要回收的對象伯复,然后統(tǒng)一回收。這是最基礎(chǔ)的算法息罗,后續(xù)的收集算法都是基于這個算法擴展的。
不足:效率低才沧;標記清除之后會產(chǎn)生大量碎片迈喉。效果圖如下:
2.復(fù)制(Copying)
此算法把內(nèi)存空間劃為兩個相等的區(qū)域,每次只使用其中一個區(qū)域温圆。垃圾回收時挨摸,遍歷當(dāng)前使用區(qū)域,把正在使用中的對象復(fù)制到另外一個區(qū)域中岁歉。此算法每次只處理正在使用中的對象得运,因此復(fù)制成本比較小,同時復(fù)制過去以后還能進行相應(yīng)的內(nèi)存整理锅移,不會出現(xiàn)“碎片”問題熔掺。當(dāng)然,此算法的缺點也是很明顯的非剃,就是需要兩倍內(nèi)存空間置逻。效果圖如下:
3. 標記-整理(Mark-Compact)
此算法結(jié)合了“標記-清除”和“復(fù)制”兩個算法的優(yōu)點。也是分兩階段备绽,第一階段從根節(jié)點開始標記所有被引用對象券坞,第二階段遍歷整個堆鬓催,把清除未標記對象并且把存活對象“壓縮”到堆的其中一塊,按順序排放恨锚。此算法避免了“標記-清除”的碎片問題宇驾,同時也避免了“復(fù)制”算法的空間問題。效果圖如下:
(以上圖文摘自JVM調(diào)優(yōu)總結(jié)(三)-基本垃圾回收算法)
4. 分代收集算法
這是當(dāng)前商業(yè)虛擬機常用的垃圾收集算法猴伶。分代的垃圾回收策略课舍,是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此蜗顽,不同生命周期的對象可以采取不同的收集方式布卡,以便提高回收效率。
為什么要運用分代垃圾回收策略雇盖?在java程序運行的過程中忿等,會產(chǎn)生大量的對象,因每個對象所能承擔(dān)的職責(zé)不同所具有的功能不同所以也有著不一樣的生命周期崔挖,有的對象生命周期較長贸街,比如Http請求中的Session對象,線程狸相,Socket連接等薛匪;有的對象生命周期較短,比如String對象脓鹃,由于其不變類的特性逸尖,有的在使用一次后即可回收。試想瘸右,在不進行對象存活時間區(qū)分的情況下娇跟,每次垃圾回收都是對整個堆空間進行回收,那么消耗的時間相對會很長太颤,而且對于存活時間較長的對象進行的掃描工作等都是徒勞苞俘。因此就需要引入分治的思想,所謂分治的思想就是因地制宜龄章,將對象進行代的劃分吃谣,把不同生命周期的對象放在不同的代上使用不同的垃圾回收方式。
如何劃分做裙?將對象按其生命周期的不同劃分成:年輕代(Young Generation)岗憋、年老代(Old Generation)、持久代(Permanent Generation)菇用。其中持久代主要存放的是類信息澜驮,所以與java對象的回收關(guān)系不大,與回收息息相關(guān)的是年輕代和年老代惋鸥。這里有個比喻很形象
“假設(shè)你是一個普通的 Java 對象杂穷,你出生在 Eden 區(qū),在 Eden 區(qū)有許多和你差不多的小兄弟卦绣、小姐妹耐量,可以把 Eden 區(qū)當(dāng)成幼兒園,在這個幼兒園里大家玩了很長時間滤港。Eden 區(qū)不能無休止地放你們在里面廊蜒,所以當(dāng)年紀稍大,你就要被送到學(xué)校去上學(xué)溅漾,這里假設(shè)從小學(xué)到高中都稱為 Survivor 區(qū)山叮。開始的時候你在 Survivor 區(qū)里面劃分出來的的“From”區(qū),讀到高年級了添履,就進了 Survivor 區(qū)的“To”區(qū)屁倔,中間由于學(xué)習(xí)成績不穩(wěn)定,還經(jīng)常來回折騰暮胧。直到你 18 歲的時候锐借,高中畢業(yè)了,該去社會上闖闖了往衷。于是你就去了年老代钞翔,年老代里面人也很多。在年老代里席舍,你生活了 20 年 (每次 GC 加一歲)布轿,最后壽終正寢,被 GC 回收来颤。有一點沒有提汰扭,你在年老代遇到了一個同學(xué),他的名字叫愛德華 (慕光之城里的帥哥吸血鬼)脚曾,他以及他的家族永遠不會死东且,那么他們就生活在永生代”炯ィ”
具體區(qū)域可以通過VisualVM中的VisaulGC插件查看珊泳,如圖(openjdk 1.7):
年輕代:是所有新對象產(chǎn)生的地方。年輕代被分為3個部分——Enden區(qū)和兩個Survivor區(qū)(From和to)當(dāng)Eden區(qū)被對象填滿時拷沸,就會執(zhí)行Minor GC色查。并把所有存活下來的對象轉(zhuǎn)移到其中一個survivor區(qū)(假設(shè)為from區(qū))。Minor GC同樣會檢查存活下來的對象撞芍,并把它們轉(zhuǎn)移到另一個survivor區(qū)(假設(shè)為to區(qū))秧了。這樣在一段時間內(nèi),總會有一個空的survivor區(qū)序无。經(jīng)過多次GC周期后验毡,仍然存活下來的對象會被轉(zhuǎn)移到年老代內(nèi)存空間衡创。通常這是在年輕代有資格提升到年老代前通過設(shè)定年齡閾值來完成的。需要注意晶通,Survivor的兩個區(qū)是對稱的璃氢,沒先后關(guān)系,from和to是相對的狮辽。
年老代:在年輕代中經(jīng)歷了N次回收后仍然沒有被清除的對象一也,就會被放到年老代中,可以說他們都是久經(jīng)沙場而不亡的一代喉脖,都是生命周期較長的對象椰苟。對于年老代和永久代,就不能再采用像年輕代中那樣搬移騰挪的回收算法树叽,因為那些對于這些回收戰(zhàn)場上的老兵來說是小兒科舆蝴。通常會在老年代內(nèi)存被占滿時將會觸發(fā)Full GC,回收整個堆內(nèi)存。
持久代:用于存放靜態(tài)文件菱皆,比如java類须误、方法等。持久代對垃圾回收沒有顯著的影響仇轻。
分代回收的效果圖如下:
我這里之所以最后講分代京痢,是因為分代里涉及了前面幾種算法。
年輕代:涉及了復(fù)制算法篷店;
年老代:涉及了“標記-整理(Mark-Sweep)”的算法祭椰。
四、 垃圾收集器
垃圾收集算法是內(nèi)存回收的方法論疲陕,而實現(xiàn)這些方法論的則是垃圾收集器方淤。不同廠商不同版本JVM所提供的垃圾收集器可能不同,這里參照《深入理解Java虛擬機》說的是JDK1.7版本Hotspot虛擬機蹄殃,關(guān)于垃圾收集器有篇博文總結(jié)的不錯携茂,我就不說了,詳見:Java虛擬機學(xué)習(xí) - 垃圾收集器)
總結(jié)
雖然我不認為學(xué)習(xí)java必須去了解Java底層的實現(xiàn)诅岩,但是我想如果你更加理解JVM和GC的話讳苦,你就會更加理解Java,在以后的學(xué)習(xí)和工作中絕對受益匪淺吩谦。畢竟我們的目標不是刷墻工鸳谜,不是搬運工,而是開發(fā)攻城獅笆酵ⅰ咐扭!