摘要:所以我們提到的內(nèi)存回收大都是指堆內(nèi)存的回收。根據(jù)堆內(nèi)存對(duì)對(duì)象的代的劃分我們對(duì)堆內(nèi)存有這樣劃分各版本和種類的垃圾回收器各有其用武之地,配合使用它們得到最好的效果十分重要。
這篇文章的素材來自周志明的《深入理解Java虛擬機(jī)》。
作為Java開發(fā)人員,一定程度了解JVM虛擬機(jī)的的運(yùn)作方式非常重要,本文就一些簡單的虛擬機(jī)的相關(guān)概念和運(yùn)作機(jī)制展開我自己的學(xué)習(xí)過程。
java虛擬機(jī)運(yùn)行在受不同操作系統(tǒng)操縱的物理機(jī)上,不同的操作系統(tǒng)使用不同的底層方法來執(zhí)行不同的操作,這些方法稱之為本地方法:Native Method,本地方法一般執(zhí)行的都是比較底層的操作,比如說IO、線程管理等,java方法則會(huì)執(zhí)行的一般是相對(duì)高級(jí)的操作,比如說數(shù)邏運(yùn)算,或者是調(diào)用底層的本地方法來完成底層任務(wù)。
java虛擬機(jī)的運(yùn)行時(shí)數(shù)據(jù)區(qū)域?qū)?nèi)存分成了不同的部分協(xié)調(diào)完成java虛擬機(jī)的內(nèi)存數(shù)據(jù)交互。
按照數(shù)據(jù)存儲(chǔ)過程的數(shù)據(jù)結(jié)構(gòu)可以大致分為:
棧區(qū):
虛擬機(jī)棧:java虛擬機(jī)運(yùn)行的java方法(java字節(jié)碼方法)構(gòu)成的??臻g,這個(gè)空間在運(yùn)行時(shí)存儲(chǔ)這些方法的局部變量表、操作棧、動(dòng)態(tài)鏈接和方法出口;
本地方法棧:本地方法在運(yùn)行時(shí)存儲(chǔ)數(shù)據(jù)產(chǎn)生的棧區(qū)。
堆區(qū):
java堆:對(duì)象的實(shí)例存儲(chǔ)在這個(gè)共享的堆空間里,由于占有最大的和最有實(shí)際意義的空間,這個(gè)空間的GC過程時(shí)虛擬機(jī)運(yùn)行的重點(diǎn)。
方法區(qū):存儲(chǔ)虛擬機(jī)運(yùn)行時(shí)加載的類信息、常量、靜態(tài)變量和即時(shí)編譯的代碼,因此可以把這一部分考慮為一個(gè)保存相對(duì)來說數(shù)據(jù)較為固定的部分,常量和靜態(tài)變量在編譯時(shí)就確定下來進(jìn)入這部分內(nèi)存,運(yùn)行時(shí)類信息會(huì)直接加載到這部分內(nèi)存,所以都是相對(duì)較早期進(jìn)入內(nèi)存的。
運(yùn)行時(shí)常量池:不是所有的常量都是在編譯時(shí)就確定下來進(jìn)入內(nèi)存的,仍然會(huì)有運(yùn)行時(shí)才進(jìn)入內(nèi)存的常量,這部分常量一般是編譯時(shí)產(chǎn)生的一些固定信息,比如說翻譯出的引用等,直接在類加載的時(shí)候把它們存入運(yùn)行時(shí)常量池有助于提高性能。
所有的內(nèi)存區(qū)域的數(shù)據(jù)交互由程序計(jì)數(shù)器指導(dǎo)虛擬機(jī)完成復(fù)雜的邏輯步驟。
如何找到一個(gè)對(duì)象的實(shí)例:
Object obj = new Object();
在這個(gè)過程中在虛擬機(jī)棧的局部變量表里創(chuàng)建obj引用,在堆內(nèi)存里創(chuàng)建Object類的一個(gè)實(shí)例,最后就是把obj引用和這個(gè)對(duì)象實(shí)例關(guān)聯(lián)起來的問題了,另外,我們需要知道的是,不是所有的實(shí)例都完整地保存了所有的類的信息,一般共有的或者靜態(tài)的類的數(shù)據(jù)將被保存在方法區(qū)中,獨(dú)有的實(shí)例數(shù)據(jù)才會(huì)真的被保存在java堆里,因此每個(gè)引用必須同時(shí)找到關(guān)聯(lián)它的實(shí)例數(shù)據(jù)和類數(shù)據(jù)。針對(duì)這個(gè)問題,有兩個(gè)辦法來做:
I. 引用存儲(chǔ)的只是實(shí)例的句柄,句柄在堆的句柄池中,句柄中保存著到堆中真正實(shí)例的地址和到方法區(qū)中類數(shù)據(jù)的地址,這樣就可以通過這個(gè)句柄可以找到這些地址。
II. 引用存儲(chǔ)的就是實(shí)例在堆中的地址,而實(shí)例中是含有可以定位類數(shù)據(jù)的地址的,也就是通過找到的實(shí)例地址可以再去尋找它對(duì)應(yīng)的類的數(shù)據(jù)。
兩個(gè)和內(nèi)存溢出相關(guān)的異常:
StackOverflowError:線程申請(qǐng)的棧深度大于虛擬機(jī)的規(guī)定值;
OutOfMemoryError:線程擴(kuò)展增加的內(nèi)存大于虛擬機(jī)的要求;
內(nèi)存回收機(jī)制虛擬機(jī)棧、本地方法棧和計(jì)數(shù)器大都是編譯期確定的內(nèi)存分配,在線程執(zhí)行完畢后即會(huì)清理,內(nèi)存回收相對(duì)比較容易。所以我們提到的內(nèi)存回收大都是指堆內(nèi)存的回收。我們通過如下幾個(gè)問題來說明內(nèi)存回收機(jī)制:
1. 什么樣的堆內(nèi)存是可以回收的呢?什么樣的堆內(nèi)存是可以回收的呢?簡而言之就是那些“沒用”的內(nèi)存,那么怎樣的內(nèi)存是“沒用”的呢?即那些通過現(xiàn)有的指針(或稱“引用”)條件下再也訪問不到的內(nèi)存對(duì)象。所以有這樣的算法來描述無效的引用:
(引用計(jì)數(shù)算法)每個(gè)對(duì)象都有一個(gè)被引用計(jì)數(shù)器,被引用一次計(jì)數(shù)器加1,引用被置空時(shí)減1,最終被引用計(jì)數(shù)器的值為0 的即是“無用”的內(nèi)存對(duì)象,它占用的內(nèi)存可以被回收。
(這個(gè)算法看起來好像沒有問題,但是遭遇到循環(huán)引用的時(shí)候就會(huì)出現(xiàn)問題:如果同時(shí)將循環(huán)引用的雙方置空,那么即使被引用計(jì)數(shù)器不為0也再也訪問不到這些對(duì)象了,即發(fā)生了內(nèi)存無故占用)。
這個(gè)過程體現(xiàn)了互相循環(huán)引用可能帶來的問題,對(duì)象仍被引用但是已經(jīng)不能被訪問了,所以是這種算法的缺陷。
(根搜索算法)將由棧內(nèi)存或方法區(qū)引用的對(duì)象作為GCRoots去構(gòu)建引用鏈,如果能找到這個(gè)對(duì)象則說明這個(gè)對(duì)象能夠訪問其內(nèi)存不能被回收,反之通過這些引用鏈找不到這個(gè)對(duì)象則說明已經(jīng)是棄用的對(duì)象了,其內(nèi)存是應(yīng)該被回收的。(上面的互相循環(huán)引用的例子就可以解決了,因?yàn)檫@個(gè)問題里面雖然其被引用計(jì)數(shù)器的值不為0,但是已經(jīng)沒有GCRoots能夠找到這些內(nèi)存了,這個(gè)問題里的GC Roots是棧內(nèi)存里的objA和objB,這兩個(gè)棧內(nèi)存里的引用被置空,因此引用鏈里沒辦法再找到對(duì)內(nèi)存里的對(duì)象了。)
2. 確定了有哪些內(nèi)存該被回收后GC機(jī)制是直接回收內(nèi)存嗎?確定了有哪些內(nèi)存該被回收后GC機(jī)制是直接回收內(nèi)存嗎?GC會(huì)給這些內(nèi)存中的某些對(duì)象一次機(jī)會(huì),就是那些重寫過finalize方法的類的對(duì)象,GC會(huì)執(zhí)行這個(gè)對(duì)象重寫過的finalize方法,如果在這個(gè)方法中對(duì)象重新將自己鏈接給了某個(gè)引用使得這塊內(nèi)存區(qū)域重新可以被訪問,那么GC就不會(huì)在這次回收它,但是,這個(gè)過程只能執(zhí)行一次,下一次再被GC遇到的話就不會(huì)顧及這個(gè)finalize方法而是直接回收了,因此要注意重寫的finalize方法只能執(zhí)行一次。
這個(gè)是堆內(nèi)存中對(duì)象的回收,在方法區(qū)里保存類信息和常量池的內(nèi)存同樣需要回收,這個(gè)過程相對(duì)來說更緩慢也并沒有那么高效,因?yàn)橐欢螘r(shí)間內(nèi)線程使用的類和常量池都比較穩(wěn)定,只有當(dāng)真的確認(rèn)有類不再使用且不被反射使用的時(shí)候才會(huì)卸載類,當(dāng)真的沒有常量再被使用的時(shí)候才會(huì)釋放常量池中不用的常量。
知道了哪些內(nèi)存該被回收、回收前的最后確認(rèn)之后來說內(nèi)存回收策略,也就是內(nèi)存回收的時(shí)候究竟是依據(jù)什么樣的算法進(jìn)行的?
(標(biāo)記-清除算法)
(復(fù)制算法)
(標(biāo)記-整理算法)
通過這些算法,jvm可以將已不被引用的無效內(nèi)存回收,標(biāo)記-清除算法清理得到的內(nèi)存往往出現(xiàn)碎片,而標(biāo)記-整理解決了內(nèi)存碎片卻增加了時(shí)間消耗,復(fù)制算法則會(huì)出現(xiàn)內(nèi)存浪費(fèi)的問題,結(jié)合不同場景使用不同算法進(jìn)行垃圾回收是十分重要的。
4. 主流垃圾回收收集器了解了內(nèi)存垃圾回收的算法,我們來看執(zhí)行垃圾回收的垃圾收集器。根據(jù)堆內(nèi)存對(duì)對(duì)象的代的劃分我們對(duì)堆內(nèi)存有這樣劃分:
各版本和種類的垃圾回收器各有其用武之地,配合使用它們得到最好的效果十分重要。因?yàn)樵诶鴥?nèi)存回收的過程中對(duì)每個(gè)對(duì)象分代處理,所以對(duì)不同代的垃圾內(nèi)存有不同的收集器去回收:創(chuàng)建不久的對(duì)象稱為新生代,新生代對(duì)象的特點(diǎn)即是生死頻率高,從生到死的過程很短,所以再回收時(shí)有大量的這樣的內(nèi)存存在,所以采用復(fù)制算法采用較大的eden:survivor比率將使得內(nèi)存較完整也較快地回收,同時(shí),老年代的內(nèi)存存儲(chǔ)的是創(chuàng)建很久仍然沒有失去引用的對(duì)象,這類對(duì)象由于長期存在于內(nèi)存中且未來的生死也常常不確定,所以需要使用速度慢但是更精確地標(biāo)記-整理算法。下面是真正執(zhí)行這些回收過程的收集器:
新生代收集器:(主要使用復(fù)制算法)
Serial收集器:單線程+“Stop the World”停頓式收集
ParNew收集器:多線程版本的Serial收集器
Parallel Scavenge收集器:多線程收集器,關(guān)注“吞吐量”
老年代收集器:(主要使用標(biāo)記-整理算法)
Serial Old收集器:Serial的老年代版本
Parallel Old收集器:Parallel的老年代版本
CMS收集器:并發(fā)收集、低停頓,關(guān)注短時(shí)間停頓
G1收集器:高級(jí)和領(lǐng)先的新型垃圾收集器
5. 內(nèi)存分配和回收的全過程:JVM虛擬機(jī)將會(huì)依次對(duì)每次即將進(jìn)入堆內(nèi)存的對(duì)象做出安排,一定時(shí)間間隔內(nèi)對(duì)于失去引用的無效內(nèi)存進(jìn)行回收,當(dāng)內(nèi)存出現(xiàn)溢出的時(shí)候試圖通過垃圾回收自發(fā)解決問題保持系統(tǒng)回歸平穩(wěn)。
申請(qǐng)內(nèi)存的對(duì)象優(yōu)先被分配到堆內(nèi)存的Eden區(qū),如果Eden區(qū)的空間不足就向survivor區(qū)上放,如果仍然放不下就會(huì)引發(fā)一次發(fā)生在新生代的minor GC,在這次GC過程中,如果發(fā)現(xiàn)仍然又放不下的對(duì)象,就將這些對(duì)象放入老年代內(nèi)存里去(這種現(xiàn)象是對(duì)垃圾回收的統(tǒng)計(jì)學(xué)規(guī)律的挑戰(zhàn),因?yàn)槔碚撋洗蠖鄶?shù)新生代內(nèi)存不應(yīng)該存活到這個(gè)時(shí)候,所以這個(gè)時(shí)候就會(huì)引發(fā)這種叫做分配擔(dān)保機(jī)制的對(duì)象向老年代轉(zhuǎn)移),如果存在失去引用的內(nèi)存,那么就將剩余存活的對(duì)象移往survivor區(qū),剩下的Eden區(qū)內(nèi)存全部清理。
大對(duì)象直接進(jìn)入老年區(qū),上面的描述中我們已經(jīng)可以看到大的對(duì)象在一旦出現(xiàn)長時(shí)間存活的時(shí)候會(huì)引發(fā)分配擔(dān)保機(jī)制進(jìn)入老年區(qū),所以不如直接在剛開始創(chuàng)建這個(gè)對(duì)象的時(shí)候就把它放入老年區(qū)。
長期存活的對(duì)象直接進(jìn)入老年區(qū):同上面的描述,長期存活的對(duì)象的移動(dòng)會(huì)耗費(fèi)資源,所以在創(chuàng)建這些長期存活的對(duì)象時(shí)就將它直接放入老年區(qū)。
動(dòng)態(tài)對(duì)象的年齡判斷:虛擬機(jī)并不是一直等待所有的對(duì)象都到達(dá)老年代的標(biāo)準(zhǔn)才將它們放入老年期,因?yàn)槟菢幼隹赡軙?huì)使新生代的空間一直很緊張引發(fā)不必要的GC,所以在當(dāng)Survivor區(qū)里的對(duì)象中相同年齡的對(duì)象的大小達(dá)到Survivor區(qū)的一半時(shí)就可以將其移入老年區(qū)。
空間分配擔(dān)保:當(dāng)每次執(zhí)行minor GC的時(shí)候應(yīng)該對(duì)要晉升到老年代的對(duì)象進(jìn)行分析,如果這些馬上要到老年區(qū)的老年對(duì)象的大小超過了老年區(qū)的剩余大小,那么執(zhí)行一次Full GC以盡可能地獲得老年區(qū)的空間。
6.一個(gè)借助VisualVM工具探查JVM內(nèi)存管理的實(shí)例這里我們使用一個(gè)實(shí)例借助VusualVM來查看程序運(yùn)行過程中的虛擬機(jī)內(nèi)存分配的過程:
在這個(gè)例子中,各種參數(shù)均使用默認(rèn)值:
public class VMTest { private static final int _1MB = 1024*1024; public static void main(String[] args) throws InterruptedException { Thread.sleep(4000); byte[] allocation1; for (int i = 0; i < 400; i++) { allocation1 = new byte[_1MB]; System.out.println("Create One"+i); Thread.sleep(1000); } } }
這個(gè)例子中,主線程每次循環(huán)向虛擬機(jī)申請(qǐng)內(nèi)存創(chuàng)建新對(duì)象,然后在循環(huán)結(jié)束的時(shí)候?qū)⒁面溄拥叫碌膶?duì)象,原來的對(duì)象就會(huì)處于失去引用的狀態(tài),每隔一段時(shí)間后JVM的minor GC就會(huì)使得這些棄用的對(duì)象占據(jù)的內(nèi)存被回收。以下即是這個(gè)過程中VisualVM展示的的實(shí)時(shí)內(nèi)存各區(qū)占據(jù)情況:
這個(gè)過程中,我們可以清楚地看出內(nèi)存分配的全過程。新的對(duì)象作為新生代對(duì)象會(huì)被分配到新生區(qū)的Eden區(qū)中,在一個(gè)循環(huán)中這些對(duì)象都會(huì)被分配到Eden區(qū)中,因?yàn)镋den區(qū)默認(rèn)的超過600M的空間足夠容納這些對(duì)象,當(dāng)一段時(shí)間后發(fā)生minor GC的時(shí)候就會(huì)將仍然存活的(也就是仍然有有效引用的)對(duì)象移至空的Survivor區(qū),在這里是Survivor0區(qū),失去引用的對(duì)象占據(jù)的Eden區(qū)空間將會(huì)被回收;下一次monor GC到來之前仍然會(huì)進(jìn)行這樣的空間分配,Eden區(qū)中會(huì)產(chǎn)生新的對(duì)象并有一些對(duì)象會(huì)失去有效引用,下一次minor GC到來的時(shí)候會(huì)把Eden區(qū)中存活的對(duì)象(以及Survivor0中存活的對(duì)象)移至空的Survivor區(qū)中,這里是Survivor1,并將Eden和Survivor0回收。注意,每次minor GC進(jìn)行的時(shí)候都會(huì)將一個(gè)Survivor(from Space)置空,并將存活的對(duì)象移至空Survivor(to Space)里,如果Survivor(to Space)空間不足,則會(huì)引發(fā)分配擔(dān)保機(jī)制將這些存活對(duì)象移至老年區(qū)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/64656.html
摘要:看來還是功力不夠,索性拆成了六篇文章,分別從自動(dòng)內(nèi)存管理機(jī)制類文件結(jié)構(gòu)類加載機(jī)制字節(jié)碼執(zhí)行引擎程序編譯與代碼優(yōu)化高效并發(fā)六個(gè)方面來做更加細(xì)致的介紹。本文先說說虛擬機(jī)的自動(dòng)內(nèi)存管理機(jī)制。在類加載檢查通過后,虛擬機(jī)將為新生對(duì)象分配內(nèi)存。 歡迎關(guān)注微信公眾號(hào):BaronTalk,獲取更多精彩好文! 書籍真的是常讀常新,古人說「書讀百遍其義自見」還是蠻有道理的。周志明老師的這本《深入理解 Ja...
摘要:如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。分配內(nèi)存在類加載檢查通過后,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存。程序計(jì)數(shù)器主要有兩個(gè)作用字節(jié)碼解釋器通過改變程序計(jì)數(shù)器來依次讀取指令,從而實(shí)現(xiàn)代碼的流程控制,如順序執(zhí)行選擇循環(huán)異常處理。 目錄介紹 01.Java對(duì)象的創(chuàng)建過程 1.0 看下創(chuàng)建類加載過程 1.1 對(duì)象的創(chuàng)建 1.2 對(duì)象的內(nèi)存布局 02.Java內(nèi)存區(qū)域 2.0 運(yùn)行...
摘要:執(zhí)行引擎作用執(zhí)行字節(jié)碼,或者執(zhí)行本地方法運(yùn)行時(shí)數(shù)據(jù)區(qū)其實(shí)就是指在運(yùn)行期間,其對(duì)內(nèi)存空間的劃分和分配。 雖是讀書筆記,但是如轉(zhuǎn)載請(qǐng)注明出處https://uestc-dpz.github.io..拒絕伸手復(fù)制黨 JVM Java 虛擬機(jī) Java 虛擬機(jī)(Java virtual machine,JVM)是運(yùn)行 Java 程序必不可少的機(jī)制。JVM實(shí)現(xiàn)了Java語言最重要的特征:即平臺(tái)...
摘要:新生代又被劃分為三個(gè)區(qū)域和兩個(gè)幸存區(qū)。這樣劃分的目的是為了使能夠更好地管理堆內(nèi)存中的對(duì)象,包括內(nèi)存的分配及回收。新生代主要存儲(chǔ)新創(chuàng)建的對(duì)象和尚未進(jìn)入老年代的對(duì)象。 在Java中主要有以下三種類加載器: 引導(dǎo)類加載器(bootstrap class loader) --用來加載java的核心庫(Strin...
摘要:一次性編譯成機(jī)器碼,脫離開發(fā)環(huán)境獨(dú)立運(yùn)行,運(yùn)行效率較高。解釋型語言使用專門的解釋器對(duì)源程序逐行解釋成特定平臺(tái)的機(jī)器碼并立即執(zhí)行的語言。垃圾回收機(jī)制保護(hù)程序的完整性,垃圾回收是語言安全性策略的一個(gè)重要部分。 Java程序運(yùn)行機(jī)制 編譯型語言 使用專門的編譯器,針對(duì)特定平臺(tái)(操作系統(tǒng))將某種高級(jí)語言源代碼一次性翻譯成可被該平臺(tái)硬件執(zhí)行的機(jī)器碼(包括機(jī)器指令和操作數(shù)),并包裝成該平臺(tái)所能識(shí)...
閱讀 1281·2023-04-25 23:22
閱讀 1680·2023-04-25 20:04
閱讀 2654·2021-11-22 15:24
閱讀 2816·2021-11-11 16:54
閱讀 1894·2019-08-30 14:03
閱讀 1492·2019-08-29 16:35
閱讀 1711·2019-08-26 10:29
閱讀 2679·2019-08-23 18:01