深入理解JVM自動(dòng)內(nèi)存管理
一、前言
對(duì)于Java虛擬機(jī)在內(nèi)存分配與回收的學(xué)習(xí),如果讀者大學(xué)時(shí)代沒有偷懶的話,操作系統(tǒng)和計(jì)算機(jī)組成原理這兩門功課學(xué)的比較好的話,理解起來(lái)JVM是比較容易的,只要底子還在,很多東西都可以觸類旁通。
1.1 計(jì)算機(jī)==>操作系統(tǒng)==>JVM
JVM全稱為Java Virtual Machine,譯為Java虛擬機(jī),讀者會(huì)問,虛擬機(jī)虛擬的是誰(shuí)呢?即虛擬是對(duì)什么東西的虛擬,即實(shí)體是什么,是如何虛擬的?下面讓我們來(lái)看看“虛擬與實(shí)體”。
關(guān)于計(jì)算機(jī)、操作系統(tǒng)、JVM三者關(guān)系,如下圖:
1.1.1 虛擬與實(shí)體(對(duì)上圖的結(jié)構(gòu)層次分析)
JVM之所以稱為之虛擬機(jī),是因?yàn)樗菍?shí)現(xiàn)了計(jì)算機(jī)的虛擬化。下表展示JVM位于操作系統(tǒng)堆內(nèi)存中,分別實(shí)現(xiàn)的了對(duì)操作系統(tǒng)和計(jì)算機(jī)的虛擬化。
操作系統(tǒng)棧對(duì)應(yīng)JVM棧,操作系統(tǒng)堆對(duì)應(yīng)JVM堆,計(jì)算機(jī)磁盤對(duì)應(yīng)JVM方法區(qū),存放字節(jié)碼對(duì)象,計(jì)算機(jī)PC寄存器對(duì)應(yīng)JVM程序計(jì)數(shù)器(注意:計(jì)算機(jī)PC寄存器是下一條指令地址,JVM程序計(jì)數(shù)器是當(dāng)前指令的地址),
唯一不同的是,整個(gè)計(jì)算機(jī)(內(nèi)存(操作系統(tǒng)棧+操作系統(tǒng)堆)+磁盤+PC計(jì)數(shù)器)對(duì)應(yīng)JVM占用的整個(gè)內(nèi)存(JVM棧+JVM堆+JVM方法區(qū)+JVM程序計(jì)數(shù)器)。
1.1.2 Java程序執(zhí)行(對(duì)上圖的箭頭流程分析)
上圖中不僅是結(jié)構(gòu)圖,展示JVM的虛擬和實(shí)體的關(guān)系,也是一個(gè)流程圖,上圖中的箭頭展示JVM對(duì)一個(gè)對(duì)象的編譯執(zhí)行。
程序員寫好的類加載到虛擬機(jī)執(zhí)行的過(guò)程是:當(dāng)一個(gè)classLoder啟動(dòng)的時(shí)候,classLoader的生存地點(diǎn)在JVM中的堆,首先它會(huì)去主機(jī)硬盤上將Test.class裝載到JVM的方法區(qū),方法區(qū)中的這個(gè)字節(jié)文件會(huì)被虛擬機(jī)拿來(lái)new Test字節(jié)碼(),然后在堆內(nèi)存生成了一個(gè)Test字節(jié)碼的對(duì)象,最后Test字節(jié)碼這個(gè)內(nèi)存文件有兩個(gè)引用一個(gè)指向Test的class對(duì)象,一個(gè)指向加載自己的classLoader。整個(gè)過(guò)程上圖用箭頭表示,這里做說(shuō)明。
就像本文開始時(shí)說(shuō)過(guò)的,有了計(jì)算機(jī)組成原理和操作系統(tǒng)兩門課的底子,學(xué)起JVM的時(shí)候會(huì)容易許多,因?yàn)镴VM本質(zhì)上就是對(duì)計(jì)算機(jī)和操作系統(tǒng)的虛擬,就是一個(gè)虛擬機(jī)。
Java正是有了這一套虛擬機(jī)的支持,才成就了跨平臺(tái)(一次編譯,永久運(yùn)行)的優(yōu)勢(shì)。
這樣一來(lái),前言部分我們成功引入JVM,接下來(lái),本文要講述的重點(diǎn)是JVM自動(dòng)內(nèi)存管理,先給出總述:
JVM自動(dòng)內(nèi)存管理=分配內(nèi)存(指給對(duì)象分配內(nèi)存)+回收內(nèi)存(回收分配給對(duì)象的內(nèi)存)
上面公式告訴我們,JVM自動(dòng)內(nèi)存管理分為兩塊,分配內(nèi)存和回收內(nèi)存
二、JVM內(nèi)存空間與參數(shù)設(shè)置
2.1 運(yùn)行時(shí)數(shù)據(jù)區(qū)
JVM在執(zhí)行Java程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的運(yùn)行時(shí)數(shù)據(jù)區(qū)域。這些運(yùn)行時(shí)數(shù)據(jù)區(qū)包括方法區(qū)、堆、虛擬棧、本地方法棧、程序計(jì)數(shù)器,如圖:
讓我們一步步介紹,對(duì)于運(yùn)行時(shí)數(shù)據(jù)區(qū),很多博客都是使用順序介紹的方式,不利于讀者對(duì)比比較學(xué)習(xí),這里筆者以表格的方式呈現(xiàn):
程序計(jì)數(shù)器 | Java虛擬機(jī)棧 | 本地方法棧 | Java 堆 | 方法區(qū) | |
---|---|---|---|---|---|
存放內(nèi)容 | JVM字節(jié)碼指令的地址或Undefined(如果線程正在執(zhí)行一個(gè) Java 方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器的值則為 (Undefined)) | 局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口 | Native方法(本地方法) | 對(duì)象實(shí)例、數(shù)組 | 類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼 |
用途 | 字節(jié)碼解釋器工作是就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行指令的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴計(jì)數(shù)器完成 | 每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行結(jié)束,就對(duì)應(yīng)著一個(gè)棧幀從虛擬機(jī)棧中入棧到出棧的過(guò)程。 | 每一個(gè)本地方法的調(diào)用執(zhí)行過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀從本地方法棧中入棧到出棧的過(guò)程。 | 用于存放對(duì)象實(shí)例,被對(duì)象引用所指向 | 存儲(chǔ)一個(gè)類型所使用到的所有類型,域和方法的符號(hào)引用,在java程序的動(dòng)態(tài)鏈接中起核心作用 |
線程共享還是私有 | 線程私有 | 線程私有 | 線程私有 | 線程間共享 | 線程間共享 |
StackOverflowError棧溢出 | 線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度。報(bào)錯(cuò)信息:java.lang.StackOverflowError | 線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度。報(bào)錯(cuò)信息:java.lang.StackOverflowError | 線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度。報(bào)錯(cuò)信息:java.lang.StackOverflowError | 無(wú) | 無(wú) |
OutOfMemoryError內(nèi)存泄露 | 無(wú) | 如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,而擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存。報(bào)錯(cuò)信息:java.lang.OutOfMemoryError:unable to create new native thread | 如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展,而擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存。報(bào)錯(cuò)信息:java.lang.OutOfMemoryError:unable to create new native thread | 如果堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí),拋出該異常。報(bào)錯(cuò)信息:java.lang.OutOfMemoryError: Java heap space | 當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求,拋出該異常。報(bào)錯(cuò)信息:java.lang.OutOfMemoryError: PermGen space |
特點(diǎn) | 是五個(gè)區(qū)域中唯一一個(gè)沒有OutOfMemoryError | Java虛擬機(jī)棧和本地方法棧都是方法調(diào)用棧,不同之處在于是一個(gè)是程序員編寫的Java方法,一個(gè)是自帶Native方法。 | Java虛擬機(jī)棧和本地方法棧都是方法調(diào)用棧,不同之處在于是一個(gè)是程序員編寫的Java方法,一個(gè)是自帶Native方法。 | 1、可以位于物理上不連續(xù)的空間,但是邏輯上要連續(xù)。2、Java堆又稱為CG堆,分為新生區(qū)和老年區(qū),新生區(qū)又分為Eden區(qū)、From Survivor區(qū)和To Survivor | 又稱為Non-Heap,非堆,與Java堆區(qū)分開來(lái), |
讓我們對(duì)上表繼續(xù)深入,講述上表中的StackOverflowError和OutOfMemoryError。
對(duì)于運(yùn)行時(shí)數(shù)據(jù)區(qū)的五個(gè)區(qū)域,如果討論生命周期,一般討論 堆 和 方法區(qū),因?yàn)槠渌齻€(gè)是線程私有的,生命周期很簡(jiǎn)單;
如果討論垃圾回收算法和垃圾收集器,一般只討論 堆,因?yàn)榉椒▍^(qū)里面存放的是要存活比較久的數(shù)據(jù),其他兩個(gè)棧和一個(gè)程序計(jì)數(shù)器僅保存了引用,只有堆中才是實(shí)際分配對(duì)象的,而要回收的就是對(duì)象;
如果討論 棧溢出,只討論本地方法棧和虛擬機(jī)棧,還有程序計(jì)數(shù)器;如果討論 內(nèi)存泄漏,討論后面四個(gè),唯獨(dú)不討論程序計(jì)數(shù)器。
方法區(qū) 是 堆 邏輯的一部分,存放一些 類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼 ,為什么這些東西放到 方法區(qū) 里面而不是放到堆里面,因?yàn)檫@些是用很久的,不用回收的,所以這里沒有放到堆上(堆分為年輕代和老年代),經(jīng)常要回收,所以里面只能放經(jīng)常要回收的對(duì)象,又按照對(duì)象存活時(shí)間分為年輕代和老年代。
方法區(qū)JDK8之后變?yōu)橹苯觾?nèi)存,理由在于
2.2 關(guān)于StackOverflowError和OutOfMemoryError
2.2.1 StackOverflowError
運(yùn)行時(shí)數(shù)據(jù)區(qū)中,拋出棧溢出的就是虛擬機(jī)棧和本地方法棧,
產(chǎn)生原因:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度。因?yàn)镴VM棧深度是有限的而不是無(wú)限的,但是一般的方法調(diào)用都不會(huì)超過(guò)JVM的棧深度,如果出現(xiàn)棧溢出,基本上都是代碼層面的原因,如遞歸調(diào)用沒有設(shè)置出口或者無(wú)限循環(huán)調(diào)用。
解決方法:程序員檢查代碼是否有無(wú)限循環(huán)即可。
2.2.2 OutOfMemoryError
容易發(fā)生OutOfMemoryError內(nèi)存溢出問題的內(nèi)存空間包括:Permanent Generation space和Heap space。
1、第一種java.lang.OutOfMemoryError: PermGen space(方法區(qū)拋出)
產(chǎn)生原因:發(fā)生這種問題的原意是程序中使用了大量的jar或class,使java虛擬機(jī)裝載類的空間不夠,與Permanent Generation space有關(guān)。所以,根本原因在于jar或class太多,方法區(qū)堆溢出,則解決方法有兩個(gè)種,要么增大方法區(qū),要么減少jar、class文件,且看解決方法。
解決方法:
從增大方法區(qū)方面入手:
增加java虛擬機(jī)中的XX:PermSize和XX:MaxPermSize參數(shù)的大小,其中XX:PermSize是初始永久保存區(qū)域大小,XX:MaxPermSize是最大永久保存區(qū)域大小。
如web應(yīng)用中,針對(duì)tomcat應(yīng)用服務(wù)器,在catalina.sh 或catalina.bat文件中一系列環(huán)境變量名說(shuō)明結(jié)束處增加一行:
JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m"
可有效解決web項(xiàng)目的tomcat服務(wù)器經(jīng)常宕機(jī)的問題。
從減少jar、class文件入手:
清理應(yīng)用程序中web-inf/lib下的jar,如果tomcat部署了多個(gè)應(yīng)用,很多應(yīng)用都使用了相同的jar,可以將共同的jar移到tomcat共同的lib下,減少類的重復(fù)加載。
2、第二種OutOfMemoryError: Java heap space(堆拋出)
產(chǎn)生原因:發(fā)生這種問題的原因是java虛擬機(jī)創(chuàng)建的對(duì)象太多,在進(jìn)行垃圾回收之間,虛擬機(jī)分配的到堆內(nèi)存空間已經(jīng)用滿了,與Heap space有關(guān)。所以,根本原因在于對(duì)象實(shí)例太多,Java堆溢出,則解決方法有兩個(gè)種,要么增大堆內(nèi)存,要么減少對(duì)象示例,且看解決方法。
解決方法:
1.從增大堆內(nèi)存方面入手:
增加Java虛擬機(jī)中Xms(初始堆大小)和Xmx(最大堆大?。﹨?shù)的大小。如:set JAVA_OPTS= -Xms256m -Xmx1024m
2.從減少對(duì)象實(shí)例入手:
一般來(lái)說(shuō),正常程序的對(duì)象,堆內(nèi)存時(shí)絕對(duì)夠用的,出現(xiàn)堆內(nèi)存溢出一般是死循環(huán)中創(chuàng)建大量對(duì)象,檢查程序,看是否有死循環(huán)或不必要地重復(fù)創(chuàng)建大量對(duì)象。找到原因后,修改程序和算法。
3、第三種OutOfMemoryError:unable to create new native thread(Java虛擬機(jī)棧、本地方法棧拋出)
產(chǎn)生原因:這個(gè)異常問題本質(zhì)原因是我們創(chuàng)建了太多的線程,而能創(chuàng)建的線程數(shù)是有限制的,導(dǎo)致了異常的發(fā)生。能創(chuàng)建的線程數(shù)的具體計(jì)算公式如下:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
注意:MaxProcessMemory 表示一個(gè)進(jìn)程的最大內(nèi)存,JVMMemory 表示JVM內(nèi)存,ReservedOsMemory 表示保留的操作系統(tǒng)內(nèi)存,ThreadStackSize 表示線程棧的大小。
在java語(yǔ)言里, 當(dāng)你創(chuàng)建一個(gè)線程的時(shí)候,虛擬機(jī)會(huì)在JVM內(nèi)存創(chuàng)建一個(gè)Thread對(duì)象同時(shí)創(chuàng)建一個(gè)操作系統(tǒng)線程,而這個(gè)系統(tǒng)線程的內(nèi)存用的不是JVMMemory,而是系統(tǒng)中剩下的內(nèi)存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。由公式得出結(jié)論:你給JVM內(nèi)存越多,那么你能創(chuàng)建的線程越少,越容易發(fā)生 java.lang.OutOfMemoryError: unable to create new native thread
解決方法:
1.如果程序中有bug,導(dǎo)致創(chuàng)建大量不需要的線程或者線程沒有及時(shí)回收,那么必須解決這個(gè)bug,修改參數(shù)是不能解決問題的。
2.如果程序確實(shí)需要大量的線程,現(xiàn)有的設(shè)置不能達(dá)到要求,那么可以通過(guò)修改MaxProcessMemory,JVMMemory,ThreadStackSize這三個(gè)因素,來(lái)增加能創(chuàng)建的線程數(shù):MaxProcessMemory 表示使用64位操作系統(tǒng),VMMemory 表示減少 JVMMemory 的分配,ThreadStackSize 表示減小單個(gè)線程的棧大小。
2.3 JVM堆內(nèi)存和非堆內(nèi)存
2.3.1 堆內(nèi)存和非堆內(nèi)存
JVM內(nèi)存劃分為堆內(nèi)存和非堆內(nèi)存,堆內(nèi)存分為年輕代(Young Generation)、老年代(Old Generation),非堆內(nèi)存就一個(gè)永久代(Permanent Generation)。
年輕代又分為Eden和Survivor區(qū)。Survivor區(qū)由FromSpace和ToSpace組成。Eden區(qū)占大容量,Survivor兩個(gè)區(qū)占小容量,默認(rèn)比例是8:1:1。
堆內(nèi)存用途:存放的是對(duì)象,垃圾收集器就是收集這些對(duì)象,然后根據(jù)GC算法回收。
非堆內(nèi)存用途:永久代,也稱為方法區(qū),存儲(chǔ)程序運(yùn)行時(shí)長(zhǎng)期存活的對(duì)象,比如類的元數(shù)據(jù)、方法、常量、屬性等。
在JDK1.8版本廢棄了永久代,替代的是元空間(MetaSpace),元空間與永久代上類似,都是方法區(qū)的實(shí)現(xiàn),他們最大區(qū)別是:永久代使用的是JVM的堆內(nèi)存空間,而元空間使用的是物理內(nèi)存,直接受到本機(jī)的物理內(nèi)存限制。在后面的實(shí)踐中,因?yàn)楣P者使用的是JDK8,所以打印出的GC日志里面就有MetaSpace。
2.3.2 JVM堆內(nèi)部構(gòu)型(新生代和老年代)
Jdk8中已經(jīng)去掉永久區(qū),這里為了與時(shí)俱進(jìn),不再贅余。
上圖演示Java堆內(nèi)存空間,分為新生代和老年代,分別占Java堆1/3和2/3的空間,新生代中又分為Eden區(qū)、Survivor0區(qū)、Survivor1區(qū),分別占新生代8/10、1/10、1/10空間。
問題1:什么是Java堆?
回答1:JVM規(guī)范中說(shuō)到:”所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配”。Java堆是垃圾回收器管理的主要區(qū)域,百分之九十九的垃圾回收發(fā)生在Java堆,另外百分之一發(fā)生在方法區(qū),因此又稱之為”GC堆”。根據(jù)JVM規(guī)范規(guī)定的內(nèi)容,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中。
問題2:為什么Java堆要分為新生代和老年代?
回答2:當(dāng)前JVM對(duì)于堆的垃圾回收,采用分代收集的策略。根據(jù)堆中對(duì)象的存活周期將堆內(nèi)存分為新生代和老年代。在新生代中,每次垃圾回收都有大批對(duì)象死去,只有少量存活。而老年代中存放的對(duì)象存活率高。這樣劃分的目的是為了使 JVM 能夠更好的管理堆內(nèi)存中的對(duì)象,包括內(nèi)存的分配以及回收。
問題3:為什么新生代要分為Eden區(qū)、Survivor0區(qū)、Survivor1區(qū)?
回答3:這是結(jié)構(gòu)與策略相適應(yīng)的原則,新生代垃圾收集使用的是復(fù)制算法(一種垃圾收集算法,Serial收集器、ParNew收集器、Parallel scavenge收集器都是用這種算法),復(fù)制算法可以很好的解決垃圾收集的內(nèi)存碎片問題,但是有一個(gè)天然的缺陷,就是要犧牲一半的內(nèi)存(即任意時(shí)刻只有一半內(nèi)存用于工作),這對(duì)于寶貴的內(nèi)存資源來(lái)說(shuō)是極度奢侈的。新生代在使用復(fù)制算法作為其垃圾收集算法的時(shí)候,對(duì)其做了優(yōu)化,拿出2/10的新生代的內(nèi)存作為交換區(qū),稱為Survivor0區(qū)和Survivor1區(qū)(注意:有的博客上稱為From Survivor Space和To Survivor Space,這樣闡述也是對(duì)的,但是容易對(duì)初學(xué)者形成誤導(dǎo),因?yàn)樵趶?fù)制算法中,復(fù)制是雙向的,沒有固定的From和To,這一次是由這一邊到另一邊,下次就是從另一邊到這一邊,使用From Survivor Space和To Survivor Space容易讓后來(lái)學(xué)習(xí)者誤以為復(fù)制只能從一邊到另一邊,當(dāng)然有的博客中會(huì)附加不管從哪邊到哪邊,起始就是From,終點(diǎn)就是To,即From Survivor Space和To Survivor Space所對(duì)應(yīng)的區(qū)循環(huán)對(duì)調(diào),但是讀者不一定想的明白。所以筆者這里使用Survivor0、Survivor1,減少誤解)
所以說(shuō),新生代在結(jié)構(gòu)上分為Eden區(qū)、Survivor0區(qū)、Survivor1區(qū),是與其使用的垃圾收集算法(復(fù)制算法)相適應(yīng)的結(jié)果。
問題4:關(guān)于永久區(qū)Permanent Space?
回答4:Jdk8中取消了永久區(qū)Permanent Space,使用 元數(shù)據(jù)空間metaspace,使用直接內(nèi)存…
問題:什么是老年代擔(dān)保機(jī)制?
回答:因?yàn)樾律屠夏甏?:2,如果在eden區(qū)放不下,會(huì)放到老年區(qū),如果minor gc的時(shí)候,survivor區(qū)放不下,也會(huì)放到老年區(qū),所有,有時(shí)候會(huì)在老年區(qū)里面有 不少gc年齡比較小 的大對(duì)象,就是因?yàn)槟贻p代放不下了,老年代擔(dān)保機(jī)制多次觸發(fā)會(huì)增加老年代負(fù)擔(dān),過(guò)早地觸發(fā)major gc,說(shuō)明當(dāng)前的 eden survivor 比例設(shè)置不太好。
問題:為什么eden:survivor1:survivor2=8:1:1?
回答:這個(gè)可以設(shè)置的,VM Options: -XX:SurvivorRatio=8 新生代中Eden區(qū)域與Survivor區(qū)域的容量比值,默認(rèn)為8,代表Eden:Survivor=8:1。如果eden區(qū)域占比小,那么minor gc會(huì)比較頻繁,gc線程是占用cpu資源的,是stop the world的,不好;如果eden區(qū)域占比大,則survivor區(qū)域變小了,survivor區(qū)滿了也會(huì)觸發(fā)老年代擔(dān)保機(jī)制。
Minor GC觸發(fā)條件:eden區(qū)滿時(shí),觸發(fā)MinorGC。即申請(qǐng)一個(gè)對(duì)象時(shí),發(fā)現(xiàn)eden區(qū)不夠用,則觸發(fā)一次MinorGC。在MinorGC時(shí),將小于 to space大小的存活對(duì)象復(fù)制到 to space(如果to space區(qū)域不夠,則利用擔(dān)保機(jī)制進(jìn)入老年代區(qū)域),然后to space和from space換位置,所以我們看到的to space一直是空的。
問題:為什么年輕代age是0~15,到了16就移動(dòng)到老年代?
回答:對(duì)象頭中用四個(gè)bit位存放分代年齡,所以就是 0~15。
無(wú)論是對(duì)象大小還是對(duì)象年輕,進(jìn)入老年代的閾值都是可以用參數(shù)設(shè)置的,VM Options: -XX:PretenureSizeThreshold=3145728,表示大于3MB都到老年代中去;VM Options: -XX:MaxTenuringThreshold=2,表示經(jīng)歷兩次Minor GC,就到老年代中去。
問題:虛擬機(jī)怎么知道哪個(gè)對(duì)象要回收,哪個(gè)對(duì)象不回收?
回答:兩種方式:要么 引用計(jì)數(shù) ,要么 根節(jié)點(diǎn)+可達(dá)性分析。
引用計(jì)數(shù):這種方式有循環(huán)引用的問題,Java中不使用,python是使用。
解釋一下引用計(jì)數(shù),一般來(lái)說(shuō),java要回收的對(duì)象要求是沒有引用指向的,就是程序中沒有了用的對(duì)象,才可以回收,要求gc不影響程序??匆粋€(gè)循環(huán)引用的問題:
public class Main1 { public static void main(String[] args) { A a = new A(); B b = new B(); // 這個(gè)時(shí)候 new A()對(duì)象和new B()對(duì)象引用計(jì)數(shù)為1,就是a b a.instance = b; b.instance = a; // 這個(gè)時(shí)候 new A()對(duì)象和new B()對(duì)象引用計(jì)數(shù)為2,就是a b a.instance b.instance a = null; b = null; // 這個(gè)時(shí)候 new A()對(duì)象和new B()對(duì)象已經(jīng)沒有引用了,但是引用計(jì)數(shù)仍然為1,instance還在指向 } } class A { B instance; } class B { A instance; }
對(duì)于 根節(jié)點(diǎn)+可達(dá)性分析,確定若干個(gè)根節(jié)點(diǎn),從根節(jié)點(diǎn)出發(fā),可以達(dá)到的就是可達(dá)的引用,所指向的對(duì)象不可回收,反之可以回收。如圖:
obj1 引用指向?qū)ο?,obj2 引用指向?qū)ο?,obj3 引用指向?qū)ο?,而對(duì)象3 中又引用 對(duì)象4,對(duì)象5 不是 gc root,所以 對(duì)象5 和 對(duì)象6 都在可達(dá)性鏈 中。最終,對(duì)象1 對(duì)象2 對(duì)象3 對(duì)象4 都是可達(dá)的,不會(huì)被垃圾收集器回收,對(duì)象5 對(duì)象6 是不可達(dá)的,要被垃圾收集器回收。
2.4 JVM堆參數(shù)設(shè)置
這些都是和堆內(nèi)存分配有關(guān)的參數(shù),所以我們放在第二部分了,和垃圾收集器有關(guān)的參數(shù)放在第四部分。
舉例:java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m
2.4.1 JVM重要參數(shù)
因?yàn)檎麄€(gè)堆大小=年輕代大?。ㄐ律笮。?+ 年老代大小 + 持久代大小,
-Xmn2g:表示年輕代大小為2G。持久代一般固定大小為64m,所以增大年輕代后,將會(huì)減小年老代大小。此值對(duì)系統(tǒng)性能影響較大,Sun官方推薦配置為整個(gè)堆的3/8。
-XX:NewRatio=4:設(shè)置年輕代(包括Eden和兩個(gè)Survivor區(qū))與年老代的比值(除去持久代)。這里設(shè)置為4,表示年輕代與年老代所占比值為1:4,又因?yàn)樯厦嬖O(shè)置年輕代為2G,則老年代大小為8G
-XX:SurvivorRatio=8:設(shè)置年輕代中Eden區(qū)與Survivor區(qū)的大小比值。這里設(shè)置為8,則兩個(gè)Survivor區(qū)與一個(gè)Eden區(qū)的比值為2:8,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/10
則Eden:Survivor0:Survivor1=8:1:1
-XX:MaxPermSize=16m:設(shè)置持久代大小為16m。
所有整個(gè)堆大小=年輕代大小 + 年老代大小 + 持久代大小= 2G+ 8G+ 16M=10G+6M=10246MB
2.4.2 JVM其他參數(shù)
-Xmx3550m:設(shè)置JVM最大可用內(nèi)存為3550M。
-Xms3550m:設(shè)置JVM促使內(nèi)存為3550m,此值可以設(shè)置與-Xmx相同。
-Xss128k:設(shè)置每個(gè)線程的堆棧大小。JDK5.0以后每個(gè)線程堆棧大小為1M,以前每個(gè)線程堆棧大小為256K。更具應(yīng)用的線程所需內(nèi)存大小進(jìn)行調(diào)整。在相同物理內(nèi)存下,減小這個(gè)值能生成更多的線程。但是操作系統(tǒng)對(duì)一個(gè)進(jìn)程內(nèi)的線程數(shù)還是有限制的,不能無(wú)限生成,經(jīng)驗(yàn)值在3000~5000左右。
關(guān)于為什么-xmx與-xms的大小設(shè)置為一樣的?
- 首先,在Java堆內(nèi)存分配中,-xmx用于指定JVM最大分配的內(nèi)存,-xms用于指定JVM初始分配的內(nèi)存,所以,-xmx與-xms相等表示JVM初次分配的內(nèi)存的時(shí)候就把所有可以分配的最大內(nèi)存分配給它(指JVM),這樣的做的好處是:
- 避免JVM在運(yùn)行過(guò)程中、每次垃圾回收完成后向OS申請(qǐng)內(nèi)存:因?yàn)樗械目梢苑峙涞淖畲髢?nèi)存第一個(gè)就給它(JVM)了。
- 延后啟動(dòng)后首次GC的發(fā)生時(shí)機(jī)、減少啟動(dòng)初期的GC次數(shù):因?yàn)榈谝淮谓o它分配了最大的;
- 盡可能避免使用swap space:swap space為交換空間,當(dāng)web項(xiàng)目部署到linux上時(shí),有一條調(diào)優(yōu)原則就是“盡可能使用內(nèi)存而不是交換空間”
- 設(shè)置堆內(nèi)存為不可擴(kuò)展和收縮,避免在每次GC 后調(diào)整堆的大小
影響堆內(nèi)存擴(kuò)展與收縮的兩個(gè)參數(shù)
MaxHeapFreeRadio | MinHeapFreeRadio |
---|---|
默認(rèn)值為70 | 默認(rèn)值為40 |
當(dāng)xmx值比xms值大,堆可以動(dòng)態(tài)收縮與擴(kuò)展,這個(gè)參數(shù)控制當(dāng)堆空間大于指定比例時(shí)會(huì)自動(dòng)收縮,默認(rèn)表示堆空間大于70%會(huì)自動(dòng)收縮 | 當(dāng)xmx值比xms值大,堆可以動(dòng)態(tài)收縮與擴(kuò)展,這個(gè)參數(shù)控制當(dāng)堆空間小于指定比例時(shí)會(huì)自動(dòng)擴(kuò)展,默認(rèn)表示堆空間小于40%會(huì)自動(dòng)擴(kuò)展 |
由上表可知,堆內(nèi)存默認(rèn)是自動(dòng)擴(kuò)展和收縮的,但是有一個(gè)前提條件,就是到xmx比xms大的時(shí)候,當(dāng)我們將xms設(shè)置為和xmx一樣大,堆內(nèi)存就不可擴(kuò)展和收縮了,即整個(gè)堆內(nèi)存被設(shè)置為一個(gè)固定值,避免在每次GC 后調(diào)整堆的大小。
附加:在Java非堆內(nèi)存分配中,一般是用永久區(qū)內(nèi)存分配:
JVM 使用 -XX:PermSize 設(shè)置非堆內(nèi)存初始值,由 -XX:MaxPermSize 設(shè)置最大非堆內(nèi)存的大小。
2.5 從日志看JVM(開發(fā)實(shí)踐)
這里了設(shè)置GC日志關(guān)聯(lián)的類和將GC日志打印
如程序所述,申請(qǐng)了10MB的空間,allocation1 2MB+allocation2 2MB+allocation3 2MB+allocation4 4MB=10MB
接下來(lái)我們開始閱讀GC日志,這里筆者以自己電腦上打印的GC日志為例,講述閱讀GC日志的方法:
heap表示堆,即下面的日志是對(duì)JVM堆內(nèi)存的打??;
因?yàn)槭褂玫氖莏dk8,所以默認(rèn)使用ParallelGC收集器,也就是在新生代使用Parallel Scavenge收集器,老年代使用ParallelOld收集器
PSYoungGen 表示使用Parallel scavenge收集器作為年輕代收集器,ParOldGen表示使用Parallel old收集器作為老年代收集器,即筆者電腦上默認(rèn)是使用Parallel scavenge+Parallel old收集器組合。
其中,PSYoungGen總共38400K(37.5MB),被使用了13568K(13.25MB),PSYoungGen又分為Eden Space 33280K(32.5MB) 被使用了40% 13MB,from space 5120K(5MB)和to space 5120K(5MB),這就是一個(gè)eden區(qū)和兩個(gè)survivor區(qū)。
此處注意,因?yàn)槭褂玫氖莏dk8,所以沒有永久區(qū)了,只有MetaSpace,見上圖。
三、HotSpot VM
3.1 HotSpot VM相關(guān)知識(shí)
問題一:什么是HotSpot虛擬機(jī)?HotSpot VM的前世今生?
回答一:HotSpot VM是由一家名為“Longview Technologies”的公司設(shè)計(jì)的一款虛擬機(jī),Sun公司收購(gòu)Longview Technologies公司后,HotSpot VM成為Sun主要支持的VM產(chǎn)品,Oracle公司收購(gòu)Sun公司后,即在HotSpot的基礎(chǔ)上,移植JRockit的優(yōu)秀特性,將HotSpot VM與JRockit VM整合到一起。
問題二:HotSpot VM有何優(yōu)點(diǎn)?
回答二:HotSpot VM的熱點(diǎn)代碼探測(cè)能力可以通過(guò)執(zhí)行計(jì)數(shù)器找出最具有編譯價(jià)值的代碼,然后通知JIT編譯器以方法為單位進(jìn)行編譯。如果一個(gè)方法被頻繁調(diào)用,或方法中有效循環(huán)次數(shù)很多,將會(huì)分別觸發(fā)標(biāo)準(zhǔn)編譯和OSR(棧上替換)編譯動(dòng)作。 通過(guò)編譯器與解釋器恰當(dāng)?shù)貐f(xié)同工作,可以在最優(yōu)化的程序響應(yīng)時(shí)間與最佳執(zhí)行性能中取得平衡,而且無(wú)須等待本地代碼輸出才能執(zhí)行程序,即時(shí)編譯的時(shí)間壓力也相對(duì)減小,這樣有助于引入更多的代碼優(yōu)化技術(shù),輸出質(zhì)量更高的本地代碼。
問題三:HotSpot VM與JVM是什么關(guān)系?
回答三:今天的HotSpot VM,是Sun JDK和OpenJDK中所帶的虛擬機(jī),也是目前使用范圍最廣的Java虛擬機(jī)。
3.2 HotSpot VM的兩個(gè)實(shí)現(xiàn)與查看本機(jī)HotSpot
HotSpot VM包括兩個(gè)實(shí)現(xiàn),不同的實(shí)現(xiàn)適合不同的場(chǎng)景:
Java HotSpot Client VM:通過(guò)減少應(yīng)用程序啟動(dòng)時(shí)間和內(nèi)存占用,在客戶端環(huán)境中運(yùn)行應(yīng)用程序時(shí)可以獲得最佳性能。此經(jīng)過(guò)專門調(diào)整,可縮短應(yīng)用程序啟動(dòng)時(shí)間和內(nèi)存占用,使其特別適合客戶端環(huán)境。此jvm實(shí)現(xiàn)比較適合我們平時(shí)用作本地開發(fā),平時(shí)的開發(fā)不需要很大的內(nèi)存。
Java HotSpot Server VM:旨在最大程度地提高服務(wù)器環(huán)境中運(yùn)行的應(yīng)用程序的執(zhí)行速度。此jvm實(shí)現(xiàn)經(jīng)過(guò)專門調(diào)整,可能是特別調(diào)整堆大小、垃圾回收器、編譯器那些。用于長(zhǎng)時(shí)間運(yùn)行的服務(wù)器程序,這些服務(wù)器程序需要盡可能快的運(yùn)行速度,而不是快速啟動(dòng)時(shí)間。
只要電腦上安裝jdk,我們就可以看到hotspot的具體實(shí)現(xiàn):
四、JVM內(nèi)存回收
我們知道,Java中是沒有析構(gòu)函數(shù)的,既然沒有析構(gòu)函數(shù),那么如何回收對(duì)象呢,答案是自動(dòng)垃圾回收。Java語(yǔ)言的自動(dòng)回收機(jī)制可以使程序員不用再操心對(duì)象回收問題,一切都交給JVM就好了。那么JVM又是如何做到自動(dòng)回收垃圾的呢,且看本節(jié),本節(jié)分為兩個(gè)部分——垃圾收集算法和垃圾收集器,其中,收集算法是內(nèi)存回收的理論,而垃圾回收器是內(nèi)存回收的實(shí)踐。
垃圾收集策略兩個(gè)目的:gc次數(shù)少,gc時(shí)間短,不同的收集算法和收集器側(cè)重不同。
4.1 垃圾收集算法(內(nèi)存回收理論)
4.1.1 標(biāo)記-清除算法
標(biāo)記-清除算法分為兩個(gè)階段,“標(biāo)記”和“清除”,
標(biāo)記:首先標(biāo)記出所有需要回收的對(duì)象;
清除:在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。
“標(biāo)記-清除”算法的不足:第一,效率問題,標(biāo)記和清除兩個(gè)過(guò)程的效率都不會(huì)太高;第二,空間問題,標(biāo)記清除后產(chǎn)生大量不連續(xù)的內(nèi)存碎片,這些內(nèi)存空間碎片可能會(huì)導(dǎo)致以后程序運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)一次垃圾收集動(dòng)作,如果很容易出現(xiàn)這樣的空間碎片多、無(wú)法找到大的連續(xù)空間的情況,垃圾收集就會(huì)較為頻繁。
4.1.2 復(fù)制算法
為了解決“標(biāo)記-清除算法”的效率問題,一種復(fù)制算法產(chǎn)生了,它將當(dāng)前可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊。當(dāng)一塊的內(nèi)存用完了,就將還活著的對(duì)象復(fù)制到另一塊上面,然后再把已使用的內(nèi)存空間一次清除掉。這樣使得每次都對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可。
這種算法處理內(nèi)存碎片的核心在于將整個(gè)半塊中活的的對(duì)象復(fù)制到另一整個(gè)半塊上面去,所以稱為復(fù)制算法。
附:關(guān)于復(fù)制算法的改進(jìn)
復(fù)制算法合理的解決了內(nèi)存碎片問題,但是卻要以犧牲一半的寶貴內(nèi)存為代價(jià),這是非常讓人心疼的。令人愉快地是,現(xiàn)代虛擬機(jī)中,早就有了關(guān)于復(fù)制算法的改進(jìn):
對(duì)于Java堆中新生代中的對(duì)象來(lái)說(shuō),99%的對(duì)象都是“朝升夕死”的,就是說(shuō)很多的對(duì)象在創(chuàng)建出來(lái)后不久就會(huì)死掉了,所有我們可以大膽一點(diǎn),不需要按照1:1的比例來(lái)劃分內(nèi)存空間,而是將新生代的內(nèi)存劃分為一塊較大的Eden區(qū)(一般占新生代8/10的大?。┖蛢蓧K較小的Survivor區(qū)(用于復(fù)制,一般每塊占新生代1/10的大小,兩塊占新生代2/10的大小)。當(dāng)回收時(shí),將Eden區(qū)和Survivor里面當(dāng)前還活著的對(duì)象全部都復(fù)制到另一塊Survivor中(關(guān)于另一個(gè)塊Survivor是否會(huì)溢出的問題,答案是不會(huì),這里將新生代90%的容量里的對(duì)象復(fù)制到10%的容量里面,確實(shí)是有風(fēng)險(xiǎn)的,但是JVM有一種內(nèi)存的分配擔(dān)保機(jī)制,即當(dāng)目的Survivor空間不夠,會(huì)將多出來(lái)的對(duì)象放到老年代中,因?yàn)槔夏甏亲銐虼蟮模?,最后清理Eden區(qū)和源Survivor區(qū)的空間。這樣一來(lái),每次新生代可用內(nèi)存空間為整個(gè)新生代90%,只有10%的內(nèi)存被浪費(fèi)掉,
正是因?yàn)檫@一特性,現(xiàn)代虛擬機(jī)中采用復(fù)制算法來(lái)回收新生代,如Serial收集器、ParNew收集器、Parallel scavenge收集器均是如此。
4.1.3 標(biāo)志-整理算法(復(fù)制算法變更后在老年代的應(yīng)用)
對(duì)于新生代來(lái)說(shuō),由于具有“99%的對(duì)象都是朝生夕死的”這一特點(diǎn),所以我們可以大膽的使用10%的內(nèi)存去存放90%的內(nèi)存中活著的對(duì)象,即使是目的Survivor的容量不夠,也可以將多余的存放到老年代中(擔(dān)保機(jī)制),所有對(duì)于新生代,我們使用復(fù)制算法是比較好的(Serial收集器、ParNew收集器、Parallel scavenge收集器)。
但是對(duì)于老年代,沒有大多數(shù)對(duì)象朝生夕死這一特點(diǎn),如果使用復(fù)制算法就要浪費(fèi)一半的寶貴內(nèi)存,所有我們用另一種辦法來(lái)處理它(指老年代)——標(biāo)志-整理算法。
標(biāo)記-整理算法分為兩個(gè)階段,“標(biāo)記”和“整理”,
標(biāo)記:首先標(biāo)記出所有需要回收的對(duì)象(和標(biāo)記-清除算法一樣);
整理:在標(biāo)記完成后讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存(向一端移動(dòng)類似復(fù)制算法)。
區(qū)別:標(biāo)志-清除算法包括先標(biāo)志,后清除兩個(gè)過(guò)程,標(biāo)志-整理算法包括先標(biāo)志,后清除,再整理三個(gè)過(guò)程。
4.1.4 分代收集算法
當(dāng)前商業(yè)虛擬機(jī)都是的垃圾收集都使用“分代收集”算法,這種算法并沒有什么新的思想,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采取最適當(dāng)?shù)氖占惴ā?/p>
在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量對(duì)象存活,就是使用復(fù)制算法,這樣只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象的存活率高、沒有額外空間對(duì)其分配擔(dān)保(新生代復(fù)制算法如果目的Survivor容量不夠會(huì)將多余對(duì)象放到老年代中,這就是老年代對(duì)新生代的分配擔(dān)保),必須使用“標(biāo)記-清除算法”或“標(biāo)記-整理算法”來(lái)回收。
新生代的minor gc頻率較高,復(fù)制算法正好浪費(fèi)一半空間,不用整理內(nèi)存空間碎片,以空間換時(shí)間;
老年代的major gc頻率較低,“標(biāo)記-整理算法”包括先標(biāo)志、再清除,最后整理,整理內(nèi)存碎片需要時(shí)間,但是整個(gè)算法不浪費(fèi)空間,以時(shí)間換空間。
四種常用算法優(yōu)缺點(diǎn)比較、用途比較
4.2 垃圾收集器(內(nèi)存回收實(shí)踐)
有了上面的垃圾回收算法,就有了很多的垃圾回收器。對(duì)于垃圾回收器,很少有表格對(duì)比,筆者以表格對(duì)比的方式呈現(xiàn):
單線程or多線程 | 新生代or老年代 | 基于的收集算法 | 備注 | |
---|---|---|---|---|
Serial收集器 | 單線程 | 新生代 | 復(fù)制算法 | 優(yōu)點(diǎn):簡(jiǎn)單; 缺點(diǎn):Stop the world,垃圾收集時(shí)要停掉所有其他線程; 常用組合:Serial + serial old 新生代和老年代都是單線程,簡(jiǎn)單 |
ParNew收集器(是Serial收集器的多線程版本) | 多線程 | 新生代 | 復(fù)制算法 | 優(yōu)點(diǎn):相對(duì)于Serial收集器,使用了多線程; 缺點(diǎn):除了多線程,其他所有和Serial收集器一樣; 常用組合:ParNew+ serial old 新生代多線程,老年代單線程,簡(jiǎn)單(新生代ParNew收集器僅僅是Serial收集器的多線程版本,所有該組合相對(duì)于Serial + serial old 只是新生代是多線程而已,其余不變) |
Parallel scavenge收集器(吞吐量?jī)?yōu)先收集器) | 多線程 | 新生代 | 復(fù)制算法 | 設(shè)計(jì)目標(biāo):盡可能達(dá)到一個(gè)可控制的吞吐量; 吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+來(lái)及收集時(shí)間); 優(yōu)點(diǎn):吞吐量高,可以高效率地利用CPU時(shí)間,盡快完成程序的計(jì)算任務(wù),適合后臺(tái)運(yùn)算; 缺點(diǎn):沒有太大缺陷; 常用組合:Parallel scavenge + Parallel old 該組合完成吞吐量?jī)?yōu)先虛擬機(jī),適用于后臺(tái)計(jì)算; |
serial old收集器(是Serial收集器的老年代版本) | 單線程 | 老年代 | 標(biāo)記-整理算法 | 優(yōu)點(diǎn):簡(jiǎn)單; 缺點(diǎn):Stop the world,垃圾收集時(shí)要停掉所有其他線程; 常用組合:Serial + serial old 新生代和老年代都是單線程,簡(jiǎn)單; |
Parallel old收集器(是Parallel scavenge收集器的老年代版本) | 多線程 | 老年代 | 標(biāo)記-整理算法 | 優(yōu)點(diǎn):吞吐量高,可以高效率地利用CPU時(shí)間,盡快完成程序的計(jì)算任務(wù),適合后臺(tái)運(yùn)算; 缺點(diǎn):沒有太大缺陷; 常用組合:Parallel scavenge + Parallel old 該組合完成吞吐量?jī)?yōu)先虛擬機(jī),適用于后臺(tái)計(jì)算; |
cms收集器(并發(fā)低停頓收集器) | 多線程 | 老年代 | 標(biāo)記-清除算法 | 優(yōu)點(diǎn):停頓時(shí)間短,適合與用戶交互的程序;四個(gè)步驟:初始標(biāo)記 CMS initial mark、并發(fā)標(biāo)記 CMS concurrent mark、重新標(biāo)記 CMS remark、并發(fā)清除 CMS concurrent sweep;常用組合:cms收集器 完成響應(yīng)時(shí)間短虛擬機(jī),適用于用戶交互; |
G1收集器 | 多線程 | 新生代+老年代 | 標(biāo)記-整理算法 | 面向服務(wù)端的垃圾回收器。特點(diǎn):并行與并發(fā)、分代收集、空間整合、可預(yù)測(cè)的停頓;四個(gè)步驟:初始標(biāo)記 Initial Marking、并發(fā)標(biāo)記 Concurrent Marking、最終篩選 Final Marking 、篩選回收 Live Data Counting and Evacuation;常用組合:G1收集器 面向服務(wù)端的垃圾回收器注意:G1收集器的收集算法加粗了,這里做出說(shuō)明,G1收集器從整體上來(lái)看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè)region之間)上看來(lái)是基于“復(fù)制”算法實(shí)現(xiàn)的。 |
注意:G1收集器的收集算法加粗了,這里做出說(shuō)明,G1收集器從整體上來(lái)看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè)region之間)上看來(lái)是基于“復(fù)制”算法實(shí)現(xiàn)的。
從上表可以得到的收集常用組合包括:
常用組合1:Serial + serial old 新生代和老年代都是單線程,簡(jiǎn)單
常用組合2:ParNew+ serial old 新生代多線程,老年代單線程,簡(jiǎn)單
常用組合3:Parallel scavenge + Parallel old 該組合完成吞吐量?jī)?yōu)先虛擬機(jī),適用于后臺(tái)計(jì)算
常用組合4:cms收集器 完成響應(yīng)時(shí)間短虛擬機(jī),適用于用戶交互
常用組合5:G1收集器 面向服務(wù)端的垃圾回收器
4.2.1 常用組合1:Serial + serial old 新生代和老年代都是單線程,簡(jiǎn)單
附:圖上有一個(gè)safepoint,譯為安全點(diǎn)(有的博客上寫成了savepoint,是錯(cuò)誤的,至少是不準(zhǔn)確的),這個(gè)safepoint干什么的呢?如何確定這個(gè)safepoint的位置?
這個(gè)safepoint是干什么的?
safepoint的定義是“A point in program where the state of execution is known by the VM”,譯為程序中一個(gè)點(diǎn)就是虛擬機(jī)所知道的一個(gè)執(zhí)行狀態(tài)。
JVM中safepoint有兩種,分別為GC safepoint、Deoptimization safepoint:
GC safepoint:用在垃圾收集操作中,如果要執(zhí)行一次GC,那么JVM里所有需要執(zhí)行GC的Java線程都要在到達(dá)GC safepoint之后才可以開始GC;
Deoptimization safepoint:如果要執(zhí)行一次deoptimization,那么JVM里所有需要執(zhí)行deoptimization的Java線程都要在到達(dá)deoptimization safepoint之后才可以開始deoptimize
我們上圖中的safepoint自然是GC safepoint,所以上圖中的兩個(gè)safepoint都是指執(zhí)行GC線程前的狀態(tài)。
對(duì)于上圖的理解是(很多博客上都有這種運(yùn)行示意圖,但是沒有加上解釋,筆者這里加上):
1、多個(gè)用戶線程(圖中是四個(gè))要開始執(zhí)行新生代GC操作,所以都要達(dá)到GC safepoint點(diǎn),先到的要等待晚到的,圖中都達(dá)到了;
2、四個(gè)線程都執(zhí)行新生代的GC操作,因?yàn)槭褂玫氖荢erial收集器,所以是基于復(fù)制算法的單線程GC,而且要Stop the world,所以只有GC線程在執(zhí)行,四個(gè)用戶線程都停止了。
3、新生代GC操作完成,四個(gè)線程繼續(xù)執(zhí)行,過(guò)了一會(huì)兒,要開始執(zhí)行老年代的GC操作了,所以四個(gè)線程都要再次達(dá)到GC safepoint點(diǎn),先到的要等待晚到的,圖中都達(dá)到了;
4、四個(gè)線程都執(zhí)行老年代的GC操作,因?yàn)槭褂玫氖荢erial Old收集器,所以是基于標(biāo)志-整理算法的單線程GC,而且要Stop the world,所以只有GC線程在執(zhí)行,四個(gè)用戶線程都停止了。
5、老年代GC操作完成,四個(gè)線程繼續(xù)執(zhí)行。
4.2.2 常用組合2:ParNew+ serial old 新生代多線程,老年代單線程,簡(jiǎn)單
該組合中新生代ParNew收集器僅僅是Serial收集器的多線程版本,所有該組合相對(duì)于Serial + serial old 只是新生代是多線程而已,其余不變
對(duì)于上圖的理解是(很多博客上都有這種運(yùn)行示意圖,但是沒有加上解釋,筆者這里加上):
1、多個(gè)用戶線程(圖中是四個(gè))要開始執(zhí)行新生代GC操作,所以都要達(dá)到GC safepoint點(diǎn),先到的要等待晚到的,圖中都達(dá)到了;
2、四個(gè)線程都執(zhí)行新生代的GC操作,因?yàn)槭褂玫氖荘arnew收集器,所以是基于復(fù)制算法的多線程GC(注意,這里的多線程GC,是指多個(gè)GC線程并發(fā),用戶線程還是要停止的)所以還是要Stop the world,所以只有GC線程在執(zhí)行,四個(gè)用戶線程都停止了。
3、新生代GC操作完成,四個(gè)線程繼續(xù)執(zhí)行,過(guò)了一會(huì)兒,要開始執(zhí)行老年代的GC操作了,所以四個(gè)線程都要再次達(dá)到GC safepoint點(diǎn),先到的要等待晚到的,圖中都達(dá)到了;
4、四個(gè)線程都執(zhí)行老年代的GC操作,因?yàn)槭褂玫氖荢erial Old收集器,所以是基于標(biāo)志-整理算法的單線程GC,而且要Stop the world,所以只有GC線程在執(zhí)行,四個(gè)用戶線程都停止了。
5、老年代GC操作完成,四個(gè)線程繼續(xù)執(zhí)行。
4.2.3 常用組合3:Parallel scavenge + Parallel old 新生代和老年代都是多線程,該組合完成吞吐量?jī)?yōu)先虛擬機(jī),適用于后臺(tái)計(jì)算
對(duì)于上圖的理解是:
1、多個(gè)用戶線程(圖中是四個(gè))要開始執(zhí)行新生代GC操作,所以都要達(dá)到GC safepoint點(diǎn),先到的要等待晚到的,圖中都達(dá)到了;
2、四個(gè)線程都執(zhí)行新生代的GC操作,因?yàn)槭褂玫氖荘arallel scavenge收集器,所以是基于復(fù)制算法的多線程GC(注意,這里的多線程GC,是指多個(gè)GC線程并發(fā),用戶線程還是要停止的)所以只有GC線程在執(zhí)行,四個(gè)用戶線程都停止了。
3、新生代GC操作完成,四個(gè)線程繼續(xù)執(zhí)行,過(guò)了一會(huì)兒,要開始執(zhí)行老年代的GC操作了,所以四個(gè)線程都要再次達(dá)到GC safepoint點(diǎn),先到的要等待晚到的,圖中都達(dá)到了;
4、四個(gè)線程都執(zhí)行老年代的GC操作,因?yàn)槭褂玫氖荘arallel Old收集器,所以是基于標(biāo)志-整理算法的多線程GC,(注意,這里的多線程GC,是指多個(gè)GC線程并發(fā),用戶線程還是要停止的)所以只有GC線程在執(zhí)行,四個(gè)用戶線程都停止了。
5、老年代GC操作完成,四個(gè)線程繼續(xù)執(zhí)行。
4.2.4 常用組合4:cms收集器 多線程,完成響應(yīng)時(shí)間短虛擬機(jī),適用于用戶交互
對(duì)于上圖的理解是:
CMS收集包括四個(gè)步驟:初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清除(CMS作為標(biāo)記-清除收集器,三個(gè)標(biāo)記一個(gè)清除)
1、多個(gè)用戶線程(圖中是四個(gè))要開始執(zhí)行新生代GC操作,所以都要達(dá)到GC safepoint點(diǎn),先到的要等待晚到的,圖中都達(dá)到了;
2、四個(gè)線程都執(zhí)行GC操作,因?yàn)槭褂玫氖荂MS收集器,第一步驟是初始標(biāo)記,初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,GC的標(biāo)記階段需要stop the world,讓所有Java線程掛起,這樣JVM才可以安全地來(lái)標(biāo)記對(duì)象。所以只有“初始標(biāo)記”在執(zhí)行,四個(gè)用戶線程都停止了。初始標(biāo)記完成后,達(dá)到第二個(gè)GC safepoint,圖中達(dá)到了;
3、開始執(zhí)行并發(fā)標(biāo)記,并發(fā)標(biāo)記是GCRoot開始對(duì)堆中的對(duì)象進(jìn)行可達(dá)性分析,找出存活的對(duì)象,并發(fā)標(biāo)記可以與用戶線程一起執(zhí)行,并發(fā)標(biāo)記完成后,所有線程達(dá)到下一個(gè)GC safepoint,圖中達(dá)到了;
4、開始執(zhí)行重新標(biāo)記,重新標(biāo)記是為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那部分標(biāo)記記錄,
重新標(biāo)記完成后,所有線程達(dá)到下一個(gè)GC safepoint,圖中達(dá)到了;
5、開始執(zhí)行并發(fā)清理,并發(fā)清理可以與用戶線程一起執(zhí)行,并發(fā)清理完成后,所有線程達(dá)到下一個(gè)GC safepoint,圖中達(dá)到了;
6、開始重置線程,就是對(duì)剛才并發(fā)標(biāo)記操作的對(duì)象,圖中是線程3(注意:重置線程針對(duì)的是并發(fā)標(biāo)記的線程,沒有被并發(fā)標(biāo)記的線程不需要重置線程操作),重置操作線程3的時(shí)候,與其他三個(gè)用戶線程無(wú)關(guān),它們可以一起執(zhí)行。
CMS為什么是多線程收集器?
因?yàn)镃MS收集器整個(gè)過(guò)程中耗時(shí)最長(zhǎng)的第二并發(fā)標(biāo)記和第四并發(fā)清除過(guò)程中,GC線程都可以與用戶線程一起工作,初始標(biāo)記和重新標(biāo)記時(shí)間忽略不計(jì),所以,從總體上來(lái)說(shuō),cms收集器的內(nèi)存回收過(guò)程與用戶線程是并發(fā)執(zhí)行的,所以上表中CMS為多線程收集器。
4.2.5 常用組合5:G1收集器 多線程,面向服務(wù)端的垃圾回收器
G1收集器是一款比CMS更優(yōu)秀的收集器,所以取代了cms,成為現(xiàn)在虛擬機(jī)的內(nèi)置收集器,它將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域,即Region,雖然還保留新生代和老年代的概念,但新生代和老年代已不再物理隔離,僅僅是邏輯分區(qū),它們都是一部分Region的集合,如下圖:
每個(gè)region內(nèi)部必須是邏輯連續(xù)的,一個(gè)大小限制為 1M ~ 32M 之間,數(shù)量為 2048 個(gè)region,所以整個(gè)收集器 大小為 2G ~ 64G 。region 分為五種,Empty就是空白region,eden 和 survivor 都是年輕代,使用復(fù)制算法,old 是老年代,使用 標(biāo)志-整理算法(先標(biāo)記,再清除,最后整理),還有一個(gè) Humongous region,是用來(lái)存放大對(duì)象的。
G1收集器的精髓1:G1 英文全名為 Garage First,就是 垃圾優(yōu)先 的意思,內(nèi)置 region 價(jià)值分析算法,垃圾收集的時(shí)候,會(huì)跟蹤各個(gè)Region里面的垃圾堆積的價(jià)值大?。ɑ厥账@得的空間大小以及回收所需時(shí)間的經(jīng)驗(yàn)值),在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次依據(jù)允許的收集時(shí)間,優(yōu)先收集回收價(jià)值最大的Region。正是這種使用Region劃分內(nèi)存空間以及有優(yōu)先級(jí)的區(qū)域回收方式,保證了G1收集器在有限時(shí)間內(nèi)可以獲取盡可能高的效率,就是 垃圾優(yōu)先 的精髓。
G1收集器的精髓2:不僅如此,在 G1 收集器中,各個(gè)region不是物理分區(qū),僅僅邏輯分區(qū),region的身份是可以切換的,比如一個(gè) old region 經(jīng)過(guò)價(jià)值分析被選中后,就會(huì)被收集,收集之后就變成了 empty region,然后下一次就可以和旁邊的 eden region 連續(xù)起來(lái),就可以分配 新對(duì)象 或 大對(duì)象了,這種 region 身份切換 讓 G1 收集器不受固定分區(qū)的影響,更靈活的處理垃圾收集,這個(gè)其他的垃圾收集器所不具備的。
region 一共八個(gè)身份/角色
FreeTag 空閑區(qū)域
Young Eden SurvTag 年輕代區(qū)域,分為Eden 和 Surv 兩種
HumStartTag 大對(duì)象頭部區(qū)域
HumContTag 大對(duì)象連續(xù)區(qū)域
OldTag 老年代對(duì)象
注意:?jiǎn)蝹€(gè)region大小范圍為 1M ~ 32M,當(dāng)對(duì)象大小超過(guò)單個(gè)region大小的一半,則會(huì)被認(rèn)為是大對(duì)象,放到Humongous里面,大對(duì)象有 HumStartTag + N 個(gè) HumContTag 構(gòu)成,所以用兩種標(biāo)記。
G1收集器運(yùn)行示意圖如下:
對(duì)于上圖的理解是:
G1收集包括四個(gè)步驟:初始標(biāo)記、并發(fā)標(biāo)記、最終篩選、篩選回收
1、多個(gè)用戶線程(圖中是四個(gè))要開始執(zhí)行新生代GC操作,所以都要達(dá)到GC safepoint點(diǎn),先到的要等待晚到的,圖中都達(dá)到了;
2、開始執(zhí)行初始標(biāo)記,初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,并且修改TAMS(Next Top at Mark Start)的值,讓下一個(gè)階段用戶程序并發(fā)標(biāo)記時(shí),能在正確可用的Region上創(chuàng)建新對(duì)象,整個(gè)標(biāo)記階段需要stop the world,讓所有Java線程掛起,這樣JVM才可以安全地來(lái)標(biāo)記對(duì)象。所以只有“初始標(biāo)記”在執(zhí)行,四個(gè)用戶線程都停止了。初始標(biāo)記完成后,達(dá)到第二個(gè)GC safepoint,圖中達(dá)到了;
3、開始執(zhí)行并發(fā)標(biāo)記,并發(fā)標(biāo)記是GCRoot開始對(duì)堆中的對(duì)象進(jìn)行可達(dá)性分析,找出存活的對(duì)象,并發(fā)標(biāo)記可以與用戶線程一起執(zhí)行,并發(fā)標(biāo)記完成后,所有線程(GC線程、用戶線程)達(dá)到下一個(gè)GC safepoint,圖中達(dá)到了;
4、開始執(zhí)行最終標(biāo)記,最終標(biāo)記是為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那部分標(biāo)記記錄,最終標(biāo)記完成后,所有線程達(dá)到下一個(gè)GC safepoint,圖中達(dá)到了;
5、開始執(zhí)行篩選回收,篩選回歸首先對(duì)各個(gè)Region的回收價(jià)值和成本排序, 根據(jù)用戶期待的GC停頓時(shí)間來(lái)制定回收計(jì)劃,篩選回收過(guò)程中,因?yàn)橥nD用戶線程將大幅提高收集效率,所以一般篩選回歸是停止用戶線程的,篩選回歸完成后,所有線程達(dá)到下一個(gè)GC safepoint,圖中達(dá)到了;
6、G1收集器收集結(jié)束,繼續(xù)并發(fā)執(zhí)行用戶線程。
4.3 垃圾收集器常用參數(shù)
(筆者這里加上idea上如何使用這些參數(shù),這些是垃圾收集器的參數(shù),所以這里放到第四部分,在本文第五部分內(nèi)存分配我們會(huì)用到)
參數(shù) | idea中使用方式 | 描述 |
---|---|---|
UseSerialGC | VM Options:-XX:+UseSerialGC | 虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)值,打開此開關(guān)之后,使用Serial+Serial Old的收集器組合進(jìn)行內(nèi)存回收 |
UseParNewGC | VM Options: -XX:+UseParNewGC | 打開此開關(guān)之后,使用ParNew+ Serial Old的收集器組合進(jìn)行內(nèi)存回收 |
UseConcMarkSweepGC | VM Options: -XX:+UseConcMarkSweepGC | 打開此開關(guān)之后,使用ParNew + CMS+ Serial Old的收集器組合進(jìn)行內(nèi)存回收。Serial Old收集器將作為CMS收集器出現(xiàn)Concurrent Mode Failure失敗后的后備收集器使用 |
UseParallelGC | VM Options: -XX:+UseParallelGC | 虛擬機(jī)運(yùn)行在Server模式下的默認(rèn)值,打開此開關(guān)之后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進(jìn)行內(nèi)存回收 |
UseParallelOldGC | VM Options: -XX:UseParallelOldGC | 打開此開關(guān)后,使用Parallel Scavenge + Parallel Old 的收集器組合進(jìn)行內(nèi)存回收 |
SurvivorRatio | VM Options: -XX:SurvivorRatio=8 | 新生代中Eden區(qū)域與Survivor區(qū)域的容量比值,默認(rèn)為8,代表Eden:Survivor=8:1 |
PretenureSizeThreshold | VM Options: -XX:PretenureSizeThreshold=3145728,表示大于3MB都到老年代中去 | 直接晉升到老年代的對(duì)象大小,設(shè)置這個(gè)參數(shù)后,這個(gè)參數(shù)以字節(jié)B為單位大于這個(gè)參數(shù)的對(duì)象將直接在老年代中分配 |
MaxTenuringThreshold | VM Options: -XX:MaxTenuringThreshold=2,表示經(jīng)歷兩次Minor GC,就到老年代中去 | 晉升到老年代的對(duì)象年齡,每個(gè)對(duì)象在堅(jiān)持過(guò)一次Minor GC之后,年齡就增加1,當(dāng)超過(guò)這個(gè)參數(shù)值就進(jìn)入到老年代 |
UseAdaptiveSizePolicy | VM Options: -XX:+UseAdaptiveSizePolicy | 動(dòng)態(tài)調(diào)整Java堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡 |
HandlePromotionFailure | jdk1.8下,HandlePromotionFailure會(huì)報(bào)錯(cuò),Unrecongnized VM option 是否允許分配擔(dān)保失敗,即老年代的剩余空間不足應(yīng)應(yīng)對(duì)新生代的整個(gè)Eden區(qū)和Survivor區(qū)的所有對(duì)象存活的極端情況 | |
ParallelGCThreads | VM Options: -XX:ParallelGCThreads=10 | 設(shè)置并行GC時(shí)進(jìn)入內(nèi)存回收線程數(shù) |
GCTimeRadio | VM Options: -XX:GCTimeRadio=99 | GC占總時(shí)間的比率,默認(rèn)值是99,即允許1%的GC時(shí)間,僅在使用Parallel Scavenge收集器時(shí)生效 |
MaxGCPauseMillis | VM Options:-XX:MaxGCPauseMillis=100 | 設(shè)置GC的最大停頓時(shí)間,僅在使用Parallel Scavenge收集器時(shí)生效 |
CMSInitiatingOccupanyFraction | VM Options:-XX:CMSInitiatingOccupanyFraction=68 | 設(shè)置CMS收集器在老年代空間被使用多少后觸發(fā)垃圾收集,默認(rèn)值68%,僅在使用CMS收集器時(shí)生效 |
UseCMSCompactAtFullCollection | VM Options: -XX:+UseCMSCompactAtFullCollection | 設(shè)置CMS收集器在完成垃圾收集后是否要進(jìn)行一次內(nèi)存碎片的整理,僅在使用CMS收集器時(shí)生效 |
CMSFullGCsBeforeCompaction | VM Options:-XX:CCMSFullGCsBeforeCompaction=10 | 設(shè)置CMS收集在進(jìn)行若干次垃圾收集后再啟動(dòng)一次內(nèi)存碎片整理,僅在使用CMS收集器時(shí)生效 |
五、JVM內(nèi)存分配
新生代GC(Minor GC):發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)镴ava對(duì)象大多具有朝生夕滅的特性,所有Minor GC非常頻繁,一般回收速度較快。
老年代GC(Major GC/):發(fā)生在老年代的GC,出現(xiàn)了major GC,經(jīng)常會(huì)伴隨一個(gè)MinorGC(但是不絕對(duì)),Major GC速度一般比Minor GC慢10倍。
在JVM中,GC分為 full Gc 和 partition gc兩種,full gc是指新生代,老年代,永久區(qū)都gc,即全局gc,其他的所有的,minor gc 和 major gc 都是partion gc。
Major GC 是清理永久代。
Full GC 是清理整個(gè)堆空間—包括年輕代和永久代。
5.1 對(duì)象優(yōu)先在Eden上分配
5.1.1 設(shè)置VM Options
-XX:+PrintGCDetails //打印GC日志 -Xms20M //初始堆大小為20M -Xmx20M //最大堆大小為20M -Xmn10M //年輕代大小為10M,則老年代大小=堆大小20M-年輕代大小10M=10M -XX:SurvivorRatio=8 //年輕代 Eden:Survivor=8 則Eden為8M Survivor0為1M Survivor1為1M -XX:+UseSerialGC //筆者使用的jdk8默認(rèn)為Parallel scavenge+Parallel old收集器組合,書上使用Serial+Serial Old的收集器組合,這里設(shè)置好
5.1.2 程序輸出(給出附加解釋)
第一步:可以看到,當(dāng)分配6M內(nèi)存時(shí),全部都在Eden區(qū),沒有任何問題,說(shuō)明JVM優(yōu)先在Eden區(qū)上分配對(duì)象
第二步:因?yàn)槟贻p代只有9M,剩下1M是給To Survivor用的,已經(jīng)使用了6M,現(xiàn)在申請(qǐng)4M, 就會(huì)觸發(fā)Minor GC,將6M的存活的對(duì)象放到目的survivor中去,但是放不下,因?yàn)槟康膕urvivor只有1M空間,所以分配擔(dān)保到老年代中去,然后將4M對(duì)象放到Eden區(qū)中。所以,最后的結(jié)果是 Eden區(qū)域使用了4096KB 4M 老年代中使用了6M 這里form space占用57%可以忽略不計(jì)。
5.2 大對(duì)象直接進(jìn)入老年代(使用-XX:PretenureSizeThreshold參數(shù)設(shè)置)
5.2.1 設(shè)置VM Options
-XX:+PrintGCDetails //打印GC日志 -Xms20M //初始堆大小為20M -Xmx20M //最大堆大小為20M -Xmn10M //年輕代大小為10M,則老年代大小=堆大小20M-年輕代大小10M=10M -XX:SurvivorRatio=8 //年輕代 Eden:Survivor=8 則Eden為8M Survivor0為1M Survivor1為1M -XX:+UseSerialGC //筆者使用的jdk8默認(rèn)為Parallel scavenge+Parallel old收集器組合,書上使用Serial+Serial Old的收集器組合,這里設(shè)置好 -XX:PretenureSizeThreshold=3145728 // 單位是字節(jié) 3145728/1024/1024=3MB 大于3M的對(duì)象直接進(jìn)入老年代
5.2.2 程序輸出(給出附加解釋)
5.3 長(zhǎng)期存活的對(duì)象應(yīng)該進(jìn)入老年代(使用-XX:MaxTenuringThreshold參數(shù)設(shè)置)
5.3.1 設(shè)置VM Options
-XX:+PrintGCDetails //打印GC日志 -Xms20M //初始堆大小為20M -Xmx20M //最大堆大小為20M -Xmn10M //年輕代大小為10M,則老年代大小=堆大小20M-年輕代大小10M=10M -XX:SurvivorRatio=8 //年輕代 Eden:Survivor=8 則Eden為8M Survivor0為1M Survivor1為1M -XX:+UseSerialGC //筆者使用的jdk8默認(rèn)為Parallel scavenge+Parallel old收集器組合,書上使用Serial+Serial Old的收集器組合,這里設(shè)置好 -XX:MaxTenuringThreshold=1 //表示經(jīng)歷一次Minor GC,就到老年代中去
5.3.2 程序輸出(給出附加解釋)
第一步驟:只分配allocation1 allocation2,不會(huì)產(chǎn)生任何Minor GC,對(duì)象都在Eden區(qū)中
第二步驟:分配allocation3,產(chǎn)生Minor GC,allocation2移入老年區(qū)
第三步驟:allocation3再次分配,allocation1也被送入老年區(qū),老年區(qū)里有allocation1 allocation2
六、尾聲
本文講述JVM自動(dòng)內(nèi)存管理(包括內(nèi)存回收和內(nèi)存),前言部分從操作系統(tǒng)引入JVM,第二部分介紹JVM空間結(jié)構(gòu)(運(yùn)行時(shí)數(shù)據(jù)區(qū)、堆內(nèi)存和非堆內(nèi)存),第三部分介紹HotSpot虛擬機(jī),第四部分和第五部分分別介紹自動(dòng)內(nèi)存回收和自動(dòng)內(nèi)存分配的原理實(shí)現(xiàn)。
到此這篇關(guān)于深入理解JVM自動(dòng)內(nèi)存管理的文章就介紹到這了,更多相關(guān)JVM自動(dòng)內(nèi)存管理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java Poi 在Excel中輸出特殊符號(hào)的實(shí)現(xiàn)方法
這篇文章主要介紹了Java Poi 在Excel中輸出特殊符號(hào)的實(shí)現(xiàn)方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07Java適配器模式的實(shí)現(xiàn)及應(yīng)用場(chǎng)景
適配器模式是Java中一種常用的設(shè)計(jì)模式,它通過(guò)將一個(gè)類的接口轉(zhuǎn)換成客戶端所期望的另一種接口來(lái)實(shí)現(xiàn)不同接口之間的兼容性。適配器模式主要應(yīng)用于系統(tǒng)的接口不兼容、需要擴(kuò)展接口功能以及需要適應(yīng)不同環(huán)境的場(chǎng)景2023-04-04