JVM方法區踩坑深挖

方法區這一部分需要仔細剖析一下。

方法區常被稱爲永久代,本質上兩者並不等價,僅僅是因爲GC分代收集會擴展至方法區。方法區存放已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。需要注意的是,Java6時,String等字符串常量的信息是置於方法區中的,但到了Java7,已經移動到了堆區域。具體的可以通過String.intern()方法體現出來。

類信息包括以下幾種:

1.類型全限定名。

2.類型的直接超類的全限定名(除非這個類型是java.lang.Object,它沒有超類)。

3.類型是類類型還是接口類型。

4.類型的訪問修飾符(public、abstract或final的某個子集)。

5.任何直接超接口的全限定名的有序列表。

6.類型的常量池。

7.字段信息。

8.方法信息。

9.除了常量意外的所有類(靜態)變量。

10.一個到類ClassLoader的引用。

11.一個到Class類的引用

 

需要注意的是,class對象是存放在堆區的,而不是方法區。類的元數據(元數據並不是類的class對象,class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等都是在方法區的)纔是存放在方法區的

方法區中存放的內容在上圖一目瞭然。

永久代

絕大部分 Java 程序員應該都見過 "java.lang.OutOfMemoryError: PermGen space "這個異常。這裏的 “PermGen space”其實指的就是方法區。不過方法區和“PermGen space”又有着本質的區別。前者是 JVM 的規範,而後者則是 JVM 規範的一種實現,並且只有 HotSpot 纔有 “PermGen space”,而對於其他類型的虛擬機,如 JRockit(Oracle)、J9(IBM) 並沒有“PermGen space”。由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出。

Java8中就不存在永久代,取而代之的是元空間(Metaspace)。

元空間的本質與永久代類似,都是對JVM規範中方法區的實現。二者最大的區別在於,元空間並不在虛擬機之中,而是使用本地內存。默認情況下,元空間的大小受本地內存的限制,但是可以通過以下參數來指定元空間的大小。

-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。

-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:

-XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少爲分配空間所導致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少爲釋放空間所導致的垃圾收集

作出永久代向元空間的轉換原因有以下幾點:

1.字符串存在永久代中,容易出現性能問題和內存溢出。

2.類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出

3.永久代會爲GC帶來不必要的複雜度,並且回收效率偏低

------------------------------

運行時常量池:

運行時常量池作爲方法區的一部分。Class文件中除了有類的版本,字段,方法,接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用。這部分內容將在類加載後進入方法區的運行時常量池中存放。

運行時常量池相對於Class文件常量池的另外一個重要特性是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,並非預先放入class文件的常量池的內容才能進入方法區的運行時常量池,運行期間也可能將新的常量放入池中。比如String.intern()方法。

常量池的好處:

常量池是爲了避免頻繁的創建和銷燬對象而影響系統性能,其實現了對象的共享。

例如字符串常量池,在編譯階段就把所有的字符串文字放到一個常量池中。可以節省內存空間,常量池中所有相同的字符串常量可以被合併,只佔用一個空間。同時可以節省運行時間。比較字符串時,==比equals()快。對於兩個引用變量,只用==判斷引用是否相等,也就可以判斷實際值是否相等。

雙等號==的含義

  • 基本數據類型之間應用雙等號,比較的是他們的數值。
  • 複合數據類型(類)之間應用雙等號,比較的是他們在內存中的存放地址。

下面爲常見的考點:

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

Integer i1=40; Java在編譯的時候會直接將代碼封裝成Integer i1=Integer.valueOf(40),從而使用常量池中的對象。同理,i2與i1引用同一個常量池中的對象。

Integer i4=new Integer(40);這種情況下回創建新的對象。

i4與i5分別代表不同的對象。

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

String與常量池

String str1 = "abcd"; String str2 = new String("abcd"); System.out.println(str1==str2);//false String str1 = "str"; String str2 = "ing"; String str3 = "str" + "ing"; String str4 = str1 + str2; System.out.println(str3 == str4);//false String str5 = "string"; System.out.println(str3 == str5);//true

Java中字符串創建的兩種方式:1.字面量形式,如String str="abc"另外一種使用new這種標準的構造對象的方法,如String str=new String("abc");

問題1

當代碼中出現字面量形式創建字符串對象時,JVM首先會對這個字面量進行檢查,如果字符串常量池中存在相同內容的字符串對象的引用,則將這個引用返回,否則新的字符串對象被創建,然後將這個引用放入字符串常量池中,並返回該引用。

當使用new來構造字符串對象時,不管字符串常量池中有沒有相同內容的對象的引用。新的字符串對象都會創建。所以str1與str2代表不同的對象。

注意若str3=str2.intern()則有str1==str3爲true。對於使用new創建的字符串對象,如果想將這個對象的引用加入到字符串常量池中,可以使用intern方法。調用intern後,首先檢查字符串常量池中是否有該對象的引用,如果存在,則將這個引用返回給變量,否則將引用加入並返回給變量。

針對問題2中的str3與str4.

str3等價於直接在字符串常量池中創建一個“string”對象,同時返回引用。str4則是

而字符串拼接中間會產生StringBuilder對象,之後返回sb.toString().

有一道有意思的題目

  1. String s = null;
  2. s += "abc";
  3. System.out.println(s);

答案是nullabc

兩個字符串str1, str2的拼接首先會調用 String.valueOf(obj),這個Obj爲str1,而String.valueOf(Obj)中的實現是return obj == null ? "null" : obj.toString(), 然後產生StringBuilder, 調用的StringBuilder(str1)構造方法, 把StringBuilder初始化,長度爲str1.length()+16,並且調用append(str1)! 接下來調用StringBuilder.append(str2), 把第二個字符串拼接進去, 然後調用StringBuilder.toString返回結果!

所以這裏str3==str4返回false的原因是二者對應的內存地址不一樣。但是equal時,返回true。

這樣一來,str3==str5也就解釋的通了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章