java內存分配和String類型的深度解析

摘要: 從總體上介紹java內存的概念、構成以及分配機制,在此基礎上深度解析java中的String類型,從內存分配狀況來解析String對象的特性。                                    java

1、引題 面試

    在java語言的全部數據類型中,String類型是比較特殊的一種類型,同時也是面試的時候常常被問到的一個知識點,本文結合java內存分配深度分析關於String的許多使人迷惑的問題。下面是本文將要涉及到的一些問題,若是讀者對這些問題都瞭如指掌,則可忽略此文。 算法

    一、java內存具體指哪塊內存?這塊內存區域爲何要進行劃分?是如何劃分的?劃分以後每塊區域的做用是什麼?如何設置各個區域的大小? shell

    二、String類型在執行鏈接操做時,效率爲何會比StringBuffer或者StringBuilder低?StringBuffer和StringBuilder有什麼聯繫和區別? 數組

    三、java中常量是指什麼?String s = "s" 和 String s = new String("s") 有什麼不同? 安全

2、java內存分配 多線程

    一、JVM簡介       架構

  Java虛擬機(Java Virtual Machine 簡稱JVM)是運行全部Java程序的抽象計算機,是Java語言的運行環境,它是Java 最具吸引力的特性之一。Java虛擬機有本身完善的硬體架構,如處理器堆棧寄存器等,還具備相應的指令系統。JVM屏蔽了與具體操做系統平臺相關的信息,使得Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就能夠在多種平臺上不加修改地運行。             app

  一個運行時的Java虛擬機實例的天職是:負責運行一個java程序。當啓動一個Java程序時,一個虛擬機實例也就誕生了。當該程序關閉退出,這個虛擬機實例也就隨之消亡。若是同一臺計算機上同時運行三個Java程序,將獲得三個Java虛擬機實例。每一個Java程序都運行於它本身的Java虛擬機實例中。         jvm

  以下圖所示,JVM的體系結構包含幾個主要的子系統和內存區:              

  垃圾回收器(Garbage Collection):負責回收堆內存(Heap)中沒有被使用的對象,即這些對象已經沒有被引用了。              

  類裝載子系統(Classloader Sub-System):除了要定位和導入二進制class文件外,還必須負責驗證被導入類的正確性,爲類變量分配並初始化內存,以及幫助解析符號引用。              

  執行引擎(Execution Engine):負責執行那些包含在被裝載類的方法中的指令。              

  運行時數據區(Java Memory Allocation Area):又叫虛擬機內存或者Java內存,虛擬機運行時須要從整個計算機內存劃分一塊內存區域存儲許多東西。例如:字節碼、從已裝載的class文件中獲得的其餘信息、程序建立的對象、傳遞給方法的參數,返回值、局部變量等等。

    二、java內存分區      

  從上節知道,運行時數據區便是java內存,並且數據區要存儲的東西比較多,若是不對這塊內存區域進行劃分管理,會顯得比較雜亂無章。 根據存儲數據的不一樣,java內存一般被劃分爲5個區域:程序計數器(Program Count Register)、本地方法棧(Native Stack)、方法區(Methon Area)、棧(Stack)、堆(Heap)。      

  程序計數器(Program Count Register):又叫程序寄存器。JVM支持多個線程同時運行,當每個新線程被建立時,它都將獲得它本身的PC寄存器(程序計數器)。若是線程正在執行的是一個Java方法(非native),那麼PC寄存器的值將老是指向下一條將被執行的指令,若是方法是 native的,程序計數器寄存器的值不會被定義。 JVM的程序計數器寄存器的寬度足夠保證能夠持有一個返回地址或者native的指針。            

  棧(Stack):又叫堆棧。JVM爲每一個新建立的線程都分配一個棧。也就是說,對於一個Java程序來講,它的運行就是經過對棧的操做來完成的。棧以幀爲單位保存線程的狀態。JVM對棧只進行兩種操做:以幀爲單位的壓棧和出棧操做。咱們知道,某個線程正在執行的方法稱爲此線程的當前方法。咱們可能不知道,當前方法使用的幀稱爲當前幀。當線程激活一個Java方法,JVM就會在線程的 Java堆棧裏新壓入一個幀,這個幀天然成爲了當前幀。在此方法執行期間,這個幀將用來保存參數、局部變量、中間計算過程和其餘數據。從Java的這種分配機制來看,堆棧又能夠這樣理解:棧(Stack)是操做系統在創建某個進程時或者線程(在支持多線程的操做系統中是線程)爲這個線程創建的存儲區域,該區域具備先進後出的特性。其相關設置參數:

  • -Xss --設置方法棧的最大值

          本地方法棧(Native Stack):存儲本地方方法的調用狀態。

          方法區(Method Area):當虛擬機裝載一個class文件時,它會從這個class文件包含的二進制數據中解析類型信息,而後把這些類型信息(包括類信息、常量、靜態變量等)放到方法區中,該內存區域被全部線程共享,以下圖所示。本地方法區存在一塊特殊的內存區域,叫常量池(Constant Pool),這塊內存將與String類型的分析密切相關。

          堆(Heap):Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域。在此區域的惟一目的就是存放對象實例,幾乎全部的對象實例都是在這裏分配內存,可是這個對象的引用倒是在棧(Stack)中分配。所以,執行String s = new String("s")時,須要從兩個地方分配內存:在堆中爲String對象分配內存,在棧中爲引用(這個堆對象的內存地址,即指針)分配內存,以下圖所示。

            JAVA虛擬機有一條在堆中分配新對象的指令,卻沒有釋放內存的指令,正如你沒法用Java代碼區明確釋放一個對象同樣。虛擬機本身負責決定如何以及什麼時候釋放再也不被運行的程序引用的對象所佔據的內存,一般,虛擬機把這個任務交給垃圾收集器(Garbage Collection)。其相關設置參數:

  • -Xms -- 設置堆內存初始大小

  • -Xmx -- 設置堆內存最大值

  • -XX:MaxTenuringThreshold -- 設置對象在新生代中存活的次數

  • -XX:PretenureSizeThreshold -- 設置超過指定大小的大對象直接分配在舊生代中

 

        Java堆是垃圾收集器管理的主要區域,所以又稱爲「GC 堆」(Garbage Collectioned Heap)。如今的垃圾收集器基本都是採用的分代收集算法,因此Java堆還能夠細分爲:新生代(Young Generation)和老年代(Old Generation),以下圖所示。分代收集算法的思想:第一種說法,用較高的頻率對年輕的對象(young generation)進行掃描和回收,這種叫作minor collection,而對老對象(old generation)的檢查回收頻率要低不少,稱爲major collection。這樣就不須要每次GC都將內存中全部對象都檢查一遍,以便讓出更多的系統資源供應用系統使用;另外一種說法,在分配對象遇到內存不足時,先對新生代進行GC(Young GC);當新生代GC以後仍沒法知足內存空間分配需求時, 纔會對整個堆空間以及方法區進行GC(Full GC)。

         

        在這裏可能會有讀者表示疑問:記得還有一個什麼永久代(Permanent Generation)的啊,難道它不屬於Java堆?親,你答對了!其實傳說中的永久代就是上面所說的方法區,存放的都是jvm初始化時加載器加載的一些類型信息(包括類信息、常量、靜態變量等),這些信息的生存週期比較長,GC不會在主程序運行期對PermGen Space進行清理,因此若是你的應用中有不少CLASS的話,就極可能出現PermGen Space錯誤。其相關設置參數:

  • -XX:PermSize --設置Perm區的初始大小

  • -XX:MaxPermSize --設置Perm區的最大值

         新生代(Young Generation又分爲:Eden區和Survivor區,Survivor區有分爲From Space和To Space。Eden區是對象最初分配到的地方;默認狀況下,From Space和To Space的區域大小相等。JVM進行Minor GC時,將Eden中還存活的對象拷貝到Survivor區中,還會將Survivor區中還存活的對象拷貝到Tenured區中。在這種GC模式下,JVM爲了提高GC效率, 將Survivor區分爲From Space和To Space,這樣就能夠將對象回收和對象晉升分離開來。新生代的大小設置有2個相關參數:

  • -Xmn -- 設置新生代內存大小。

  • -XX:SurvivorRatio -- 設置Eden與Survivor空間的大小比例

            老年代(Old Generation)OLD 區空間不夠時, JVM 會在 OLD 區進行 major collection 徹底垃圾收集後,若SurvivorOLD區仍然沒法存放從Eden複製過來的部分對象,致使JVM沒法在Eden區爲新對象建立內存區域,則出現"Out of memory錯誤"  。

 

3、String類型的深度解析

    讓咱們從Java數據類型開始提及吧!Java數據類型一般(分類方法多種多樣)從總體上能夠分爲兩大類:基礎類型和引用類型,基礎類型的變量持有原始值,引用類型的變量一般表示的是對實際對象的引用,其值一般爲對象的內存地址。對於基礎類型和引用類型的細分,直接上圖吧,你們看了一目瞭然。固然,下圖也僅僅只是其中的一種分類方式。  

    針對上面的圖,有3點須要說明:

  •     char類型能夠單獨出來造成一類,不少基本類型的分類爲:數值類型、字符型(char)和bool型。

  •     returnAddress類型是一個Java虛擬機在內部使用的類型,被用來實現Java程序中的finally語句。

  •     String類型在上圖的什麼位置?yes,屬於引用類型下面的類類型。下面開始對String類型的挖掘!

 

    1String的本質            

  打開String的源碼,類註釋中有這麼一段話「Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.」。

  這句話總結概括了String的一個最重要的特色:

  String是值不可變(immutable)的常量,是線程安全的(can be shared)     

   接下來,String類使用了final修飾符,代表了String類的第二個特色:String類是不可繼承的。        

 

  下面是String類的成員變量定義,從類的實現上闡明瞭String值是不可變的(immutable)           

   private final char value[];             

     private final int count;         

 

所以,咱們看String類的concat方法。實現該方法第一步要作的確定是擴大成員變量value的容量,擴容的方法從新定義一個大容量的字符數組buf。第二步就是把原來value中的字符copybuf中來,再把須要concat的字符串值也copybuf中來,這樣子,buf中就包含了concat以後的字符串值。下面就是問題的關鍵了,若是value不是final的,直接讓value指向buf,而後返回this,則大功告成,沒有必要返回一個新的String對象。可是。。。惋惜。。。因爲valuefinal型的,因此沒法指向新定義的大容量數組buf,那怎麼辦呢?「return new String(0, count + otherLen, buf);」,這是Stringconcat實現方法的最後一條語句,從新new一個String對象返回。這下真相大白了吧!

       總結:String實質是字符數組,兩個特色:1、該類不可被繼承;2、不可變性(immutable)

 

    2String的定義方法        在討論String的定義方法以前,先了解一下常量池的概念,前面在介紹方法區的時候已經提到過了。下面稍微正式的給一個定義吧。        常量池(constant pool)指的是在編譯期被肯定,並被保存在已編譯的.class文件中的一些數據。它包括了關於類、方法、接口等中的常量,也包括字符串常量。常量池還具有動態性,運行期間能夠將新的常量放入池中,String類的intern()方法是這一特性的典型應用。不懂嗎?後面會介紹intern方法的。虛擬機爲每一個被裝載的類型維護一個常量池,池中爲該類型所用常量的一個有序集合,包括直接常量(string、integer和float常量)和對其餘類型、字段和方法的符號引用(與對象引用的區別?讀者能夠本身去了解)。

       String的定義方法概括起來總共爲三種方式:

  •        使用關鍵字new,如:String s1 = new String("myString");

  •        直接定義,如:String s1 = "myString";

  •        串聯生成,如:String s1 = "my" + "String";這種方式比較複雜,這裏就不贅述了,請參見java--String常量池問題的幾個例子。 

       第一種方式經過關鍵字new定義過程:在程序編譯期,編譯程序先去字符串常量池檢查,是否存在「myString,若是不存在,則在常量池中開闢一個內存空間存放「myString」;若是存在的話,則不用從新開闢空間,保證常量池中只有一個「myString」常量,節省內存空間。而後在內存堆中開闢一塊空間存放new出來的String實例,在棧中開闢一塊空間,命名爲「s1」,存放的值爲堆中String實例的內存地址,這個過程就是將引用s1指向new出來的String實例。各位,最模糊的地方到了!堆中new出來的實例和常量池中的「myString」是什麼關係呢?等咱們分析完了第二種定義方式以後再回頭分析這個問題。

       第二種方式直接定義過程:在程序編譯期,編譯程序先去字符串常量池檢查,是否存在「myString」,若是不存在,則在常量池中開闢一個內存空間存放「myString」;若是存在的話,則不用從新開闢空間。而後在棧中開闢一塊空間,命名爲「s1」,存放的值爲常量池中「myString」的內存地址。常量池中的字符串常量與堆中的String對象有什麼區別呢?爲何直接定義的字符串一樣能夠調用String對象的各類方法呢?

       帶着諸多疑問,我和你們一塊兒探討一下堆中String對象和常量池中String常量的關係,請你們記住,僅僅是探討,由於本人對這塊也比較模糊。        第一種猜測:由於直接定義的字符串也能夠調用String對象的各類方法,那麼能夠認爲其實在常量池中建立的也是一個String實例(對象)String s1 = new String("myString");先在編譯期的時候在常量池建立了一個String實例,而後clone了一個String實例存儲在堆中,引用s1指向堆中的這個實例。此時,池中的實例沒有被引用。當接着執行String s1 = "myString";時,由於池中已經存在「myString」的實例對象,則s1直接指向池中的實例對象;不然,在池中先建立一個實例對象,s1再指向它。以下圖所示:        這種猜測認爲:常量池中的字符串常量實質上是一個String實例,與堆中的String實例是克隆關係。

       第二種猜測也是目前網上闡述的最多的,可是思路都不清晰,有些問題解釋不通。下面引用《JAVA String對象和字符串常量的關係解析》一段內容。        在解析階段,虛擬機發現字符串常量"myString",它會在一個內部字符串常量列表中查找,若是沒有找到,那麼會在堆裏面建立一個包含字符序列[myString]的String對象s1,而後把這個字符序列和對應的String對象做爲名值對( [myString], s1 )保存到內部字符串常量列表中。以下圖所示:

            若是虛擬機後面又發現了一個相同的字符串常量myString,它會在這個內部字符串常量列表內找到相同的字符序列,而後返回對應的String對象的引用。維護這個內部列表的關鍵是任何特定的字符序列在這個列表上只出現一次。            例如,String s2 = "myString",運行時s2會從內部字符串常量列表內獲得s1的返回值,因此s2和s1都指向同一個String對象。            這個猜測有一個比較明顯的問題,紅色字體標示的地方就是問題的所在。證實方式很簡單,下面這段代碼的執行結果,javaer都應該知道。                String s1 = new String("myString");                String s2 = "myString";                System.out.println(s1 == s2);  //按照上面的推測邏輯,那麼打印的結果爲true;而實際上真實的結果是false,由於s1指向的是堆中String對象,而s2指向的是常量池中的String常量。

           雖然這段內容不那麼有說服力,可是文章提到了一個東西——字符串常量列表,它多是解釋這個問題的關鍵。

           文中提到的三個問題,本文僅僅給出了猜測,請知道真正內幕的高手幫忙分析分析,謝謝!

  •            堆中new出來的實例和常量池中的「myString」是什麼關係呢?

  •            常量池中的字符串常量與堆中的String對象有什麼區別呢?

  •            爲何直接定義的字符串一樣能夠調用String對象的各類方法呢?      

    3StringStringBufferStringBuilder的聯繫與區別          上面已經分析了String的本質了,下面簡單說說StringBuffer和StringBuilder。

     StringBuffer和StringBuilder都繼承了抽象類AbstractStringBuilder,這個抽象類和String同樣也定義了char[] value和int count,可是與String類不一樣的是,它們沒有final修飾符。所以得出結論:String、StringBuffer和StringBuilder在本質上都是字符數組,不一樣的是,在進行鏈接操做時,String每次返回一個新的String實例,而StringBuffer和StringBuilder的append方法直接返回this,因此這就是爲何在進行大量字符串鏈接運算時,不推薦使用String,而推薦StringBuffer和StringBuilder。那麼,哪一種狀況使用StringBuffe?哪一種狀況使用StringBuilder呢?         

     關於StringBuffer和StringBuilder的區別,翻開它們的源碼,下面貼出append()方法的實現。                

        上面第一張圖是StringBuffer中append()方法的實現,第二張圖爲StringBuilder對append()的實現。區別應該一目瞭然,StringBuffer在方法前加了一個synchronized修飾,起到同步的做用,能夠在多線程環境使用。爲此付出的代價就是下降了執行效率。所以,若是在多線程環境可使用StringBuffer進行字符串鏈接操做,單線程環境使用StringBuilder,它的效率更高。

相關文章
相關標籤/搜索