物件在記憶體中的佈局——物件的建立
我們在任何一個專案中,無時無刻不關注物件的建立,時時刻刻都在建立物件,都在使用物件,那麼,我們就從虛擬機器的角度來看物件的建立。
首先,我們知道,建立物件有多種方式,最直觀的一種方式就是通過new關鍵字來建立物件,而且我們之前也提到過了,我們知道,通過new建立一個物件,那麼,那個物件就會儲存到堆記憶體中,那麼,下面我們就來具體的看一下物件的建立過程
這個圖表示的就是一個物件的建立過程,我們知道,在我們的Java程式碼中,通過new,後面跟一個類的名字,那麼,就可以例項化一個物件,第一步就是我們在執行過程中,首先根據new的引數在常量池中定位一個類的符號引用
這裡做到的,最後就是呼叫物件的<init>方法,這個所謂的<init>方法,其實就是我們的程式碼塊,包括我們的構造方法。
這個是整個物件的建立過程,其實,我們所能夠看到的只有new 類名、呼叫物件的<init>方法
建立物件的初始化方法的時候,我們是有感知的,我們可以通過斷點除錯等一些手段,能夠看到它確實是在建立物件的過程中觸發了這麼一個事件。
那麼關於這四個位置
其實是虛擬機器內部所執行的,對於開發者來講是遮蔽的,但是,它遮蔽了我們,我們為什麼還要來學習它呢?就是因為,我們要知道它的原理。
前兩個我們後面會講,我們從虛擬機器為物件分配記憶體開始講,那麼,虛擬機器是如何為物件分配記憶體的呢?如何往堆中去進行分配的呢?那麼,在分配的過程中會出現一些問題,比如說,執行緒安全性問題,它是如何解決的呢?
下面說一下關於給物件分配記憶體的一些策略。
我們知道,堆是一塊很不連續的儲存空間,我們在這裡假設堆記憶體是規整的,那麼,用過的記憶體放到一邊,空閒的記憶體放到另一邊,中間放一個指標作為分界點指示器,那麼,分配記憶體其實就是指標移動的過程,我們可以畫個圖表示一下,比如說,這是我們的一塊記憶體
然後我們把這個記憶體分成兩塊區域
那麼,假設右邊是我們使用的記憶體,左邊是空閒的記憶體
那麼,我們在進行物件建立的過程中,要給這個物件分配堆記憶體,那麼,分配多少呢?我們後面會說,其實在物件的建立過程中,在堆中所建立的記憶體區域也是已經確定了的,但是,如何計算,我們後面再說,我們只要知道,我們建立了一個物件扔到堆記憶體中,那麼,它肯定會佔用儲存空間,那麼,也就是說,已經使用的空間肯定要增加,那麼,剩餘的空間肯定要減小,那麼,也就是說,這個要往左邊移動
從剛才的位置移動到了這裡,就說明,我們剛才所建立的那個物件就佔用這麼多的儲存空間
這種分配方式稱之為指標碰撞,這是第一種給物件分配記憶體的方式,當然了,我們說Java堆記憶體並不是規整的,那麼,已使用的記憶體和空閒的記憶體,它並不是這麼有規矩的,而是,可能使用的和未使用的進行相互交錯的,那麼,就沒辦法使用我們這種所謂的指標碰撞了,那,這個時候該怎麼辦呢?虛擬機器就必須維護一個列表,記錄哪些記憶體塊是可用的,那麼,在分配的時候就可以從這張列表中去找出來一塊區域給這個物件的例項,並更新在這麼一個表中。
這一塊記錄的就是哪些記憶體可以使用,哪些記憶體已經使用,比如說,我們就僅僅記錄哪些記憶體沒有使用
假設我們記錄了第零塊記憶體沒有使用,記憶體都有編號,那麼,給它分配了之後,我們把這一塊記憶體再從這張表裡面給刪除掉
這麼一個方式,就是說,使用一張表來進行記錄,那麼,這種分配的方式叫做空閒列表。那麼,到底該選擇哪一種記憶體的分配方式呢?其實,記憶體的分配方式是由Java的堆是否規整來進行決定的,而,Java堆是否規整是由我們的垃圾回收策略決定的,垃圾回收器,如果說,它帶有壓縮整理的功能,就是說,在進行垃圾回收的過程中,它會自動的進行壓縮整理,那麼,把這個記憶體區域化分成非常有規則的,像已經使用的和未使用的空間的話,那麼,我們就可以使用指標碰撞,如果垃圾回收器,它沒有這個功能的話,那麼,我們就不能使用指標碰撞,就必須使用空閒列表。關於垃圾回收器,我們到後面會詳細的講。垃圾回收器也有非常的多,也有不同的實現,我們這裡就不在去介紹垃圾回收器了。
關於給物件分配記憶體,我們就說這兩點,一個是指標碰撞,一個是空閒列表。
接下來我們說執行緒安全性問題。
物件的建立怎麼還會涉及到執行緒安全性問題呢?其實這個非常好理解,那麼,我們可以通過剛才的這個圖來看
比如說指標碰撞的方式,每一次建立物件,棧記憶體中的這個指標都要像左移動,如果在一個高併發的環境下,記憶體的建立可能,在同一時刻可能會有多個物件在進行建立,那麼,指標在進行移動的過程中,有可能會出現執行緒的安全性問題,那麼,關於執行緒安全性問題該如何解決呢?當然了,如果是空閒列表的方式,比如說第一個執行緒過來了,從列表中讀了一塊,發現這塊區域沒人用,那麼給這塊記憶體分配了,那麼,還沒來得及更新這個列表,第二個執行緒有過來了,認為這塊依然是空閒空間,那麼,就把原來的那一塊分配的物件給佔用了,
所以,執行緒安全性問題還是有的。
那麼,我們出現了執行緒安全性問題該如何解決呢?這個型別於我們學習多執行緒的過程中解決執行緒安全性問題的方案是一樣的。
最簡單的方案就是實現執行緒的同步,加鎖,當有一個執行緒過來了,加鎖,不讓其他執行緒進來,當這個執行緒執行完畢之後,下一個執行緒才能進來,那麼,這種方式雖然安全,但是有一個致命的問題,就是執行效率太低,但是這也不失為一種解決方案,那麼,如何來提高執行效率呢?我們可以這樣,針對每一個執行緒,在堆記憶體中給它單獨的分配一塊區域,我們之前再講記憶體劃分的時候也提到過這麼一回事,比如說這是我們的堆記憶體
這一塊區域很大,我們會每一個執行緒都來分配一塊自己的區域,這塊區域並不是特別大,當然這塊區域的容量我們可以通過虛擬機器引數來指定,有幾個執行緒我們就給它分配幾個區域
我們稱之為本地執行緒分配緩衝,簡稱就是TLAB,當這個執行緒來進行物件的分配的時候,我們就在這塊區域裡面進行分配
另外一個執行緒在這個裡面去進行分配
每一個執行緒操作不同的區域,那麼,就不會再導致執行緒安全性問題了
當然了,如果這塊記憶體佔滿了怎麼辦呢?
佔滿了之後,我們可以再給他分配一塊區域,
那麼再進行這塊區域分配的時候,我們要對它進行採用同步的策略。這個就是解決執行緒安全問題的第二個方案,就是採用本地執行緒緩衝,這種方案其實有效的提高了我們每次都加鎖這種效能的問題,提高了一些效能。關於執行緒安全性問題,我們就說到這裡。
接下來講初始化物件。這裡所謂的初始化物件,其實就是把將分配的記憶體初始化為零值,我們知道,任何一個物件,初始化完畢之後,其實都是有一個預設值的,那麼,這裡其實就是初始化預設值的問題,當然了,不僅僅這麼簡單,它還要具體的初始化說物件是哪個類的例項,如何才能找到類的源資料資訊,物件的雜湊碼等等,這些資訊都是包含在物件頭中,物件頭我們後面在說,其實在這一步之後
我們的物件其實在Java虛擬機器中已經產生了,因為,記憶體也都分配好了,也都初始化好了,但是,對於我們開發者來講,其實這個物件還沒有建立完成,也就差最後一步,呼叫初始化方法,我這裡說的是執行構造方法,其實不然啊,它就是呼叫物件的<init>方法,包括程式碼塊等等,
到這裡,物件的建立就完畢了,我們可以通過程式碼來簡單的看一下,其實也沒有什麼好看的,因為記憶體分配我們是看不到的,我們所能夠看到的其實就是一個初始化物件,以及執行構造方法這麼一個過程。
我們提供get/set方法,目的就是來獲取它的預設值。
然後,我們寫一個測試類
我們可以看到,抽象資料型別的預設初始化值就是空即null,基本資料型別int的預設值就是0,boolean型的初始化預設值是false。
初始化完畢的最後一步會執行構造方法
我們發現,這句話已經打印出來了,而且是在System.out.println(usr.toString())這句話之前執行的。