多是把Java內存區域講的最清楚的一篇文章

寫在前面(常見面試題)

基本問題:

  • 介紹下 Java 內存區域(運行時數據區)
  • Java 對象的建立過程(五步,建議能默寫出來而且要知道每一步虛擬機作了什麼)
  • 對象的訪問定位的兩種方式(句柄和直接指針兩種方式)

拓展問題:

  • String類和常量池
  • 8種基本類型的包裝類和常量池

Java程序員必看文檔

哈哈 皮一下!我本身開源的一個Java學習指南文檔。一份涵蓋大部分Java程序員所須要掌握的核心知識,正在一步一步慢慢完善,期待您的參與。Github地址:https://github.com/Snailclimb/Java-Guide 。看一眼,我以爲你必定不會後悔,若是能夠的話,能夠給個Star鼓勵一下!html

1 概述

對於 Java 程序員來講,在虛擬機自動內存管理機制下,再也不須要像C/C++程序開發程序員這樣爲內一個 new 操做去寫對應的 delete/free 操做,不容易出現內存泄漏和內存溢出問題。正是由於 Java 程序員把內存控制權利交給 Java 虛擬機,一旦出現內存泄漏和溢出方面的問題,若是不瞭解虛擬機是怎樣使用內存的,那麼排查錯誤將會是一個很是艱鉅的任務。java

2 運行時數據區域

Java 虛擬機在執行 Java 程序的過程當中會把它管理的內存劃分紅若干個不一樣的數據區域。
運行時數據區域
這些組成部分一些事線程私有的,其餘的則是線程共享的。git

線程私有的:程序員

  • 程序計數器
  • 虛擬機棧
  • 本地方法棧

線程共享的:github

  • 方法區
  • 直接內存

2.1 程序計數器

程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都須要依賴這個計數器來完。面試

另外,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。算法

2.2 Java 虛擬機棧

與程序計數器同樣,Java虛擬機棧也是線程私有的,它的生命週期和線程相同,描述的是 Java 方法執行的內存模型。數組

Java 內存能夠粗糙的區分爲堆內存(Heap)和棧內存(Stack),其中棧就是如今說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。緩存

局部變量表主要存放了編譯器可知的各類數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不一樣於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)。安全

2.3 本地方法棧

和虛擬機棧所發揮的做用很是類似,區別是: 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。

2.4 堆

Java 虛擬機所管理的內存中最大的一塊,Java 堆是全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例以及數組都在這裏分配內存。

Java 堆是垃圾收集器管理的主要區域,所以也被稱做GC堆(Garbage Collected Heap).從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此Java堆還能夠細分爲:新生代和老年代:在細緻一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。

2.5 方法區

方法區與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作 Non-Heap(非堆),目的應該是與 Java 堆區分開來。

HotSpot 虛擬機中方法區也常被稱爲 「永久代」,本質上二者並不等價。僅僅是由於 HotSpot 虛擬機設計團隊用永久代來實現方法區而已,這樣 HotSpot 虛擬機的垃圾收集器就能夠像管理 Java 堆同樣管理這部份內存了。可是這並非一個好主意,由於這樣更容易遇到內存溢出問題。

相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入方法區後就「永久存在」了。

2.6 運行時常量池

運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用於存放編譯期生成的各類字面量和符號引用)

既然運行時常量池時方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryError 異常。

JDK1.7及以後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。同時在 jdk 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域

推薦閱讀:《Java中幾種常量池的區分》: http://www.javashuo.com/article/p-rcrreqnh-nt.html

2.7 直接內存

直接內存並非虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,可是這部份內存也被頻繁地使用。並且也可能致使OutOfMemoryError異常出現。

JDK1.4中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel)緩存區(Buffer) 的 I/O 方式,它能夠直接使用Native函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣就能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆之間來回複製數據

本機直接內存的分配不會收到 Java 堆的限制,可是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

3 HotSpot 虛擬機對象探祕

