JVM內存分配以及存儲總結

最近看了一下JVM的內存分配,還是比較複雜的。這裏做個總結,首先一個common sense就是操作系統會爲每個java進程實例化一個jvm實例。jvm然後再來運行java程序,具體的過程就不多說了,簡單來說就是核心classloader如bootstrap, extention, System對類的加載(一定是此順序,jvm對類的加載採取的是代理委託方式,防止核心類被hack),找到對應的main入口來運行。

這裏主要是想總結一下,每個java進程對應的jvm對內存的分配,運行時是什麼樣的。我們都知道jvm內存分爲
1. PC計數器
2. 堆
3. 棧(jvm棧,本地方法棧)
4. 方法區(包括class信息,靜態變量,class文件常量池,運行時常量池,JIT緩存代碼)。

其中,PC計數器,棧是線程私有的,而堆和方法區是線程共享的。操作系統分配給jvm的內存的總大小是有限的,這和操作系統以及是32bit還是64bit,具體的jvm實現都有關係。另外,堆和方法區內存的大小是可以在jvm啓動參數中配置的。我們程序員平時經常說的內存操作,OOM都是指的堆內存。

這裏順便說一下JVM的GC機制,JVM的GC不是採用的引用計數算法,而是可達性分析算法。堆根據GC回收的分代算法,又可以分爲新生代Eden+2*Survivor, 和老年代。對新生代進行標記-複製算法,進行一次Minor GC,而對老年代進行標記-整理算法,進行一次Major/Full GC,當然肯定有一次Minor GC。程序中絕大部分new的對象放置在Eden區,少數比如大型對象,數組,和長期存活對象直接進入老年代。進入新生代的根據對象年齡計數器可以逐步晉升至老年代中。

那麼對於不同的區域,什麼時候存放什麼東西都是有規範的。

總結如下:
1. 堆: 1) 類的成員變量引用,這條實際上是對2)的說明。 2)new且只有new出來的對象放在堆裏。
2. 棧:運行時,成員函數的局部變量引用及其字面量。
3. 方法區:1) class 文件常量池,存放成員變量裏的字面量,字符串常量。 2) 靜態成員變量。 3)運行時常量池,存放成員函數運行時的常量。

現在舉例說明:

class A{
    int i1 = 1; //i1在堆中A對象裏,1在方法區的class文件常量池中
    String s1 = "abc"; //s1在堆中A對象裏,"abc"在方法區的class文件常量池中
    String s2 = new String("abc"); //s2在堆中A對象裏,"abc"在方法區的class文件常量池中,只有一份,new出來的String對象在堆裏

    static int i2 = 2; //i2在方法區,2在方法區的class文件常量池中
    //s3在方法區,"xyz"在方法區的class文件常量池中
    static String s3 = "xyz"; 
    //s4在方法區,"xyz"在方法區的class文件常量池中,只有一份,new出來的String對象在堆裏
    static String s4 = new String("xyz");

    public void func(){
        int i3 = 3; //i3在棧中,字面量3也在棧中
        int i4 = 3; //i4在棧中,字面量3已經存在,此時只有一份
        String s5 = "china"; //s5在棧中,"china"在方法區的運行時常量池中
        String s6 = new String("china"); //s6在棧中,"china"在方法區的運行時常量池中已經有一份相同拷貝,不再存, new出來的對象在堆中
    } 
}

那麼,當A被classloader裝載並且調用func函數的時候,其所在的jvm內存中不同的地方都有哪些關於A的信息呢?分析如下:

  1. 首先jvm第一次碰到A時,比如new A()時,會查看方法區裏是否已經存放過關於此類的信息,如果沒有,則先調用classloader(還是按照那個Bootstrap, extention…的順序),最後裝載A,此時,方法區裏就有關於A類的Class信息了,並且由於在編譯期間就能確定成員變量所引用的常量,因此,此時class文件常量池也會有信息,i1所引用的1,s1所引用的”abc”, i2所引用的2,s3所引用的”xyz”。
  2. new A()緊接着會導致在堆中分配關於A的內存,其中包括成員變量i1, s1, s2。其實s2所引用的new String(“abc”)中”abc”也是編譯期間就能夠確定,因此這裏的”abc”也會存在class文件常量池,於是會先去class文件常量池找是否已經有一份相同的,由於之前已經有一份,於是不再存第二份。而new出來的String(“abc”)對象會在堆中有一份。
  3. 由於i2, s3, s4都是靜態變量,因此它們存在方法區,2和”xyz”存在class文件常量池,注意”xyz”也只有一份,道理同2。另外s4引用的new對象也會在堆中有一份。
  4. 當程序運行調用A的func函數時,此時,jvm棧就開始工作了,會有一個關於func函數的棧幀(statck Frame),入棧,裏面有i3, i4變量引用和常量或者說字面量3,注意3此時在棧中只有一份!如果後期i4被賦值爲4,則棧會開闢新的空間存一個4,i3不變仍然爲3。s5,s6也在棧中,”china”由於是在運行時才確定,因此存放在方法區的運行時常量池中,s6所引用的new的String(“china”)中的“china”也只在運行時常量池中保存一份,另外new會在堆中開闢一個新的對象空間存放此對象。

因此,對於equals相等的字符串,在常量池(class文件常量池或者運行時常量池)中永遠只有一份,在堆中有多份。因爲String類重寫/覆蓋了Object類的equals方法,只要字符串內容相等即爲true,而Object是必須同一個對象的引用地址才爲true。但是String並沒有重寫/覆蓋==操作符,所以String對象的==還是隻有同一個對象的地址引用才爲true。

並且,延伸出來很多面試題的答案,比如:

1) String s = new String(“xyz”); 產生幾個對象?

一個或兩個。如果常量池中原來沒有 ”xyz”, 就是兩個。如果原來的常量池中存在“xyz”時,就是一個。

2) String作爲一個對象來使用

例子一:對象不同,內容相同,”==”返回false,equals返回true

String s1 = new String(“java”);
String s2 = new String(“java”);

System.out.println(s1==s2); //false
System.out.println(s1.equals(s2)); //true

例子二:同一對象,”==”和equals結果相同

String s1 = new String(“java”);
String s2 = s1;

System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
String作爲一個基本類型來使用

如果值不相同,對象就不相同,所以”==” 和equals結果一樣

String s1 = “java”;
String s2 = “java”;

System.out.println(s1==s2); //true
System.out.println(s1.equals(s2)); //true
如果String緩衝池內不存在與其指定值相同的String對象,那麼此時虛擬機將爲此創建新的String對象,並存放在String緩衝池內。

如果String緩衝池內存在與其指定值相同的String對象,那麼此時虛擬機將不爲此創建新的String對象,而直接返回已存在的String對象的引用。

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