這個(gè)題很難回答,你的知識儲備要比較強(qiáng)了,他可擴(kuò)展性太強(qiáng)了,咱們開始講解 :
JVM內(nèi)存管理分為兩部分:內(nèi)存分配、內(nèi)存回收、內(nèi)存回收經(jīng)常也被叫做垃圾回收。
很多人迷惑一個(gè)問題,既然Java采用自動(dòng)內(nèi)存管理,程序員不用關(guān)心內(nèi)存管理的細(xì)節(jié),那么為什么我們?nèi)匀恍枰私釰ava內(nèi)存管理的內(nèi)幕?
1.了解Java內(nèi)存管理的細(xì)節(jié),有助于程序員編寫出性能更好的程序。
比如,在新的線程創(chuàng)建時(shí),JVM會為每個(gè)線程創(chuàng)建一個(gè)專屬的棧(stack),其棧是先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),這種方式的特點(diǎn),讓程序員編程時(shí),必須特別注意遞歸方法要盡量少使用,另外棧的大小也有一定的限制,如果過多的遞歸,容易導(dǎo)致stack overflow。
2.了解Java內(nèi)存管理的細(xì)節(jié),一旦內(nèi)存管理出現(xiàn)問題,有助于找到問題的根本原因所在。
3.了解Java內(nèi)存管理的內(nèi)幕,有助于優(yōu)化JVM,使自己的應(yīng)用獲得最好性能體驗(yàn)。
內(nèi)存---Java中哪些組件用到內(nèi)存---內(nèi)存分配機(jī)制---內(nèi)存回收機(jī)制
內(nèi)存:物理內(nèi)存和虛擬內(nèi)存,物理內(nèi)存就是常說的RAM(隨機(jī)存儲器),操作系統(tǒng)作為我們管理計(jì)算機(jī)物理內(nèi)存的接口,我們通常都是通過調(diào)用計(jì)算機(jī)操作系統(tǒng)來訪問內(nèi)存的,在Java中,甚至不需要寫和內(nèi)存相關(guān)的代碼。通常操作系統(tǒng)管理內(nèi)存申請空間是按照進(jìn)程來管理的,每個(gè)進(jìn)程都有一段獨(dú)立的空間,互不重合,互不訪問。這里所說的內(nèi)存空間的獨(dú)立是指邏輯上的獨(dú)立,由操作系統(tǒng)來保證的。虛擬內(nèi)存的出現(xiàn)使得多個(gè)進(jìn)程可以同時(shí)運(yùn)行時(shí)共享物理內(nèi)存,空間上共享,邏輯仍然互不訪問。虛擬內(nèi)存提高了內(nèi)存利用率,擴(kuò)展了內(nèi)存地址空間。
內(nèi)核空間和用戶空間:通常一個(gè)4GB的物理內(nèi)存地址空間并不能完全被使用,因?yàn)樗粍澐譃閮刹糠郑簝?nèi)核空間和用戶空間【內(nèi)核空間】主要是指操作系統(tǒng)運(yùn)行時(shí)用于程序調(diào)度、虛擬內(nèi)存的使用或者連接硬件資源的程序邏輯。【用戶空間】是用戶運(yùn)行程序能夠申請使用的空間。
為什么這么劃分呢?
1)有效抵御惡意用戶的窺探,也能防止質(zhì)量低劣的用戶程序的侵害,從而使系統(tǒng)運(yùn)行得更穩(wěn)定可靠。
2)用戶空間與內(nèi)核空間的權(quán)限不同,內(nèi)核空間擁有所有硬件設(shè)備的權(quán)限,用戶空間只有普通硬件的權(quán)限;兩者隔離可以防止用戶程序直接訪問硬件資源。
但是,每一次系統(tǒng)調(diào)用都會在兩個(gè)內(nèi)存空間之間切換,通過網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)首先被接收到內(nèi)核空間,然后再從內(nèi)核空間復(fù)制到用戶空間供用戶使用。這樣比較費(fèi)時(shí),雖然保證了程序運(yùn)行的安全性和穩(wěn)定性,但同時(shí)也犧牲了一部分效率。(后來出現(xiàn)了一些列優(yōu)化技術(shù),如Linux提供的sendfile文件傳輸方式)
另外:Windows32位操作系統(tǒng)【內(nèi)核空間:用戶空間=1:1】、Linux32位操作系統(tǒng)【內(nèi)核空間:用戶空間=1:3】
Java中的內(nèi)存就是從物理內(nèi)存中申請下來的內(nèi)存,它怎么被劃分的呢?
Java內(nèi)存模型:一個(gè)Java程序的具體執(zhí)行流程如下:
首先Java源代碼文件(.java后綴)會被Java編譯器編譯為字節(jié)碼文件(.class后綴),然后由JVM中的類加載器加載各個(gè)類的字節(jié)碼文件,加載完畢之后,交由JVM執(zhí)行引擎執(zhí)行。在整個(gè)程序執(zhí)行過程中,JVM會用一段空間來存儲程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被稱作為Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū)),也就是我們常說的JVM內(nèi)存。
我們常說的Java內(nèi)存管理就是指這塊區(qū)域的內(nèi)存分配和回收,那么,這塊兒區(qū)域具體是怎么劃分的呢?
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,運(yùn)行時(shí)數(shù)據(jù)區(qū)通常包括這幾個(gè)部分:
程序計(jì)數(shù)器(ProgramCounter Register)
Java棧(VM Stack)
本地方法棧(Native MethodStack)
方法區(qū)(Method Area)
堆(Heap)
Java堆和方法區(qū)是所有線程共享(所有執(zhí)行引擎可訪問);
【Java堆】用于存儲Java對象,每個(gè)Java對象都是這個(gè)對象類的副本,會復(fù)制包含繼承自它父類的所有非靜態(tài)屬性。
【方法區(qū)】用于存儲類結(jié)構(gòu)信息,class文件加載進(jìn)JVM時(shí)會被解析成JVM識別的幾個(gè)部分分別存儲在不同的數(shù)據(jù)結(jié)構(gòu)中:常量池、域、方法數(shù)據(jù)、方法體、構(gòu)造函數(shù),包括類中的方法、實(shí)例初始化、接口初始化等。
方法區(qū)被JVM的GC回收器管理,但是比較穩(wěn)定,并沒有那么頻繁的被GC回收。
java棧和PC寄存器(程序計(jì)數(shù)器)是線程私有,每個(gè)執(zhí)行引擎啟動(dòng)時(shí)都會創(chuàng)建自己的java棧和PC寄存器;
【Java棧】和線程關(guān)聯(lián),每個(gè)線程創(chuàng)建的時(shí)候,JVM都會為他分配一個(gè)對應(yīng)的Java棧,這個(gè)棧含有多個(gè)棧幀;棧幀則是個(gè)方法關(guān)聯(lián),每個(gè)方法的運(yùn)行都會創(chuàng)建一個(gè)自己的棧幀,含有內(nèi)存變量,操作棧、方法返回值。(用于存儲方法參數(shù)、局部變量、方法返回值和運(yùn)算中間結(jié)果)
【PC寄存器】則用于記錄下一條要執(zhí)行的字節(jié)碼指令地址和被中斷地址。如果方法是 native的,程序計(jì)數(shù)器的值不會被定義為空。
【本地方法棧】是為JVM運(yùn)行Native方法(本地方法:非java語言編寫的方法,被編譯成和處理器相關(guān)的代碼)準(zhǔn)備的空間,類似于Java棧。
【運(yùn)行時(shí)常量池】關(guān)于這個(gè)東西要明白三個(gè)概念:
常量池(Constant Pool):常量池?cái)?shù)據(jù)編譯期被確定,是Class文件中的一部分。存儲了類、方法、接口等中的常量,當(dāng)然也包括字符串常量。
字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲編譯期類中產(chǎn)生的字符串類型數(shù)據(jù)。
運(yùn)行時(shí)常量池(Runtime Constant Pool):方法區(qū)的一部分,所有線程共享。虛擬機(jī)加載Class后把常量池中的數(shù)據(jù)放入到運(yùn)行時(shí)常量池。
Java中哪些組件用到內(nèi)存
Java堆:Java堆用于存儲Java對象,在JVM啟動(dòng)時(shí)就一次性申請到固定大小的空間,所以,一旦分配,大小不變。
內(nèi)存空間管理:JVM
對象創(chuàng)建:Java應(yīng)用程序
對象所占空間釋放:垃圾收集器
線程:JVM運(yùn)行實(shí)際程序的實(shí)體就是線程,每個(gè)線程創(chuàng)建的時(shí)候JVM都為它創(chuàng)建了私有的堆棧和程序計(jì)數(shù)器(或者叫做PC寄存器);很多應(yīng)用程序是根據(jù)CPU的核數(shù)來分配創(chuàng)建的線程數(shù)。
類和類加載器:Java中的類和類加載器同樣需要存儲空間,被存儲在永久代(PermGen區(qū))當(dāng)中。
JVM加載類方式:按需加載,只加載那些你在程序中明確使用到的類,通常只加載一次,如果一直重復(fù)加載,可能會導(dǎo)致內(nèi)存泄露,所以也要注意對PernGen區(qū)失效類的卸載內(nèi)存回收問題。
通常PernGen區(qū)滿足內(nèi)存回收的條件為:
1) 堆中沒有對該類加載器的引用;(java.lang.ClassLoader對象)
2) 堆中沒有對類加載器加載的類的引用;(java.lang.Class對象)
3) 該類加載器加載的類的所有實(shí)例化的對象不再存活。
NIO:使用java.nio.ByteBuffer.allocateDirect()方法分配內(nèi)存,每次分配內(nèi)存都會調(diào)用操作系統(tǒng)函數(shù)os::malloc(),所以,分配的內(nèi)存是本機(jī)的內(nèi)存而不是Java堆上的內(nèi)存;
另外利用該方法產(chǎn)生的數(shù)據(jù)和網(wǎng)絡(luò)、磁盤發(fā)生交互的時(shí)候都是在內(nèi)核空間發(fā)生的,不需要復(fù)制到用戶空間Java內(nèi)存中,這種技術(shù)避免了Java堆和本機(jī)堆之間的數(shù)據(jù)復(fù)制;但是利用該方法生成的數(shù)據(jù)會作為Java堆GC的一部分來自動(dòng)清理本機(jī)緩沖區(qū)。
JNI:技術(shù)使本機(jī)代碼可調(diào)用java代碼,Java代碼的運(yùn)行本身也依賴于JNI代碼來實(shí)現(xiàn)類庫功能,所以JNI也增加內(nèi)存占用。
JVM內(nèi)存分配與回收:內(nèi)存分配、通常的內(nèi)存分配策略、操作系統(tǒng)中內(nèi)存分配策略通常分為三類:
【靜態(tài)內(nèi)存分配】編譯時(shí)就分配了固定的內(nèi)存空間(編譯器確定所需空間大小),不允許有可變數(shù)據(jù)和遞歸嵌套等情況,這樣難以計(jì)算具體空間;
【棧內(nèi)存分配】在程序運(yùn)行時(shí)進(jìn)入一個(gè)程序模塊(程序入口處確定空間大小)知道一個(gè)程序模塊分配所需數(shù)據(jù)區(qū)大小并為之分配內(nèi)存。
【堆內(nèi)存分配】在程序運(yùn)行到相應(yīng)代碼是才會知道所需空間大小。(運(yùn)行時(shí)確定空間大小)
很明顯,三種分配策略中,堆內(nèi)存分配策略最自由,但是效率也是比較差的。
Java內(nèi)存分配: 在Java程序運(yùn)行過程中,JVM定義了各種區(qū)域用于存儲運(yùn)行時(shí)數(shù)據(jù)。其中的有些數(shù)據(jù)區(qū)域在JVM啟動(dòng)時(shí)創(chuàng)建,并只在JVM退出時(shí)銷毀;其它的數(shù)據(jù)區(qū)域與每個(gè)線程相關(guān)。這些數(shù)據(jù)區(qū)域,在線程創(chuàng)建時(shí)創(chuàng)建,在線程退出時(shí)銷毀。
棧和線程:JVM是基于棧的虛擬機(jī),為每個(gè)新創(chuàng)建的線程都分配一個(gè)棧,也就是說一個(gè)Java程序來說,它的運(yùn)行就是通過對棧的操作來完成的。棧以幀為單位保存線程的狀態(tài)。JVM對棧只進(jìn)行兩種操作:以幀為單位的壓棧和出棧操作。
某個(gè)線程正在執(zhí)行的方法稱為此線程的當(dāng)前方法,當(dāng)前方法使用的幀稱為當(dāng)前幀。當(dāng)線程激活一個(gè)Java方法,JVM就會在線程的 Java堆棧里新壓入一個(gè)幀。這個(gè)幀自然成為了當(dāng)前幀.在此方法執(zhí)行期間,這個(gè)幀將用來保存參數(shù),局部變量,中間計(jì)算過程和其他數(shù)據(jù)。這個(gè)幀在這里和編譯原理中的活動(dòng)紀(jì)錄的概念是差不多的。
從Java的這種分配機(jī)制來看,可以這樣理解:棧(Stack)是操作系統(tǒng)在建立某個(gè)進(jìn)程時(shí)或者線程(在支持多線程的操作系統(tǒng)中是線程)為這個(gè)線程建立的存儲區(qū)域,該區(qū)域具有先進(jìn)后出的特性。
堆和棧的區(qū)別:棧(stack)與堆(heap)都是Java用來在Ram中存放數(shù)據(jù)的地方 。與C++不同,Java自動(dòng)管理?xiàng):投眩绦騿T不能直接地設(shè)置棧或堆。
棧的優(yōu)勢:是存取速度比堆要快 ,僅次于直接位于CPU中的寄存器,缺點(diǎn)是,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。另外,棧數(shù)據(jù)可 以共享,詳見第4點(diǎn)。
堆的優(yōu)勢:是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動(dòng)收走這些不再使用的數(shù)據(jù)。但缺點(diǎn)是,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,存取速度較慢。
兩者存儲數(shù)據(jù)類型不同:堆是一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),存放通過new、newayyray.anewarray和mulitanewarray等指令建立的對象,無需代碼顯式的釋放;棧中存放一些基本類型的變量數(shù)據(jù)和對象句柄(引用);
Java中所有對象的存儲空間都是在堆中分配的,但是這個(gè)對象的引用卻是在堆棧中分配;也就是說在建立一個(gè)對象時(shí)從兩個(gè)地方都分配內(nèi)存,在堆中分配的內(nèi)存實(shí)際建立這個(gè)對象,而在堆棧中分配的內(nèi)存只是一個(gè)指向這個(gè)堆對象的指針(引用)。
JVM內(nèi)存回收:幾個(gè)問題要搞清楚
問題一:什么叫垃圾回收機(jī)制?
垃圾回收是一種動(dòng)態(tài)存儲管理技術(shù),它自動(dòng)地釋放不再被程序引用的對象,按照特定的垃圾收集算法來實(shí)現(xiàn)資源自動(dòng)回收的功能。當(dāng)一個(gè)對象不再被引用的時(shí)候,內(nèi)存回收它占領(lǐng)的空間,以便空間被后來的新對象使用,以免造成內(nèi)存泄露。
問題二:java的垃圾回收有什么特點(diǎn)?
Java語言不允許程序員直接控制內(nèi)存空間的使用。內(nèi)存空間的分配和回收都是由JRE負(fù)責(zé)在后臺自動(dòng)進(jìn)行的,尤其是無用內(nèi)存空間的回收操作(garbagecollection,也稱垃圾回收),只能由運(yùn)行環(huán)境提供的一個(gè)超級線程進(jìn)行監(jiān)測和控制。
問題三:垃圾回收器什么時(shí)候會運(yùn)行?
一般是在CPU空閑或空間不足時(shí)自動(dòng)進(jìn)行垃圾回收,而程序員無法精確控制垃圾回收的時(shí)機(jī)和順序等。
問題四:什么樣的對象符合垃圾回收條件?
當(dāng)沒有任何獲得線程能訪問一個(gè)對象時(shí),該對象就符合垃圾回收條件。
問題五:垃圾回收器是怎樣工作的?
垃圾回收器如發(fā)現(xiàn)一個(gè)對象不能被任何活線程訪問時(shí),他將認(rèn)為該對象符合刪除條件,就將其加入回收隊(duì)列,但不是立即銷毀對象,何時(shí)銷毀并釋放內(nèi)存是無法預(yù)知的。垃圾回收不能強(qiáng)制執(zhí)行,然而java提供了一些方法(如:System.gc()方法),允許你請求JVM執(zhí)行垃圾回收,而不是要求,虛擬機(jī)會盡其所能滿足請求,但是不能保證JVM從內(nèi)存中刪除所有不用的對象。
問題六:一個(gè)java程序能夠耗盡內(nèi)存嗎?
可以。垃圾收集系統(tǒng)嘗試在對象不被使用時(shí)把他們從內(nèi)存中刪除。然而,如果保持太多活動(dòng)對象,系統(tǒng)則可能會耗盡內(nèi)存。垃圾回收器不能保證有足夠的內(nèi)存,只能保證可用內(nèi)存盡可能的得到高效的管理。
問題七:程序中的數(shù)據(jù)類型不一樣存儲地方也不一樣,原生數(shù)據(jù)類型存儲在java棧中,方法執(zhí)行結(jié)束就會消失;對象類型存儲在Java堆中,可以被共享,不一定隨著方法執(zhí)行結(jié)束而消失。
問題八:如何檢測垃圾?(垃圾檢測機(jī)制)
垃圾收集器的兩個(gè)任務(wù):正確檢測出垃圾對象和釋放垃圾對象占用的內(nèi)存空間,而前者是關(guān)鍵所在。
垃圾收集器有一個(gè)根對象集合,包含的元素:1)方法中局部變量的引用;2)Java操作棧中的對象引用;3)常量池中的對象引用;4)本地方法持有的對象引用;5)類的class對象。
JVM在垃圾回收的時(shí)候會檢查堆中的所有對象是否會被根對象直接或間接的引用,能夠被根對象到達(dá)的叫做活動(dòng)對象,否則叫做非活動(dòng)對象可以被回收。內(nèi)存回收- gc原理: jvm內(nèi)存回收采用的是基于分代的垃圾收集算法
Sun的JVM Generational Collecting(垃圾回收)原理是這樣的:把對象分為年青代(Young)、年老代(Tenured)、持久代(Perm),對不同生命周期的對象使用不同的算法。(基于對象生命周期分析)
【設(shè)計(jì)思路】:把對象按照壽命長短來分組,分為年輕代和年老代,新創(chuàng)建的對象被分在年輕代,如果對象經(jīng)過幾次回收后仍然存活,那么再把這個(gè)對象劃分到年老代。年老代的收集頻度沒有那么頻繁,這樣就減少了每次垃圾收集時(shí)所需要的掃描的對象和數(shù)量,從而提高垃圾回收效率。
Young(年輕代)
年輕代分三個(gè)區(qū)。一個(gè)Eden區(qū),兩個(gè)Survivor區(qū)。大部分對象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時(shí),還存活的對象將被復(fù)制到Survivor區(qū)(兩個(gè)中的一個(gè)),當(dāng)這個(gè)Survivor區(qū)滿時(shí),此區(qū)的存活對象將被復(fù)制到另外一個(gè)Survivor區(qū),當(dāng)這個(gè)Survivor去也滿了的時(shí)候,從第一個(gè)Survivor區(qū)復(fù)制過來的并且此時(shí)還存活的對象,將被復(fù)制年老區(qū)(Tenured,需要注意,Survivor的兩個(gè)區(qū)是對稱的,沒先后關(guān)系,所以同一個(gè)區(qū)中可能同時(shí)存在從Eden和Survivor區(qū)復(fù)制過來的對象,而復(fù)制到年老區(qū)的只有從第一個(gè)Survivor過來的對象,而且,Survivor區(qū)總有一個(gè)是空的。
Tenured(年老代)
年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象;如果Tenured區(qū)(old區(qū))也滿了,就會觸發(fā)Full GC回收整個(gè)堆內(nèi)存。
Perm(持久代)
用于存放類的Class文件或靜態(tài)文件,如Java類、方法等,垃圾回收是由FullGC觸發(fā)的。持久代對垃圾回收沒有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如hibernate等,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來存放這些運(yùn)行過程中新增的類。持久代大小通過-XX:MaxPermSize=進(jìn)行設(shè)置。
舉個(gè)例子:當(dāng)在程序中生成對象時(shí),正常對象會在年輕代中分配空間,如果是過大的對象也可能會直接在年老代生成(據(jù)觀測在運(yùn)行某程序時(shí)候每次會生成一個(gè)十兆的空間用收發(fā)消息,這部分內(nèi)存就會直接在年老代分配)。年輕代在空間被分配完的時(shí)候就會發(fā)起內(nèi)存回收,大部分內(nèi)存會被回收,一部分幸存的內(nèi)存會被拷貝至Survivor的from區(qū),經(jīng)過多次回收以后如果from區(qū)內(nèi)存也分配完畢,就會也發(fā)生內(nèi)存回收然后將剩余的對象拷貝至to區(qū)。等到to區(qū)也滿的時(shí)候,就會再次發(fā)生內(nèi)存回收然后把幸存的對象拷貝至年老區(qū)。
通常我們說的JVM內(nèi)存回收總是在指堆內(nèi)存回收,確實(shí)只有堆中的內(nèi)容是動(dòng)態(tài)申請分配的,所以以上對象的年輕代和年老代都是指的JVM的Heap空間,而持久代則是之前提到的MethodArea,不屬于Heap。
關(guān)于JVM內(nèi)存管理我們需要注意的幾個(gè)地方:
1、程序中的無用對象、中間對象置為null,可加快內(nèi)存回收。
2、對象池技術(shù)如果生成的對象是可重用的對象,只是其中的屬性不同時(shí),可以考慮采用對象池減少對象的生成。如果對象池中有空閑的對象就取出使用,沒有則生成新的對象,提高對象復(fù)用率。
3、JVM性能調(diào)優(yōu)通過配置JVM的參數(shù)來提高垃圾回收的速度,如果在沒有出現(xiàn)內(nèi)存泄露且上面兩種辦法都不能保證JVM內(nèi)存回收時(shí),可以考慮采用JVM調(diào)優(yōu) 的方式來解決,不過一定要經(jīng)過實(shí)體機(jī)的長期測試,因?yàn)椴煌膮?shù)可能引起不同的效果。如-Xnoclassgc參數(shù)等。
jvm的垃圾回收算法: Java中,垃圾回收(GC,Garbage Collection)的對象是Java堆和方法區(qū)(即永久區(qū)或持久區(qū))
垃圾指的是在系統(tǒng)運(yùn)行過程當(dāng)中所產(chǎn)生的一些無用的對象,這些對象占據(jù)著一定的內(nèi)存空間,如果長期不被釋放,可能導(dǎo)致OOM。后臺專門有一個(gè)專門用于垃圾回收的線程來進(jìn)行監(jiān)控、掃描,自動(dòng)將一些無用的內(nèi)存進(jìn)行釋放,這就是垃圾收集的一個(gè)基本思想,目的在于防止由程序猿引入的人為的內(nèi)存泄露。
現(xiàn)代java虛擬機(jī)常用的垃圾回收算法有三種,分別是標(biāo)記-清除算法、復(fù)制算法、標(biāo)記-整理算法
標(biāo)記-清除算法
(1)概念:標(biāo)記-清除算法是現(xiàn)代垃圾回收算法的思想基礎(chǔ)。它將垃圾回收分為兩個(gè)階段:標(biāo)記階段和清除階段。
標(biāo)記階段:首先,通過根節(jié)點(diǎn),標(biāo)記所有從根節(jié)點(diǎn)開始的可達(dá)對象。未被標(biāo)記的對象就是未被引用的垃圾對象;
清除階段:然后,清除所有未被標(biāo)記的對象。
(2)算法詳解原理:當(dāng)堆中的可用有效內(nèi)存空間(available memory)被耗盡的時(shí)候,就暫停整個(gè)程序(也被成為stop the world),然后進(jìn)行標(biāo)記和清除兩項(xiàng)工作,然后讓程序恢復(fù)運(yùn)行。
標(biāo)記:標(biāo)記的過程其實(shí)就是,遍歷所有的GC Roots,然后將所有GC Roots可達(dá)的對象標(biāo)記為存活的對象。
清除:清除的過程將遍歷堆中所有的對象,將沒有標(biāo)記的對象全部清除掉。
疑問:為什么非要停止程序的運(yùn)行呢?
答:不難理解,假設(shè)程序與GC線程一起運(yùn)行,當(dāng)對象A處于標(biāo)記階段,被標(biāo)記為垃圾對象后,試想此時(shí)新new了一個(gè)對象B,且對象A可達(dá)B。但是由于此時(shí)A對象已經(jīng)標(biāo)記結(jié)束,B對象錯(cuò)過了標(biāo)記階段。因此當(dāng)接下來清除階段會被,新對象B會隨著A被標(biāo)記被清除掉,變?yōu)閚ull,這樣就亂套了。如此一來,要想正常清除垃圾資源,GC線程必須要暫停程序。
(3)標(biāo)記-清除算法的缺點(diǎn):首先,它的缺點(diǎn)就是效率比較低(遞歸與全堆對象遍歷),暫停程序stop the world的時(shí)間比較長。(尤其對于交互式的應(yīng)用程序來說簡直是無法接受。試想一下,如果你玩一個(gè)網(wǎng)站,這個(gè)網(wǎng)站一個(gè)小時(shí)就掛五分鐘,你還玩嗎?)
第二則是這種方式清理出來的空閑內(nèi)存不連續(xù),這點(diǎn)不難理解,我們的死亡對象都是隨即的出現(xiàn)在內(nèi)存的各個(gè)角落的,現(xiàn)在把它們清除之后,內(nèi)存的布局自然會亂七八糟。而為了應(yīng)付這一點(diǎn),JVM就不得不維持一個(gè)內(nèi)存的空閑列表,這又是一種開銷。而且在分配數(shù)組對象的時(shí)候,尋找連續(xù)的內(nèi)存空間會不太好找。
2 復(fù)制算法(適用于年輕代GC)
(1)概念:內(nèi)存空間分為兩塊,每次只使用其中一塊,在垃圾回收時(shí),將正在使用的內(nèi)存中的存活對象復(fù)制到未使用的內(nèi)存塊中,之后,清除正在使用的內(nèi)存塊中的所有對象,交換兩個(gè)內(nèi)存的角色,完成垃圾回收。
與標(biāo)記-清除算法相比,復(fù)制算法是一種相對高效的回收方法,且內(nèi)存連續(xù)。
不適用于存活對象較多的場合,如老年代(復(fù)制算法適合做新生代的GC)
(2)優(yōu)點(diǎn):實(shí)現(xiàn)簡單,運(yùn)行高效,內(nèi)存連續(xù)。每次只要一動(dòng)指針,就可聯(lián)系分配內(nèi)存存放復(fù)制過來的對象。
缺點(diǎn):空間浪費(fèi),只用了一半內(nèi)存,所以,要想用復(fù)制算法,最起碼對象的存活率要非常低才行,而且最重要的是要克服50%內(nèi)存的浪費(fèi)。
針對這種缺點(diǎn),這種算法比較適合,且已經(jīng)用于年輕代垃圾回收,新生代中的對象98%都是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊比較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當(dāng)回收時(shí),將Eden和Survivor中還存活著的對象一次性地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1,也就是說,每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80%+10%),只有10%的空間會被浪費(fèi)。
當(dāng)然,98%的對象可回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對象存活,當(dāng)Survivor空間不夠用時(shí),需要依賴于老年代進(jìn)行分配擔(dān)保,所以大對象直接進(jìn)入老年代。
標(biāo)記-整理算法(適用于年老代的GC)
(1)引入:如果在對象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會變低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選中這種算法。
(2)概念: 適合用于存活對象較多的場合,如老年代。它在標(biāo)記-清除算法的基礎(chǔ)上做了一些優(yōu)化。和標(biāo)記-清除算法一樣,標(biāo)記-壓縮算法也首先需要從根節(jié)點(diǎn)開始,對所有可達(dá)對象做一次標(biāo)記;但之后,它并不簡單的清理未標(biāo)記的對象,而是將所有的存活對象壓縮到內(nèi)存的一端;之后,清理邊界外所有的空間。
標(biāo)記:它的第一個(gè)階段與標(biāo)記/清除算法是一模一樣的,均是遍歷GC Roots,然后將存活的對象標(biāo)記。
整理:移動(dòng)所有存活的對象,按照內(nèi)存地址次序依次排列,然后將末端內(nèi)存地址以后的內(nèi)存全部回收。因此,第二階段才稱為整理階段。(JVM只需要持有一個(gè)內(nèi)存的起始地址即可,這比維護(hù)一個(gè)空閑列表顯然少了許多開銷)
(3)優(yōu)點(diǎn):標(biāo)記/整理算法不僅可以彌補(bǔ)標(biāo)記/清除算法當(dāng)中,內(nèi)存區(qū)域分散的缺點(diǎn),也消除了復(fù)制算法當(dāng)中,內(nèi)存減半的高額代價(jià)。
缺點(diǎn):就是效率也不高。不僅要標(biāo)記所有存活對象,還要整理所有存活對象的引用地址。從效率上來說,要低于復(fù)制算法。
標(biāo)記-清除算法、復(fù)制算法、標(biāo)記整理算法的總結(jié)
三個(gè)算法都基于根搜索算法去判斷一個(gè)對象是否應(yīng)該被回收,而支撐根搜索算法可以正常工作的理論依據(jù),就是語法中變量作用域的相關(guān)內(nèi)容。因此,要想防止內(nèi)存泄露,最根本的辦法就是掌握好變量作用域,而不應(yīng)該使用C/C++式內(nèi)存管理方式。
在GC線程開啟時(shí),或者說GC過程開始時(shí),它們都要暫停應(yīng)用程序(stop the world)。
它們的區(qū)別如下:
(1)效率:復(fù)制算法 > 標(biāo)記/整理算法 > 標(biāo)記/清除算法
(2)內(nèi)存整齊度:復(fù)制算法=標(biāo)記/整理算法>標(biāo)記/清除算法
(3)內(nèi)存利用率:標(biāo)記/整理算法=標(biāo)記/清除算法>復(fù)制算法
注1:可以看到標(biāo)記/清除算法是比較落后的算法了,但是后兩種算法卻是在此基礎(chǔ)上建立的。
注2:時(shí)間與空間不可兼得。
更多關(guān)于“Java培訓(xùn)”的問題,歡迎咨詢千鋒教育在線名師。千鋒已有十余年的培訓(xùn)經(jīng)驗(yàn),課程大綱更科學(xué)更專業(yè),有針對零基礎(chǔ)的就業(yè)班,有針對想提升技術(shù)的好程序員班,高品質(zhì)課程助理你實(shí)現(xiàn)java程序員夢想。