【朝花夕拾】Android性能篇之(二)Java內存分配

前言html

       轉載請聲明,轉自【http://www.javashuo.com/article/p-qfumhikl-gd.html】,謝謝!java

       在內存方面,相比於C/C++程序員,我們java系程序員算是比較幸運的,由於對於內存的分配和回收,都交給了JVM來處理了,而不須要手動在代碼中去完成。有了虛擬機內存管理機制,也就不那麼容易出現內存泄漏和內存溢出的問題了。不那麼容易出現,並不表明就不會出現。正是因爲程序員將內存的控制大權交了出去,那麼一旦出現了內存泄漏和內存溢出的問題,若是虛擬機如何分配內存的工做機制不瞭解,那這就成了一個難以處理的問題了。因此說,放權能夠,但不能徹底失去控制,不然,就有被架空的危險,出了問題,你只能幹捉急。程序員

       本文的主要內容以下:數組

      

 

1、內存的家庭住址數據結構

        咱們這麼關心的內存,究竟是何方神聖呢?看圖比看文字舒服,我們先上圖:多線程

   

        似曾相識吧!這個就是第一節中JVM執行java程序的流程。ClassLoader加載完畢.class文件後,交由執行引擎執行。整個程序執行過程當中,JVM會用一段空間來存儲執行期間須要用到的數據和相關信息,這段空間通常被稱做Runtime Data Area (運行時數據區),這就是我們常說的JVM內存,咱們常說到的內存管理就是針對這段空間進行管理。這樣,咱們就找到內存的家庭住址了。函數

 

2、內存你們庭中都有哪些成員呢?性能

         我們仍然先上圖:優化

        

        Java虛擬機在執行Java程序的過程當中會把它所管理的內存(運行時數據區)劃分爲若干個不一樣的數據區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,有些區域則依賴用戶線程的啓動和結束而創建和銷燬。根據《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存包含了上圖中的5個區域:程序計數器,虛擬機棧,本地方法棧,GC堆,方法區ui

 

