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