對於學過C++
的開發者而言,他們對內存的分配與回收確定不陌生,由於他們要對每個對象負責(從建立到結束
)。可是對於Java程序員來講,就不須要考慮那麼多,由於虛擬機的內存管理機制
能夠幫助咱們自動的管理內存
,咱們再也不須要爲每個new操做去寫配對的delete/free代碼 。html
既然虛擬機都這麼方便了,那麼咱們爲何還要學內存管理呢,這不是自討苦吃麼,事實上,虛擬機的自動內存管理確實能幫助咱們減小內存泄漏和內存溢出
的狀況;可是也正由於咱們把內存的控制權交給了虛擬機,一旦出現內存泄漏和內存溢出的問題,若是咱們不瞭解虛擬機是怎麼使用的內存的,那麼咱們該怎麼解決呢?因此,咱們很是有必要學習內存管理,學習它不是爲了本身控制管理內存,而是在出現問題的可以有效的定位並予以解決。java
因此,讓咱們一塊兒來學習吧。程序員
Java虛擬機將Java程序執行的區域稱爲運行時數據區
,根據各自功能不一樣將運行時數據區劃分爲若干個不一樣的區域,具體分爲兩大塊,線程共享
部分和線程私有
部分。線程共享部分能夠分爲堆
、方法區
(jdk1.8後這塊區域被稱爲元空間
);線程私有部分能夠分爲虛擬機棧
、本地方法棧
和程序計數器
。算法
上述的這些區域都有各自的用途,下面讓咱們一個個來學習學習他。緩存
程序計數器
是一塊較小的內存空間,它能夠看作是當前線程所執行的字節碼的行號指示器
,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支
、跳轉
、循環
、異常處理
、線程恢復
等基礎功能都須要依賴這個計數器來完成。安全
這是《深刻理解Java虛擬機》書籍對程序計數器的介紹,事實上,在此基礎應該補充上,程序計數器是線程私有,在執行Java方法時有值,可是在執行native方法時,程序計數器值爲空。markdown
有沒有看懵,懵了也不要緊,下面咱們抽出程序計數器的特色,並介紹每一個特色的來源及做用。多線程
首先,爲何線程私有呢,咱們都瞭解Java虛擬機的多線程是經過輪流切換
、分配處理器
的執行時間的方式來實現的,也就是說,在同一時刻一個處理器內核只會執行一條線程,處理器切換時並不會記錄上一個線程執行到那個位置,因此爲了線程切換後依然可以恢復到原位,每條線程都須要有各自的獨立的程序計數器
,計數器之間互不影響,獨立存儲。併發
這個特色列出來好像有點白癡,咱們在上面都已說了它是行號計數器,那確定是有值啊,那麼咱們還要單獨列出來呢,咱們單獨列出來一方面是爲了與執行native方法比較,另外一發面我是想解釋下線程執行字節碼時,這個行號指示器究竟是個啥?好吧,通俗來說,JAVA代碼經javac編譯後的得字節碼在未通過JIT(實時編譯器)編譯前,其執行方式是經過「字節碼解釋器」進行解釋執行的,其工做原理就是爲解釋器讀取裝載入內存的字節碼,按照順序讀取字節碼指令,這個過程就是行號指示器在不斷變化的過程。當讀取一條指令後,就講該指令「翻譯」成固定的操做,並根據這些操做進行分支、循環、跳轉等流程。jvm
當執行native本地方法
時,程序計數器是空的,這是由於native方法是java調用本地的C/C++庫
,能夠近似的認爲native方法至關於C/C++暴露給java的一個接口
,java經過調用這個接口從而調用到C/C++方法。因爲該方法是經過C/C++而不是java進行實現。那麼天然沒法產生相應的字節碼,而且C/C++執行時的內存分配是由本身語言決定的,而不是由JVM決定的。
NOTE:學到這裏,相信你對程序計數器已經瞭解的的差很少了,可是你可能還存在這樣的疑惑,程序計數器佔用的內存那麼小,會不會拋出內存溢出錯誤OutOfMemorryError
,別擔憂,不會出現錯誤的,既然程序計數器存儲的是字節碼文件的行號,那麼程序字節碼執行的範圍確定是已知的,在虛擬機將字節碼文件加載進內存時就已經分配一個絕對不可能的溢出的內存,爲啥會提早知道,由於字節碼文件包含的有相關信息,若是想要更加具體的瞭解,能夠看看個人上一篇文章,認識Class文件結構
好了,學習完程序計數器,咱們接下來學習線程私有的另外一部份內容:Java虛擬機棧
。
虛擬機棧描述的是Java方法執行的內存模型
,每一個方法被執行的時候,Java虛擬機都會同步建立一個棧幀
用於存儲局部變量表
、操做數棧
、動態連接
、方法出口
等信息。每個方法被調用直至執行完畢的時候,就對應着一個棧幀從入棧到出棧的過程。
看到上面這麼長的定義可能有點懵逼,棧幀是個啥,裏面存的都是些啥玩意,我學它幹啥,搞得挺痛苦的。莫慌,咱們一個個解釋,看完個人解釋後絕對讓你喊出「真香」。
首先,既然虛擬機棧描述的是Java方法的內存模型,那咱們就認爲他是存儲Java方法集合
的內存,而棧幀就能夠認爲集合中的一個方法,方法間的調用就對用着棧幀的調用,當執行一個方法,就將該方法的棧幀壓入棧頂,方法執行完就退出棧,也即從方法集合中去掉。
抽象?來一張圖看看
虛擬機棧裏存儲的是一個個棧幀,棧幀裏面包含啥啊?下面,咱們下先看一張圖來直觀感覺下
局部變量表
是一組變量值存儲空間,用於存放方法參數
和方法內部定義
的局部變量。在Java程序編譯爲Class文件時就在方法的code屬性的max_locals數據項中肯定了該方法所須要分配的局部變量表的最大容量。局部變量能夠存放基本數據類型
(boolean、byte、char、short、int、float、long、double)和對象引用類型
(reference)。
局部變量是以變量槽
(Slot)爲單位,每一個槽的容量爲32位
,因此對於小於32位的類型佔用一個變量槽,64位長度的long和double類型的數據會佔用兩個變量槽。
JVM會爲局部變量表中的每個slot都分配一個訪問索引
,經過這個索引就能夠成功的訪問到局部變量表中的指定局部變量值。當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照聲明順序被複制到局部變量表中的每個slot上。
Note:棧幀中的局部變量表中的槽位是能夠重複利用的,若是一個局部變量過了其做用域,那麼在其做用域以後申明的新的局部變量就頗有可能會複用過時局部變量的槽位,從而達到節省資源的目的。
本地方法棧
也是線程私有的部分,本地方法棧與虛擬機棧方法做用類似,其區別僅是虛擬機棧爲Java方法服務,而本地方法棧則是爲虛擬機使用到的本地方法服務。
Java堆
是虛擬機的內存中的最大一塊,它能被全部的線程共享,在虛擬機啓動時建立,咱們在Java代碼編寫的對象實例就存在這快內存區域。
堆究竟是個什麼樣的結構呢?它由那幾部份組成呢,每部分都各自有什麼做用呢?
別慌,咱們一個一個來。
咱們都知道對象的存活是有周期的,若是一個對象沒有被引用,那麼就能夠認爲該對象能夠被清除掉了,就是咱們認爲的垃圾。因爲每一個對象存活的時間不一樣,爲了減小GC線程掃描垃圾時間及頻率,咱們能夠將存活時間較長的對象單獨放一個區域。所以,堆的佈局也就肯定下來了。總的來講,堆被劃分紅兩部分:新生代和老年代。
新生代和老年代比值爲1:2
,這個比例並非惟一的,咱們能夠能夠經過參數 –XX:NewRatio
按照具體的場景來指定。
若是再細粒度的劃分,新生代又能夠分爲Eden區
和Survivor區
,而Survivor區
又能夠分爲FromSurvivor
和ToSurvivor
,默認比值爲8:1:1
這時問題又來了,爲何要將Survivor分爲兩塊相等大小的空間啊?好問題,我先說答案,這兩分爲兩部分主要是爲了解決內存碎片化
的問題,若是內存碎片化嚴重,也就是兩個對象佔用不連續的內存,已有的連續內存不夠新對象存放,就會觸發GC。不懂GC的先暫時這樣理解,在下一篇文章垃圾回收算法時,我會重點講解。
知道堆的內存結構佈局後,咱們聊一聊對象是如何在堆中建立的。
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過,若是沒有,先執行相應的類加載過程,接下來爲新生對象分配內存。爲對象分配空間的任務等同於把一塊肯定大小的內存從堆中劃分出來。劃分方式按照堆內存是否規整分爲兩種。
能夠採用指針碰撞
方式解決,即全部被使用過的內存都放到一邊,空閒的內存被放到另外一邊,中間放着一個指針做爲分界點的指示器,那所分配的內存就僅僅是把那個指針向空閒空間方向挪動一段與對象大小相等的距離。
能夠採用空閒列表
的方式解決,空閒和使用的內存相互交錯,JVM必須維護一個列表,記錄哪些內存塊是可用的,分配時候找到一塊足夠大的分配給對象實例。
咱們還有一個問題值得考慮的是,若是在併發狀況下,對象的建立是否安全呢,會不會出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存。廢話,確定會出現這樣的狀況,能夠有兩種辦法解決:①能夠對分配內存空間的動做進行同步處理,這其實是虛擬機採用CAS
加上重試機制
保證更新操做的原子性。②把內存分配的動做按照線程劃分在不一樣的空間中進行,即每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB)
,哪一個線程要分配內存,就在哪一個線程的TLAB上分配,只有TLAB用完並分配心得TLAB時,才須要同步鎖定。
方法區
與Java堆同樣,是各個線程共享的內存區域,在jdk1.8後,這部份內存被放置在元空間中,是一種邏輯內存
部分。它是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類型信息
、常量
、靜態變量
、即時編譯器編譯後的代碼緩存數據,這些信息是由類加載時從類文件中提取出來的。
除此以外,方法區還具有如下特色:
由於jvm在運行應用時要大量使用存儲在方法區中的類型信息,因此在類型信息的表示上,設計者除了要儘量提升應用的運行效率外,還要考慮空間問題。根據不一樣的需求,jvm的實現者能夠在時間和空間上追求一種平衡,具體體如今方法區的大小沒必要是固定的,根據應用的須要動態調整。一樣方法區也沒必要是連續的。方法區能夠在堆(甚至是虛擬機本身的堆)中分配。jvm能夠容許用戶和程序指定方法區的初始大小,最小和最大尺寸。
由於方法區是被全部線程共享的,因此必須考慮數據的線程安全。假如兩個線程都在試圖找lava的類,在lava類尚未被加載的狀況下,只應該有一個線程去加載,而另外一個線程等待。
這篇文章詳細講解了Java虛擬機內存的各部分區域,這部份內容很是重要,接下來的文章:類加載機制、內存分配和垃圾回收算法都是以這篇爲基礎的。
好了,今天的文章就到這裏了,我是Simon郎
,一個想要天天博學一點點的少年郎,若是這篇文章對你有幫助,歡迎在看
、點贊
、轉發
哦!
參考文獻
[1]https://www.cnblogs.com/yanl55555/p/12616356.html
[2]深刻理解JVM虛擬機.周志華