初識Java虛擬機

終於到了Java最核心的知識點JVM,今天主要來學習如下知識點:java

  • Java內存模型
  • 虛擬機對象探祕
  • 類的加載
  • String類和常量池

Java內存模型

Java 虛擬機在執行 Java 程序的過程當中會把它管理的內存劃分紅若干個不一樣的數據區域。JDK. 1.8 和以前的版本略有不一樣,如圖所示:git

2.png

JDK 1.8github

1.png

線程私有的:面試

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

線程共享的:算法

  • 方法區
  • 直接內存 (非運行時數據區的一部分)

程序計數器

程序計數器主要有兩個做用:數組

  • 字節碼解釋器經過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
  • 在多線程的狀況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候可以知道該線程上次運行到哪兒了。

注意:程序計數器是惟一一個不會出現 OutOfMemoryError 的內存區域,它的生命週期隨着線程的建立而建立,隨着線程的結束而死亡。緩存

虛擬機棧

Java 虛擬機棧是由一個個棧幀組成,而每一個棧幀中都擁有:局部變量表、操做數棧、動態連接、方法出口信息。多線程

虛擬機棧中保存的主要內容是棧幀,每一次函數調用都會有一個對應的棧幀被壓入虛擬機棧,每個函數調用結束後,都會有一個棧幀被彈出。jvm

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

Java 虛擬機棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError:若 Java 虛擬機棧的內存大小不容許動態擴展,那麼當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 異常。
  • OutOfMemoryError:若 Java 虛擬機棧的內存大小容許動態擴展,且當線程請求棧時內存用完了,沒法再動態擴展了,此時拋出 OutOfMemoryError 異常。

Java 虛擬機棧也是線程私有的,每一個線程都有各自的 Java 虛擬機棧,並且隨着線程的建立而建立,隨着線程的死亡而死亡。

本地方法棧

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

本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的局部變量表、操做數棧、動態連接、出口信息。

方法執行完畢後相應的棧幀也會出棧並釋放內存空間,也會出現 StackOverFlowErrorOutOfMemoryError 兩種異常。

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

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

3.png

上圖所示的 eden 區、s0 區、s1 區都屬於新生代,tentired 區屬於老年代。大部分狀況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,若是對象還存活,則會進入 s0 或者 s1,而且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增長到必定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold 來設置。

方法區

方法區與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。方法區也被稱爲永久代

JDK 1.8 的時候,方法區(HotSpot 的永久代)被完全移除了,取而代之是元空間,元空間使用的是直接內存

運行時常量池

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

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

JDK1.7 及以後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。

3.jpg

直接內存

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

虛擬機對象探祕

對象的建立

1.png

類加載檢查

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

分配內存

在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存

初始化零值

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

設置對象頭

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

執行init方法

執行init方法其實就是調用類的構造器方法,這樣就能建立出一個可用的Java對象了。

對象的內存佈局

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

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

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

對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位做用。

類的加載

Java類的加載是動態的,它並不會一次性將全部類所有加載後再運行,而是保證程序運行的基礎類(像是基類)徹底加載到jvm中,至於其餘類,則在須要的時候才加載。這固然就是爲了節省內存開銷。

全部的類都由類加載器加載,加載的做用就是將 .class文件加載到內存

類加載器

JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其餘類加載器均由 Java 實現且所有繼承自java.lang.ClassLoader

  1. BootstrapClassLoader(啓動類加載器):最頂層的加載類,由C++實現,負責加載%JAVA_HOME%/lib目錄下的jar包和類或者或被-Xbootclasspath參數指定的路徑中的全部類。
  2. ExtensionClassLoader(擴展類加載器):主要負責加載目錄%JRE_HOME%/lib/ext目錄下的jar包和類,或被java.ext.dirs系統變量所指定的路徑下的jar包。
  3. AppClassLoader(應用程序類加載器):面向咱們用戶的加載器,負責加載當前應用classpath下的全部jar包和類。

2.png

工做過程

  1. 當AppClassLoader加載一個class時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtensionClassLoader去完成。
  2. 當ExtensionClassLoader加載一個class時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
  3. 若是BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用ExtensionClassLoader來嘗試加載;
  4. 若ExtensionClassLoader也加載失敗,則會使用AppClassLoader來加載
  5. 若是AppClassLoader也加載失敗,則會報出異常ClassNotFoundException

其實這就是所謂的雙親委派模型。簡單來講:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上。

雙親委派模型的好處

雙親委派模型保證了Java程序的穩定運行,能夠避免類的重複加載(JVM 區分不一樣類的方式不只僅根據類名,相同的類文件被不一樣的類加載器加載產生的是兩個不一樣的類),也保證了 Java 的核心 API 不被篡改。若是沒有使用雙親委派模型,而是每一個類加載器加載本身的話就會出現一些問題,好比咱們編寫一個稱爲java.lang.Object類的話,那麼程序運行的時候,系統就會出現多個不一樣的Object類。

注意:類加載器在成功加載某個類以後,會把獲得的 java.lang.Class類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。

類加載詳細過程

加載器加載到jvm中,接下來其實又分了好幾個步驟:

加載,查找並加載類的二進制數據,在Java堆中也建立一個java.lang.Class類的對象。

鏈接,鏈接又包含三塊內容:驗證、準備、初始化。
1)驗證,文件格式、元數據、字節碼、符號引用驗證;
2)準備,爲類的靜態變量分配內存,並將其初始化爲默認值;
3)解析,把類中的符號引用轉換爲直接引用

初始化,爲類的靜態變量賦予正確的初始值。

image

自定義類加載器

除了 BootstrapClassLoader 其餘類加載器均由 Java 實現且所有繼承自java.lang.ClassLoader。若是咱們要自定義本身的類加載器,很明顯須要繼承 ClassLoader。

String類和常量池

String 對象的兩種建立方式:

String str1 = "abcd";//先檢查字符串常量池中有沒有"abcd",若是字符串常量池中沒有,則建立一個,而後 str1 指向字符串常量池中的對象,若是有,則直接將 str1 指向"abcd"";
String str2 = new String("abcd");//堆中建立一個新的對象
String str3 = new String("abcd");//堆中建立一個新的對象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false

這兩種不一樣的建立方法是有差異的。

  • 第一種方式是在常量池中拿對象;
  • 第二種方式是直接在堆內存空間建立一個新的對象。

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

3.png

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

直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。
若是不是用雙引號聲明的 String 對象,可使用 String 提供的 intern 方法。String.intern() 是一個 Native 方法,它的做用是:若是運行時常量池中已經包含一個等於此 String 對象內容的字符串,則返回常量池中該字符串的引用;若是沒有,JDK1.7以前(不包含1.7)的處理方式是在常量池中建立與此 String 內容相同的字符串,並返回常量池中建立的字符串的引用,JDK1.7以及以後的處理方式是在常量池中記錄此字符串的引用,並返回該引用。

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 對象

字符串拼接:

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

1.png

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

題目:String s1 = new String("abc");這句話建立了幾個字符串對象?

將建立 1 或 2 個字符串。若是池中已存在字符串常量「abc」,則只會在堆空間建立一個字符串常量「abc」。若是池中沒有字符串常量「abc」,那麼它將首先在池中建立,而後在堆空間中建立,所以將建立總共 2 個字符串對象。

更多關於String的面試題:String常見面試題

總結

今天先對Java虛擬機有個初步的認識,後面咱們會簡單介紹一下垃圾回收機制。

參考

Java內存區域詳解

相關文章
相關標籤/搜索