3、內存的家庭成員分別都是幹嗎的呢?

        這一部分比較理論,文字描述比較多,可是若是有必定的基礎並且認真讀的話,其實很容易懂的,同時要想更好地理解內存這方面的知識,也須要耐着性子好好看。

  一、程序計數器

        程序計數器(Program Counter Register)是一塊較小的內存空間,也有的稱爲PC寄存器。

        學過彙編語言或者計算機機構與組成原理的童鞋,應該對着個概念不陌生,在彙編語言中,程序計數器是指CPU中的寄存器,它保存的是程序當前執行的指令的地址,當CPU須要執行指令的時候,就從中取出這條地址,並根據這條地址獲取到指令。獲取到指令後,程序計數器會自動+1或者根據轉移指針獲得下一條指令的地址,如此循環,直到執行完全部的指令。字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。

        雖然JVM中的程序計數器並不像彙編語言中的程序計數器同樣是物理概念上的CPU寄存器,可是JVM中的程序計數器的功能跟彙編語言中的程序計數器的功能在邏輯上是等同的,也就是說是用來指示執行哪條指令的。  

        因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存

        若是線程執行的是非native方法,則程序計數器中保存的是當前須要執行的指令的地址;若是線程執行的是native方法,則程序計數器中的值爲空(undefined)。這塊內存中存儲的數據所佔空間的大小不會隨程序的執行而發生改變,因此,此內存區域不會發生內存溢出(OutOfMemory)問題,該內存區域也是惟一一個在JVM規範中沒有規定任何OutOfMemoryError狀況的區域。

  二、Java虛擬機棧

        Java虛擬機棧(Java Vitual Machine Stack)簡稱爲Java棧,也就是咱們經常說的棧內存。它是Java方法執行的內存模型。

        

        如上圖所示,Java棧中存放的是一個個的棧幀,每一個棧幀對應的是一個被調用的方法。每個棧幀中包括了以下部分:局部變量表(Local Variables)、操做數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。所以能夠知道,線程當前執行的方法所對應的棧一定位於Java虛擬機棧的頂部。在Java虛擬機規範中,對Java棧區域規定了兩種異常情況:1)若是線程請求的棧深度大於虛擬機所容許的深度,將拋出棧內存溢出(StackOverflowError)異常;2) 若是虛擬機棧能夠動態擴展,並且擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。到這裏,咱們就容易理解,在使用遞歸方法的時候,若是這個方法的層次太深,就會致使Java棧中的棧幀過多,從而致使棧內存溢出。這部分空間的分配和釋放都是由系統自動實施的,而不須要程序員去管理了。

        常常有人把Java的內存區分爲堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,實際劃分遠比這複雜。之因此這種分法可以流行,說明大多數程序員最關注的、與對象內存分配關係最密切的內存區域主要是這兩塊。下面我們對棧幀再作細緻的描述。

    (1)局部變量表。顧名思義,它是一組變量值存儲空間,用於存放對應方法的形參和方法內部定義的非static局部變量。其中存放的數據的類型有以下幾種:a)基本數據類型。boolean,char,byte,short,int,long,float,double,java中定義的8種基本數據類型。b)對象引用(reference)。不是對象自己,而是指向對象實例的一個引用,這個就是Java中的指針,他的值爲一個地址,在堆中該實例的首地址。例如,Date date = new Date(...);new Date(...)表示在堆內存中開闢了一個空間來存儲該實例對象,而date就是對象的引用,局部變量表中存儲的就是指向堆中該對象的首地址。c) retunAddress類型。它指向了一條字節碼指令的地址。這一點沒有查得很明白,筆者估計應該是方法執行完畢後,返回給程序計數器的當前指令的地址吧。局部變量表所需的內存空間在編譯期間完成分配,即在Java程序被編譯成.class文件時,就肯定了所須要分配的最大局部變量表的容量。當進入一個方法時,這個方法須要在棧中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變其大小。

    (2)操做數棧。其又常被稱爲操做棧,它的最大深度也是在編譯的時候就肯定了。當一個方法開始執行時,它的操做棧是空的,在方法執行過程當中,會有各類字節碼指令(好比:加操做、賦值運算等)向操做棧中寫入內容,也就是入棧,計算完畢後提取內容,即出棧操做。學過數據結構的童鞋,必定對錶達式求職問題不會陌生,棧最典型的一個應用就是用來對錶達式求值。一個線程執行方法的過程當中,實際上就是不斷執行語句的過程,歸根到底就是進行計算的過程,能夠說,程序中的全部計算過程都是在藉助於操做數棧來完成的。Java虛擬機的解釋執行引擎也被稱爲「基於棧的執行引擎」,這裏「棧」就是操做數棧。所以咱們也稱Java虛擬機是基於棧的,這點不一樣於Android虛擬機,Android虛擬機是基於寄存器的。基於棧的指令集最主要的優勢是可移植性強,主要缺點是執行速度相對較慢;而因爲寄存器由硬件直接提供,因此基於寄存器指令集最主要的優勢是執行速度快,主要缺點是可移植性差。

    (3)指向當前方法所屬的類的運行時常量池的引用。不少地方也稱這個部分爲動態鏈接。每一個棧幀都包含一個指向運行時常量池(在方法區中詳細介紹)的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。Class文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就是以常量池中指向方法的符號引用爲參數。這些符號引用,一部分會在類加載階段或一第一次使用的時候轉化爲直接引用(如final,static域等),稱爲靜態解析,另外一部分將在每一次的運行期間轉化爲直接引用,這部分稱爲動態鏈接。簡單點說,就是由於在方法執行的過程當中有可能須要用到類中的常量,因此須要有一個引用指向運行時常量。

    (4)方法返回地址。當一個方法執行完畢以後,要返回以前調用它的地方,所以在棧幀中必須保存一個方法返回地址。方法被執行後,有兩種方式退出該方法:一種是執行引擎遇到任意一個方法返回的字節碼指令,也就是遇到了return,或者void函數執行完畢;另一種是遇到了異常,而且該異常沒有在方法體內獲得處理,即沒有用try-catch進行捕獲。不管是哪一種退出方式,在退出後都須要返回到方法被調用的位置,程序才能繼續執行。方法返回時可能須要在幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。通常來講,方法正常退出時,調用者的PC計數器的值就能夠做爲返回地址,棧幀中極可能保存了這個計數器值。而方法異常退出時,返回地址是要經過異常處理來肯定的,棧幀通常不會保存這部分信息。方法退出的過程實際上等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層局部變量表和操做數棧,若是有返回值,則把它壓入調用者棧幀的操做數棧中,調整程序計數器的值以指向方法方法調用指令後面的一條指令。

        每一個線程擁有本身的Java棧,調用本身的方法,互不干擾,屬於「私有內存」。

  三、本地方法棧(Native Method Stack)

        本地方法棧與Java虛擬機棧的做用和原理很是類似,區別在與前者爲執行Nativit方法服務的,然後者是爲執行Java方法服務的。在JVM規範中對本地方法棧中方法使用的語言,使用方式和數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。在HotSpot虛擬機中,直接把本地方法棧和Java棧合二而一了,而咱們平時Java開發中,最經常使用到的就是HotSpot虛擬機。與Java虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

  四、GC堆

        對於大多數應用來講,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配。堆是被全部線程共享的,在JVM中只有一個堆。這一點在Java虛擬機規範中的描述爲:全部的對象實例以及數組都要在對上分配,可是隨着JIT編譯器(即時編譯器:是一種提升程序運行效率的方法,一般由兩種運行方式,靜態編譯與動態編譯。靜態編譯是指執行前所有翻譯爲機器碼,動態編譯時指,一句一句地邊翻譯邊運行)的發展與逃逸技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在對上也漸漸變得不那麼絕對了。

       Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱做「GC堆」(Garbage Collected Heap)。若是還細分,有新生代和老年代等的劃分,此處不詳細展開,有興趣和須要深刻的能夠自行研究。根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。在實現時,既能夠實現成固定大小的,也能夠是可擴展的,不過當前主流仍是能夠擴展的(經過-Xmx和-Xms控制)。若是在堆中沒有內存完成實例分配,而且也沒法再擴展時,將會拋出OutOfMemoryError異常。

  五、方法區

        方法區(Method Area)在JVM中也是一個很是重要的區域,它與堆同樣,是被線程共享的區域,通常用來存儲不容易改變的數據,因此通常也被稱爲「永久代」。在方法區中,存儲了每一個類的信息(包括類名,方法信息,字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。在Class文件中除了類的字段、方法、接口等描述信息外,還有運行時常量池,用來儲存編譯期間生成的字面量和符號引用。在方法區中有一個很是重要的部分就是運行時常量池,它是每個類或者接口的常量池的運行時表示形式,在類和接口被加載到JVM後,對應的運行時常量池就被建立出來。固然並不是Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,好比String的intern方法(這一段是否是看得比較蒙?這裏先總體提一下,後面還會對該段內容作詳細整理,畢竟這一段全是知識點)。

        JVM垃圾收集器能夠像管理堆區同樣管理這部分區域,從而不須要專門爲這部分設計垃圾回收機制。不過,從JDK7以後,HotSpot虛擬機便將運行時常量池從永久代中移除了

        Java虛擬機規範把方法區描述爲Java堆的一個邏輯部分,並且它和Java Heap同樣不須要連續的內存,能夠選擇固定大小或可擴展,能夠容許該區域選擇不實現垃圾回收。相對而言,垃圾收集行爲在這個區域出現比較少,該區域的內存回收目標主要是針對廢棄常量和無用類的回收。爲了區別於Java-Heap,方法區也被稱爲Non-Heap區。根據規範,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。

 

4、方法區到底存儲了哪些信息?

        上一節中的第5點中,歸納性地講到了方法區存儲的信息,說得比較籠統,那樣是遠遠不夠的,筆者仍然須要再更細緻地探究一下,才能更深刻地理解。

       

  一、類信息

    (1)類型的全限定名:即類的完整有效名。在Java源代碼中,完整有效名由類的所屬包名稱加一個".",再加上類名組成。如,Object類所屬的包爲java.lang,那它的完整名稱爲java.lang.Object,可是在類的文件裏,全部的「.」都被斜槓 「/」 代替,就成爲java/lang/Object。完整有效名在方法區中的表示根據不一樣的實現而不一樣。

    (2)超類的全限定名:即直接父類的完整有效名。

    (3)直接超接口的全限定名:即實現的接口的完整有效名。

    (4)類型標誌:即該類是普通類類型仍是接口類型。

    (5)類的訪問描述符:如publlic,private,default,protected,abstract,final,static等

  二、類的常量池

        JVM爲每一個已加載的類型都維護一個常量池,是這個類用到的常量的一個有序集合,包括實際的常量(String,Integer,Floating Point常量)和對類、域(屬性)和方法的符號引用(符號引用在後面會講到)。池中的數據項像數組項同樣,是經過索引訪問的。由於常量池存儲了一個類型所使用到的全部類、域和方法的符號引用,因此它在Java程序的動態連接中起了核心的做用。(這一部分下一節會和運行時常量一塊兒詳細講到)

  三、字段信息(該類聲明的全部字段,也稱Field,屬性,域)

  (1)字段修飾符:如public、protected,private,default

  (2)字段的類型:好比int,float等8種基本類型和引用類型

  (3)字段的名稱:這個好理解

  四、方法信息(方法信息中包含類中的因此方法,每一個方法又包含了以下信息)

  (1)方法修飾符:public、protected,private,default,static,final,synchronized,native,abstract等

  (2)方法返回類型:好比public String getName(String id)中的String即爲返回類型,包括void

  (3)方法名:如上述中的getName

  (4)方法參數個數、類型、順序等

  (5)方法字節碼

  (6)操做數棧和方法棧幀的局部變量區的大小

五、類變量

       即靜態成員變量,被static修飾的變量,爲該類全部對象共享的變量,即便沒有任何實例對象時,也能夠訪問的類變量,它們與類進行綁定,成爲類數據在邏輯上的一部分。這個和第3)點區分開來,第3)點爲實例變量。在JVM使用一個類以前,它必須在方法區中爲每個non-final的類變量提早分配空間。對於被final修飾的類變量(常量),會在常量池中有一個拷貝,而non-final 類變量則被存儲在聲明它的類信息中,這裏要注意final和no-final修飾的區別。

       注意:Java類中的成員變量有靜態和非靜態之分。靜態成員變量在方法區,爲共享數據;非靜態成員變量,在new 一個對象的時候被分配在堆內存中。局部變量則是方法內定義的變量,前面已經講過,它會被分配在Java虛擬機棧內存中。虛擬機棧內存中會爲當前方法非配一個棧幀,棧幀中有一個局部變量表,該表存儲了該變量的值(基礎類型)或對象在堆中的地址(引用類型)。

       舉個栗子:

       

 

  • int i; 在類中定義(不是在方法中定義),爲第3)點中講到的,爲實例變量,須要類的實例才能調用,保存在堆中對應的對象實例中。
  • static int i ;non-final修飾的類變量,保存方法區中的類信息中。
  • final static int I=0; final修飾的類變量,此時I就成爲了一個常量了,必須賦值,不然報錯。它會在常量池中有一個拷貝。

  六、指向類加載器的引用

       每個被JVM加載的類,都保存這個類加載器的引用,類加載器動態連接時會用到。當解析一個類到另外一個類的引用時,JVM須要保證這兩個類的加載器是相同的,這對JVM區分名字空間的方式是相當重要的。

  七、指向Class實例的引用

       類加載的過程當中,虛擬機會爲每一個加載的類(包括類和接口)都建立一個java.lang.Class的實例,JVM必須以某種方式把這個Class實例和存儲在方法區中的類數據聯繫起來。在Class類中有個靜態方法能夠得帶這個實例的引用,public static Class forName(String className),經過Class.forName(String className)(反射)來查找得到該實例的引用,而後建立該類的對象(這裏和直接new一個對象區分開來)。例如,經過調用 Class.forName(「java.lang.Object」),能夠獲得與java.lang.Object對應的類對象(這裏用到了工廠模式),甚至能夠經過這個函數獲得任何包中任何已經加載的類引用,只要這個類可以被加載到當前的名字空間。若是不能把類加載到當前名字空間,forName就會拋出ClassNotFoundException。

       Class類還提供了以下方法,獲取到類的對象後,能夠用這些方法獲得對應的類存儲在方法區中的類信息:

  • public String getName(); //獲取類名
  • public Class getSuperClass(); //獲取父類對象
  • public boolean isInterface(); // 判斷是否爲接口
  • public Class[] getInterfaces(); //返回一組接口對象,對應該類實現的接口對象。
  • public ClassLoader getClassLoader(); //返回類加載器的引用。   

  八、方法表

       爲了提升訪問效率,JVM可能會對每一個裝載的非抽象類和非接口,都建立一個數組,數組的每一個元素都是實例可能調用的方法的直接引用(注意,這裏說的是引用,不是方法自己,方法自己是在Java虛擬機棧的棧幀中),包括父類中繼承過來的方法。JVM能夠經過方法錶快速激活實例方法。

  九、運行時常量

       JDK7後已經移除了方法區。結合第2點類的常量池,後面會有個小節再繼續擴展分析。

  十、即時編譯(JIT)後的代碼

       Java的字節碼文件.class文件,被JVM加載後,會一句一句翻譯程機器碼執行。這個區域就存儲了這些機器碼。(這個是筆者本身的理解,沒有查到權威的結論)

 

