天天看點

Java JVM運作時資料區,記憶體管理和GC垃圾回收

Java JVM運作時資料區,記憶體管理和GC垃圾回收

一 . 運作時資料區

         程式計數器是線程私有的,是一塊很小的記憶體空間,是目前線程執行到位元組碼行号的計數訓示器。每個CPU處理器核心 在任何一個時刻,都隻可能運作着唯一的一個線程,執行着一條指令。是以在多線程的應用中,線程不斷切換和配置設定時間片。線上程切換來切換去的過程中,就是靠程式計數器來了解,該如果繼續恢複運作該線程。

    虛拟機棧和Native方法棧也是線程私有的。在Sun HotSpot虛拟機中虛拟機棧和Native方法棧是被合二為一的。虛拟機棧描述的是Java方法執行的記憶體模型,棧幀用于存儲局部變量表,方法出口等資訊。每一個方法調用到執行完成都對應着一個棧幀在虛拟機棧中的入棧和出棧的過程。局部變量表中存放了八種基本資料類型和對象引用,還有returnAddress類型。前兩種不難了解,returnAddress實際上是指向一條位元組碼指令的位址,局部變量表所需的記憶體空間在編譯期間完成配置設定,當進入某個方法時,需要在棧幀中配置設定多大的局部空間是完全确定的,方法運作期間是不會改變局部變量表的大小。本地方法棧和虛拟機棧作用相似,虛拟機棧執行我們的java方法,本地方法棧就是執行Native方法,是以就是Native方法棧。簡單地講,一個Native Method就是一個java調用非java代碼的接口。

        Java堆是所有線程共享的區域,虛拟機管理的記憶體中最大的一塊。基礎所有對象的執行個體都是在這裡配置設定記憶體。Java堆是GC的主要回收對象,虛拟機棧上局部變狼表所存的對象引用,就是指向堆記憶體中的一塊位址。詳細内容後面垃圾回收會提到更多。

    方法區也是一塊所有線程共享的區域,這裡存儲虛拟機加載的類資訊,常量,靜态變量,JIT編譯後的代碼等資料。運作時常量池是方法區的一部分。

接下來要說到JVM GC機制,在垃圾回收中堆記憶體上移動對象位置是很常見的, 是以對象的通路方式也要順便提一下,也可以展示下方法區的類型是如何被引用的。我們都知道棧上的局部變量表中,有存儲reference資料的引用,它可能是對象的直接位址,也可能是句柄位址。

Java JVM運作時資料區,記憶體管理和GC垃圾回收
Java JVM運作時資料區,記憶體管理和GC垃圾回收

由于對象通路極其頻繁,是以Hot Spot也使用第二種方式,直接存執行個體引用是效率比較高的。但是第一種句柄的方式,好處在于,垃圾回收中,不需要更改棧上所存儲的位址,棧上的存儲穩定,隻需要修改句柄池。

二.垃圾回收GC

            說到垃圾回收 ,就不得不說垃圾回收算法的思想, 說到回收算法, 又不得不想到,什麼樣的對象需要被回收?什麼是存活對象,什麼對象已死亡。判斷對象的存活與死亡,通常有兩種方法,引用計數法和可達性分析算法。引用計數算法的問題在于,無法解決兩個對象間互相引用的問題,導緻不得達的對象依然無法回收。下面是執行個體: 

A.b=B ;    //這裡 B 引用計數+1   A對象在堆上   其屬性b存着B的引用
B.a=A  ; // 與上方同理
A=null;    
B=null;  //A B兩對象棧上引用  給null  AB堆上執行個體做到不可達
//此時導緻  AB對象執行個體記憶體不會被回收      

是以為了解決這個問題,Java虛拟機中 使用可達性分析算法。其思想是從可作為GC Root根結點的對象,向下搜尋,搜尋過的路成為引用鍊,凡事不能達到的對象,認為是可回收對象。有幾種對象可以作為GC Root對象。

1. 虛拟機棧上(棧幀的本地變量表)引用的對象。   2. 方法區中類靜态屬性引用的對象。    3. 方法區中常量引用的對象      4.  本地方法棧上Native方法引用的對象。

試想一下,為什麼這些對象可以作為GC Root,為什麼其他對象無法作為GC Root。  比如你的一個接口,處理請求的時候,有很多線程本地變量 , 但是 處理完請求後,這些對象就都沒用了,是以他們一般不能作為GC Root。什麼時候本地變量會成為GC Root呢?  個人的猜測應該是在GC大面積回收,程式暫停的時候, 這個時候,不能把目前正在運作的資料和變量清理掉呀。這時候提升為GC Root是很恰當的我覺得。至于靜态屬性,常量什麼的作為GC Root毋庸置疑了,他們本來生命周期就是整個應用程式生命周期。

    如何判斷對象存活與死亡說過了,還需要了解的就是垃圾回收算法 。有四種思想需要提到, 其中有一種最基本的叫做 标記-清除算法。另外很重要的複制算法,是年輕代最合适的。還有一種标記整理算法,是老年代最合适的。最後一種就是分代這個思想。

    标記清除就是首先對所有需要回收的對象進行标記,最後統一進行清除。這樣的做法效率不高,清理後記憶體碎片很多,可能導緻後期大對象無法配置設定記憶體,會經常觸發另一次垃圾回收。

    複制算法的思想是,将記憶體分為兩塊,其中一塊保持為空,回收的時候,将存活的對象複制到空的一塊,複制完成後,對原來那半記憶體全部清理。但是java虛拟機并不能留出一半的記憶體,這樣太浪費資源,并且在Java堆中,年輕代裡絕大部分記憶體都是要被回收的。是以Hot Spot中,将年輕代分為8:1:1的Eden,Survivor  * 2, 就是一塊Eden , 兩塊Survivor。  是以年輕代的政策,優先使用大的Eden,必須保持一塊Survivor為空。回收的時候,将存活的對象,複制到空的Survivor記憶體區域,清理Eden和剛才使用的Survivor。

            如果Survivor放不下存活的對象了,和時候 就該有對象進入老年代了~     老年代使用标記—整理算法。标記整理思路就是,先标記,然後在回收的時候,将存活的對象移動到一端,将被清理的移動到另一端。然後清理端邊界意外的記憶體。