一般粗略的可以把內存的分配分爲棧區和堆區,但是對於理解垃圾回收和分配還要分得細一點。如下圖:
分爲堆和方法區,虛擬機棧和本地方法棧,程序計數器。
前兩個是多線程共享的,後面三個是每個線程單獨的。
方法區
保存的是加載的類信息,常量(有常量池),靜態變量等與實例無關與類有關的信息。裏面還包括運行常量池,會在類加載時,把class文件常量池的字面量,符號引用等裝進運行常量池。使用String的intern()方法時也會把常量裝入運行常量池。
堆
就是存放對象實例的地方,也是垃圾回收器管理的主要區域。它的詳細劃分將在後面說。
java虛擬機棧
當進入一個方法的時候就會創建一個棧幀,裏面存放局部變量表,操作數棧,動態鏈接(不知道是什麼),方法出口信息。每個方法的調用就是棧幀在虛擬機棧中出棧和入棧的過程。
本地方法棧
和虛擬機棧類似,不同的是它存放的是調用native()方法的存儲的數據。
程序計數器
就是記錄代碼執行的地址的,代碼執行的行號指示器。
對象創建
當虛擬機遇到new指令時,檢查指令後面的參數能否定位到常量池中的一個類的符號引用,並檢查符號引用的類是否已經加載,解析,初始化過。
內存分配的方式
如果內存是連續的,則計算創建的對象的大小,並移動指針同樣大小的距離進行分配,這種方式叫“指針碰撞”。
如果內存不連續,用”空閒列表“記錄沒有使用的空間。
解決併發分配內存問題
一種是使用CSA和失敗重試的方式保證指針更新的原子性(不知道CSA是什麼只知道可以處理併發)
第二種是,每個線程劃分自己的TLAB(本地線程分配緩衝),每個線程的指針在自己的緩衝區上移動。
句柄和直接指針
句柄就是指向指針的指針,直接指針就直接指向的對象實例的堆地址
判斷對象的死亡
引用計數法
給對象添加一個引用計數器,當有一個地方引用它的時候,+1。引用失效時,-1。
問題,當兩個對象相互引用時,對象已經無法訪問了,但是因爲計數器不爲0,GC無法回收他們。
ObjA obja = new ObjA();
ObjB objb = new ObjB();
obja.instance = objb;
objb.instance = obja;
似乎表難理解什麼時候有引用,什麼時候失效.
可達性分析算法
以GC Roots對象爲起點,搜索它的引用鏈。如果存在引用該對象則可達。沒有則不可達。
可作爲GC Roots的對象
- 虛擬機棧中的局部變量表中的變量
- 本地方法棧中引用的變量
- 方法區常量引用的對象
- 方法區靜態變量引用的對象
垃圾收集算法
分代收集算法
只是定義了新生代和老年代使用不同的收集算法
新生代因爲對象生存週期短使用複製算法,老年代則使用標記-清除和標記整理算法
標記-清除
每個對象不可達後會被標記,並篩選。
篩選的標準是,如果對象沒有執行過finalize方法或覆蓋了finalize方法,則把它放到一個隊列,會有一個優先級比較低的線程執行對象的finalize方法。在執行finalize方法時,可以使對象重新被引用到。這樣就不會被回收。這是對象拯救自己的最後的機會。但是可能因爲執行finalize線程的優先級比較低,所以作者建議不要依賴這個方法。
如果標記後沒有被篩選到隊列,那麼真的就會被回收。另外finalise方法只能執行一次。
複製算法
把堆分爲eden space,form survival,to survival。對象分配在eden,每次gc後,將eden和survival存活的對象放到另一個survival。當存活的對象大於survival時,就需要依賴老年代(老年代使用其他的算法回收對象)。
標記-整理
讓存活的對象向一段移動,直接清除邊界外的內存。
內存分配
對象優先進入eden
大對象直接進入老年代
經歷過一定次數的對象會進入老生代
動態對象年齡判定
空間分配擔保
如果老年代的空間大於,新生代的總對象的大小,則MinorGC是安全的。否則根據HandlePromissionFailure設置是否冒險。冒險是指,根據上一次虛擬機會檢查歷次放入晉升到老年代的對象的平均大小,如果小於則執行MinorGC否則FullGC。