經過上面的介紹咱們大概知道了虛擬機的內存狀況,下面咱們來詳細的瞭解一下 HotSpot 虛擬機在 Java 堆中對象分配、佈局和訪問的全過程。

3.1 對象的建立

下圖即是 Java 對象的建立過程,我建議最好是能默寫出來,而且要掌握每一步在作什麼。
Java對象的建立過程

①類加載檢查: 虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,而且檢查這個符號引用表明的類是否已被加載過、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。

②分配內存:類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後即可肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從 Java 堆中劃分出來。分配方式「指針碰撞」「空閒列表」 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定

內存分配的兩種方式:(補充內容,須要掌握)

選擇以上兩種方式中的哪種,取決於 Java 堆內存是否規整。而 Java 堆內存是否規整,取決於 GC 收集器的算法是"標記-清除",仍是"標記-整理"(也稱做"標記-壓縮"),值得注意的是,複製算法內存也是規整的

內存分配併發問題(補充內容,須要掌握)

在建立對象的時候有一個很重要的問題,就是線程安全,由於在實際開發過程當中,建立對象是很頻繁的事情,做爲虛擬機來講,必需要保證線程是安全的,一般來說,虛擬機採用兩種方式來保證線程安全:

  • CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操做的原子性。
  • TLAB: 爲每個線程預先在Eden區分配一起內存,JVM在給線程中的對象分配內存時,首先在TLAB分配,當對象大於TLAB中的剩餘內存或TLAB的內存已用盡時,再採用上述的CAS進行內存分配

③初始化零值:內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

④設置對象頭: 初始化零值完成以後,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不一樣,如是否啓用偏向鎖等,對象頭會有不一樣的設置方式。

⑤執行 init 方法: 在上面工做都完成以後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象建立纔剛開始,<init> 方法尚未執行,全部的字段都還爲零。因此通常來講,執行 new 指令以後會接着執行 <init> 方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。

3.2 對象的內存佈局

在 Hotspot 虛擬機中,對象在內存中的佈局能夠分爲3快區域:對象頭實例數據對齊填充

Hotspot虛擬機的對象頭包括兩部分信息第一部分用於存儲對象自身的自身運行時數據(哈希嗎、GC分代年齡、鎖狀態標誌等等),另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是那個類的實例。

實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各類類型的字段內容。

對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位做用。 由於Hotspot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或2倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

3.3 對象的訪問定位

創建對象就是爲了使用對象,咱們的Java程序經過棧上的 reference 數據來操做堆上的具體對象。對象的訪問方式有虛擬機實現而定,目前主流的訪問方式有①使用句柄②直接指針兩種:

  1. 句柄: 若是使用句柄的話,那麼Java堆中將會劃分出一塊內存來做爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;

使用句柄

  1. 直接指針: 若是使用直接指針訪問,那麼 Java 堆對像的佈局中就必須考慮如何防止訪問類型數據的相關信息,reference 中存儲的直接就是對象的地址。

使用直接指針

這兩種對象訪問方式各有優點。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 自己不須要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

四 重點補充內容

String 類和常量池

1 String 對象的兩種建立方式:

String str1 = "abcd";
     String str2 = new String("abcd");
     System.out.println(str1==str2);//false

這兩種不一樣的建立方法是有差異的,第一種方式是在常量池中拿對象,第二種方式是直接在堆內存空間建立一個新的對象。

記住:只要使用new方法,便須要建立新的對象。

2 String 類型的常量池比較特殊。它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。
  • 若是不是用雙引號聲明的 String 對象,可使用 String 提供的 intern 方String.intern() 是一個 Native 方法,它的做用是:若是運行時常量池中已經包含一個等於此 String 對象內容的字符串,則返回常量池中該字符串的引用;若是沒有,則在常量池中建立與此 String 內容相同的字符串,並返回常量池中建立的字符串的引用。
String s1 = new String("計算機");
          String s2 = s1.intern();
          String s3 = "計算機";
          System.out.println(s2);//計算機
          System.out.println(s1 == s2);//false,由於一個是堆內存中的String對象一個是常量池中的String對象,
          System.out.println(s3 == s2);//true,由於兩個都是常量池中的String對