5、常量池

       在上一節中,咱們提到了「類的常量池」和「運行時常量池」,這裏咱們接着來說。

       常量池分爲靜態常量池和運行時常量池,它們的區別在於動態性。

  一、靜態常量池

       靜態常量就是我上面提到的「類常量池」,即*.class文件中的常量池。當java文件被編譯爲.class文件的時候,會專門有一部分區域用於保存類中的常量,這個區域就是類常量池。.class文件中的常量池不只僅包含字符串(數字)字面量,還包含類、方法的信息,他們佔據了class文件的絕大部分空間。整體來講,它主要存儲了兩大類常量:字面量和符號引用。在這裏,咱們解釋幾個名詞:

  • 常量:有兩種狀況,第一種是一個值,如1024(整型常量)、‘a’'b''c'(字符常量)、「abc」(字符串常量)、true/false(boolean型常量)等。第二種就是被final修飾的變量,由於它的值不能再改變,也被稱做常量,好比final int I = 0; 這裏,I 就成爲了一個常量。
  • 字面量:至關於Java語言層面常量的概念。好比 String s = 「abc」,這裏"abc"就是一個字面量。
  • 符號引用:屬於編譯原理方面的概念,包含了以下三種類型的的常量:

         I)類和接口的全限定名:即前面第4節第1)點類信息中提到過的,好比Object類的全限定名就是java.lang.Object

         II)字段名稱和描述符:即前面第4節第3)點中對應的名稱和修飾符。

         III)方法名稱和描述符:即前面第4節第4)點中對應的名稱和修飾符。

          

  二、運行時常量

        上述中的靜態常量池(類常量池),是在編譯的時候,存在於.class文件中的,而JVM在完成.class文件的裝載後,靜態常量池就被載入到內存中用於程序的運行,此時,靜態常量池搖身一變,成爲了運行時常量池。JDK7以前的版本中,運行時常量是方法區中的一部分,可能因爲方法區的空間有限,JDK7及之後的版本就把它移除了方法區,這一點在前面也屢次提到過。有些資料說是移到了Java堆中,沒有看到權威的資料,筆者也不敢去肯定。

        一點疑惑:從上面的描述來看,類/接口、方法、字段的相關信息,在上訴第4節中方法區中的類信息、字段信息、方法信息存儲了一份,在類常量池中又存儲一次,這樣是否是冗餘了?方法區是內存中的一部分,在運行期出現,而類常量池是.class文件中的一部分,在運行前就出現了,爲何方法區中會存在類變量? 是筆者參考的資料中描述有誤?仍是筆者理解有誤?這裏若是有幸被讀者讀到,能夠本身研究一下,順便告知於我,3Q!

  三、常量池的好處

       常量池是爲了不頻繁地建立和銷燬對象而影響到系統性能,而實現的對對象的共享。例如,字符串常量池,在編譯階段就把全部的字符串文字放到一個常量池中,這樣作有兩個好處:I)節省內存空間:常量池中全部相同的字符串常量合併,只佔用一個空間。II)節省運行時間:比較字符時,==比equals()快。對於兩個引用變量,只用==判斷引用是否相等,也就能夠判斷實際值是否相等。

       

6、總結 

        本章中理論性的東西太多了,下圖對這一章節的內容作個簡單的梳理和概括。

             

 

推薦閱讀及參考資料

        《深刻理解Java虛擬機——JVM高級特性與最佳實踐》

          http://www.javashuo.com/article/p-nqxnsmdx-et.html

          http://www.javashuo.com/article/p-kwefokhs-em.html

          https://blog.csdn.net/huangfan322/article/details/53220169        

          https://blog.csdn.net/zzhangxiaoyun/article/details/7518917

          https://blog.csdn.net/gcw1024/article/details/51026840

          https://blog.csdn.net/wangtaomtk/article/details/52267548

          http://www.javashuo.com/article/p-hsndzvis-hk.html

相關文章
相關標籤/搜索