在上一篇文章中,咱們花了較大的篇幅去介紹了JVM的運行時數據區,而且重點介紹了棧區的結構及做用,相關內容請猛戳!在本文中,咱們將主要介紹對象的建立過程及在堆中的分配方式。java
相關連接(注:文章講解JVM以Hotspot虛擬機爲例,jdk版本爲1.8,我的技術博客www.17coding.info)
一、 你必須瞭解的java內存管理機制-運行時數據區
二、 你必須瞭解的java內存管理機制-內存分配
三、 你必須瞭解的java內存管理機制-垃圾標記
四、 你必須瞭解的java內存管理機制-垃圾回收算法
在上文咱們提過一些問題,你的對象是怎麼new出來的?new出來又放在哪裏?怎麼引用的? 老規矩,咱們仍是經過字節碼來了解一下。安全
public static void main (String[] args){
People p = new People(); }
這樣的代碼你們一點也不會陌生,咱們都知道使用new關鍵字能夠建立一個對象,對應的字節碼以下
咦!一看字節碼才知道,咱們的一行new的代碼,對應的字節碼原來要作這麼多操做!咱們逐一來分析一下。多線程
JVM遇到new指令時,先檢查指令參數(上面字節碼中的#2)是否能在常量池中定位到一個類的符號引用(上面最終定位到常量池中的com/test/entity/People):
1)、若是能定位到,檢查這個符號引用表明的類是否已被加載、解析和初始化過;
2)、若是不能定位到,或沒有檢查到,就先執行相應的類加載過程;
具體類的加載、解析、初始化的過程你們能夠去查找JVM類加載機制相關資料,這裏就不展開啦!咱們須要知道的是這一步保證了在方法區中,存在要建立實例對象的類對象!jvm
我們到了適婚年齡,也就該找個對象了吧!你看上了一個姑娘,長得楚楚動人,就跑去跟他媽說:「我要一個對象,把你女兒嫁給我吧!」。她媽媽卻是十分爽快:「好啊,我女兒總得有個地方住吧,小夥子你有房嗎?」。這時候場面一度十分尷尬,內心嘀咕着「要是國家能分配房子就行了!」。這在當前社會顯然不現實,畢竟我們還沒進入共產主義社會!然而在JVM王國裏,對象住的「房子」倒是「國家」統一分配的。國家集中圈了一大塊「地」,誰家要娶「媳婦」,就給他家分配一塊「地」,「媳婦」胖點呢,地就大一點,「媳婦」瘦一點呢,「地」就小一點。在這裏,你一我的能夠同時擁有多個對象,在這裏,多我的能夠擁有同一個對象。因此這裏的老百姓安居樂業、這裏一片祥和……固然,因爲這塊「地」大小有限,而你又同時擁有不少對象,還有其餘人也要娶對象,因此那些不用了的對象的「地」國家就會進行統一徵收(固然這裏不會給補貼,畢竟是免費分配的~)以繼續分給其餘人用。
上面扯了這麼多,相信你已經知道「你」就表明着一個線程,「國家」指的是JVM,「國家」圈的一塊「地」就是堆空間,你娶的「對象」就是實例對象,「國家」分配地的動做就是內存分配,而國家徵收的動做就是垃圾回收。
因爲要找對象的人太多了,因此分配的操做也很頻繁,那麼擺在「國家」的問題就來了:怎麼合理分配?怎麼最大限度的提升空間利用率?怎麼提升分配效率?不用了的空間怎麼回收?怎麼知道哪些空間不用了?上面不少問題都須要結合後面的垃圾回收相關的內容來討論,這裏只討論分配內存的方式。
一個對象須要佔用多大的內存?這個問題其實在類加載完成後就已經肯定啦!JVM能夠經過普通java對象的類元信息肯定對象大小。爲對象分配內存至關與把一塊肯定大小的內存從java堆中劃分出來。那麼問題來了,這麼大的一塊堆空間擺在JVM的面前,JVM該劃哪一塊空間來分配內存呢?隨機找一塊空間分配算了?or緊挨着以前分配的空間後面進行分配?這裏須要說到的是兩種分配方式:
1)、 指針碰撞
若是Java堆是絕對規整的:一邊是用過的內存,一邊是空閒的內存,中間一個指針做爲邊界指示器,分配內存只需向空閒那邊移動指針,這種分配方式稱爲"指針碰撞"(Bump the Pointer)。這裏有個條件就是「絕對規整」,相似下圖,左邊全是被綠過了的,右邊則全是等着被綠的。新分配對象時候就是多綠了一塊,邊界指示器向後移動!
性能
2)、 空閒列表
若是Java堆不是規整的:用過的和空閒的內存相互交錯。須要維護一個列表,記錄哪些內存可用。分配內存時查表找到一個足夠大的內存,並更新列表,這種分配方式稱爲"空閒列表"(Free List)。相似下圖,好好的一塊內存被綠得亂七八糟,用上面指針碰撞的方式是碰不動了!因此就用一個小本本記着哪裏有多大的空閒空間能夠綠!固然下圖的地址編號是虛擬的,空閒列表的樣子也是我意淫出來的,表達的意思你懂就行!
this
咱們能看到,致使這兩種方式的差別主要取決於java堆是否規整,而java堆是否規整又是由jvm採用的垃圾收集器是否帶有壓縮功能決定的。使用Serial、ParNew等帶Compact過程的收集器時,JVM採用指針碰撞方式分配內存。而使用CMS這種基於標記-清除(Mark-Sweep)算法的收集器時,採用空閒列表方式。(下篇文章會具體介紹不一樣的垃圾收集器)
不論是指針碰撞仍是空閒列表,都會存在同一個問題,那就是在多線程的場景下的線程安全問題。多個線程同時在new的時候把對象分配到同一塊內存了咋辦,不得幹起來麼!因而jvm採用了兩種方案來解決:
1)、 同步處理:JVM採用CAS(Compare and Swap)機制加上失敗重試的方式,保證更新操做的原子性。CAS機制是一種輕量級鎖機制,後續在聊多線程的時候再講!
2)、 本地線程分配緩衝區:把分配的內存按照不一樣的線程劃分在不一樣的空間進行,每一個線程在java堆區預先分配一小塊內存,稱爲本地線程分配緩衝區(Thread Local Allocation Buffer)。哪一個線程須要分配就從哪一個線程的TLAB上分配,只有在TLAB用完須要分配新的TLAB的時候才須要作同步處理(經過上一點中的CAS機制)。spa
內存分配完後,就須要初始化實例對象了,虛擬機須要將分配到的內存空間中的數據類型都初始化爲零值(不包括對象頭,若是是使用TLAB,初始化0值的操做提早至分配TLAB時)。接下來虛擬機要對對象進行必要的設置,例如這個對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息都存放在對象的對象頭中。作完以上之後,從虛擬機視角來看,一個新的對象已經產生了!線程
JVM完成對象內存的分配及對象初始化以後,會返回對象的地址,而且壓入操做數的棧頂,供後續操做!指針
dup命令沒猜錯的話是duplicate的簡寫。在討論dup命令前,咱們先看一個簡單的例子
public static void main (String[] args){
int a; int b = a = 88; }
咱們看看對應的字節碼
public static void main(java.lang.String[]);
Code:
0: bipush 88
2: dup 3: istore_1 4: istore_2 5: return
因爲88這個值在一條語句中須要重複賦給兩個變量,因此使用dup指令對棧頂的值進行了複製,且壓入棧頂。咱們在new對象的時候,new指令後面都會緊跟dup指令!而後是invokespecial和astore指令,相信聰明的你應該想到invokespecial和astore指令都會須要從棧頂彈出值來執行!在執行完dup指令後,操做數棧棧頂就有兩個指向該對象實例內存的reference數據,若是<init>方法有參數,還須要把參數加載到操做棧。
invokespecial指令調用對象實例方法<init>,經過符號引用#3定位到的是People對象的實例方法<init>。這時候操做數棧棧頂值(指向對象實例的內存reference)會被彈出(若是<init>方法有參數,參數也會出棧)。執行<init>方法會在java虛擬機棧中建立<init>方法的棧幀(相關棧和棧幀的介紹看上一篇文章),而且把出棧的數據放入棧幀的局部變量表中。變量表中指向對象實例的內存reference就是咱們常常用到的this,表示對該對象實例進行操做!執行完該指令後,一個完整的對象就建立完成啦!
astore依然須要彈出棧頂值,而後存儲到編號爲1的變量中供後續使用。至此一個完整的對象已經建立且返回對象內存引用給本地變量存儲了。
咱們上面已經把對象建立的問題解決了,同時咱們也都知道,引用類型的變量存儲的是**對象的引用**!那這個引用類型數據怎麼定位到堆中的對象呢?目前主流的對象訪問方式有兩種:
JVM在堆區劃分一塊內存做爲句柄池,引用類型變量中存儲就是對象的句柄地址。對象句柄包含兩個地址(以下圖):
一、在堆中分配的對象實例數據的地址。
二、這個對象類型數據地址。
引用類型變量中存儲就是在堆中分配的對象實例數據的地址。
句柄池的方式會在句柄池中存放類型對象的相關信息,而直接訪問的方式會把類型對象的信息放入實例對象的對象頭中(咱們知道對象頭包含「指向對象類型數據的指針」,其實這並非必須的,咱們經常使用的HotSpot虛擬機採用的是直接指針的方式,因此對象頭中會包含「指向對象類型數據的指針」,若是某類虛擬機採用的是句柄的方式訪問對象,那可能就不須要在頭部存儲這個指針了)。這兩種方式都互有優缺點: 1)、 句柄方式訪問對象時,多一次指針定位的時間開銷。可是對象移動時(垃圾回收時常見的動做),棧上的變量的引用不須要修改,只需改變句柄中實例數據指針。 2)、 直接指針對象相對句柄方式訪問節省了一次指針定位的時間開銷,性能更好。若是對象訪問很是頻繁,提高會更明顯!可是在對象移動時,棧上的變量的引用也須要變化。