3 String 字符串拼接

String str1 = "str";
          String str2 = "ing";
          
          String str3 = "str" + "ing";//常量池中的對象
          String str4 = str1 + str2; //在堆上建立的新的對象      
          String str5 = "string";//常量池中的對象
          System.out.println(str3 == str4);//false
          System.out.println(str3 == str5);//true
          System.out.println(str4 == str5);//false

儘可能避免多個字符串拼接,由於這樣會從新建立對象。若是須要改變字符串的花,可使用 StringBuilder 或者 StringBuffer。

String s1 = new String("abc");這句話建立了幾個對象?

建立了兩個對象。

驗證:

String s1 = new String("abc");// 堆內存的地值值
        String s2 = "abc";
        System.out.println(s1 == s2);// 輸出false,由於一個是堆內存,一個是常量池的內存,故二者是不一樣的。
        System.out.println(s1.equals(s2));// 輸出true

結果:

false
true

解釋:

先有字符串"abc"放入常量池,而後 new 了一份字符串"abc"放入Java堆(字符串常量"abc"在編譯期就已經肯定放入常量池,而 Java 堆上的"abc"是在運行期初始化階段才肯定),而後 Java 棧的 str1 指向Java堆上的"abc"。

8種基本類型的包裝類和常量池

  • Java 基本類型的包裝類的大部分都實現了常量池技術,即Byte,Short,Integer,Long,Character,Boolean;這5種包裝類默認建立了數值[-128,127]的相應類型的緩存數據,可是超出此範圍仍然會去建立新的對象。
  • 兩種浮點數類型的包裝類 Float,Double 並無實現常量池技術。
Integer i1 = 33;
        Integer i2 = 33;
        System.out.println(i1 == i2);// 輸出true
        Integer i11 = 333;
        Integer i22 = 333;
        System.out.println(i11 == i22);// 輸出false
        Double i3 = 1.2;
        Double i4 = 1.2;
        System.out.println(i3 == i4);// 輸出false

Integer 緩存源代碼:

/**
*此方法將始終緩存-128到127(包括端點)範圍內的值,並能夠緩存此範圍以外的其餘值。
*/
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

應用場景:

  1. Integer i1=40;Java 在編譯的時候會直接將代碼封裝成Integer i1=Integer.valueOf(40);,從而使用常量池中的對象。
  2. Integer i1 = new Integer(40);這種狀況下會建立新的對象。
Integer i1 = 40;
  Integer i2 = new Integer(40);
  System.out.println(i1==i2);//輸出false

Integer比較更豐富的一個例子:

Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);
  
  System.out.println("i1=i2   " + (i1 == i2));
  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));
  System.out.println("i1=i4   " + (i1 == i4));
  System.out.println("i4=i5   " + (i4 == i5));
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));   
  System.out.println("40=i5+i6   " + (40 == i5 + i6));

結果:

i1=i2   true
i1=i2+i3   true
i1=i4   false
i4=i5   false
i4=i5+i6   true
40=i5+i6   true

解釋:

語句i4 == i5 + i6,由於+這個操做符不適用於Integer對象,首先i5和i6進行自動拆箱操做,進行數值相加,即i4 == 40。而後Integer對象沒法與數值進行直接比較,因此i4自動拆箱轉爲int值40,最終這條語句轉爲40 == 40進行數值比較。

寫在最後

開源文檔推薦

Java-Guide:一份涵蓋大部分Java程序員所須要掌握的核心知識,正在一步一步慢慢完善,期待您的參與。

Github地址:https://github.com/Snailclimb/Java-Guide

參考:

你若怒放,清風自來。 歡迎關注個人微信公衆號:「Java面試通關手冊」,一個有溫度的微信公衆號。公衆號有大量資料,回覆關鍵字「1」你可能看到想要的東西哦!

相關文章
相關標籤/搜索