談談JAVA中JVM裏的那些事


JVM所管理的內存分爲以下幾個運行時數據區:程序計數器、Java虛擬機棧、本地方法棧、Java堆、方法區。

程序計數器(Program Counter Register)

一塊較小的內存空間,它是當前線程所執行的字節碼的信號指示器,字節碼解釋器工作時通過改變該計數器的值來選擇下一條需要執行的字節碼指令,分支、跳轉、循環等基礎功能都要依賴它來實現。每條線程都有一個獨立的的程序計數器,各線程間的計數器互不影響,因此該區域是線程私有的。

當線程在執行一個Java方法時,該計數器記錄的是正在執行的虛擬機字節碼指令的地址,當線程在執行的是Native方法(調用本地操作系統方法)時,該計數器的值爲空。另外,該內存區域是唯一一個在Java虛擬機規範中沒有規定任何OOM(內存溢出:OutOfMemoryError)情況的區域。

Java虛擬機棧(Java Virtual Machine Stacks)

該區域也是線程私有的,它的生命週期也與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀,棧它是用於支持續虛擬機進行方法調用和方法執行的數據結構。對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時,棧幀中需要多大的局部變量表、多深的操作數棧都已經完全確定了,並且寫入了方法表的Code屬性之中。因此,一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。

本地方法棧(Native Method Stacks)

該區域與虛擬機棧所發揮的作用非常相似,只是虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則爲使用到的本地操作系統(Native)方法服務。

Java堆(Java Heap)

Java Heap是Java虛擬機所管理的內存中最大的一塊,它是所有線程共享的一塊內存區域。幾乎所有的對象實例和數組都在這類分配內存。Java Heap是垃圾收集器管理的主要區域,因此很多時候也被稱爲“GC堆”。根據Java虛擬機規範的規定,Java堆可以處在物理上不連續的內存空間中,只要邏輯上是連續的即可。如果在堆中沒有內存可分配時,並且堆也無法擴展時,將會拋出OutOfMemoryError異常。

方法區(Method Area)

方法區也是各個線程共享的內存區域,它用於存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。方法區域又被稱爲“永久代”,但這僅僅對於Sun HotSpot來講,JRockit和IBM J9虛擬機中並不存在永久代的概念。Java虛擬機規範把方法區描述爲Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的內存,可以選擇固定大小或可擴展,另外,虛擬機規範允許該區域可以選擇不實現垃圾回收。相對而言,垃圾收集行爲在這個區域比較少出現。該區域的內存回收目標主要針是對廢棄常量的和無用類的回收。運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Class文件常量池),用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。運行時常量池相對於Class文件常量池的另一個重要特徵是具備動態性,Java語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class文件中的常量池的內容才能進入方法區的運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的是String類的intern()方法。

根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。


內存泄漏和內存溢出的差別

  • 內存泄露是指分配出去的內存沒有被回收回來,由於失去了對該內存區域的控制,因而造成了資源的浪費。Java中一般不會產生內存泄露,因爲有垃圾回收器自動回收垃圾,但這也不絕對,當我們new了對象,並保存了其引用,但是後面一直沒用它,而垃圾回收器又不會去回收它,這邊會造成內存泄露,
  • 內存溢出是指程序所需要的內存超出了系統所能分配的內存(包括動態擴展)的上限。

類型擦除

Java語言在JDK1.5之後引入的泛型實際上只在程序源碼中存在,在編譯後的字節碼文件中,就已經被替換爲了原來的原生類型,並且在相應的地方插入了強制轉型代碼,因此對於運行期的Java語言來說,ArrayList<String>ArrayList<Integer>就是同一個類。所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型被稱爲僞泛型。

下面是一段簡單的Java泛型代碼:

Map<Integer,String> map = new HashMap<Integer,String>();  
map.put(1,"No.1");  
map.put(2,"No.2");  
System.out.println(map.get(1));  
System.out.println(map.get(2));  

將這段Java代碼編譯成Class文件,然後再用字節碼反編譯工具進行反編譯後,將會發現泛型都變回了原生類型,如下面的代碼所示:

Map map = new HashMap();  
map.put(1,"No.1");  
map.put(2,"No.2");  
System.out.println((String)map.get(1));  
System.out.println((String)map.get(2));  

爲了更詳細地說明類型擦除,再看如下代碼:

import java.util.List;  
public class FanxingTest{  
    public void method(List<String> list){  
        System.out.println("List String");  
    }  
    public void method(List<Integer> list){  
        System.out.println("List Int");  
    }  
}  

當我用Javac編譯器編譯這段代碼時,報出瞭如下錯誤:

FanxingTest.java:3: 名稱衝突:method(java.util.List<java.lang.String>) 和 method

(java.util.List<java.lang.Integer>) 具有相同疑符

public void method(List<String> list){

^

FanxingTest.java:6: 名稱衝突:method(java.util.List<java.lang.Integer>) 和 metho

d(java.util.List<java.lang.String>) 具有相同疑符

public void method(List<Integer> list){

^

這是因爲泛型List和List編譯後都被擦除了,變成了一樣的原生類型List,擦除動作導致這兩個方法的特徵簽名變得一模一樣,在Class類文件結構一文中講過,Class文件中不能存在特徵簽名相同的方法。

把以上代碼修改如下:

import java.util.List;  
public class FanxingTest{  
    public int method(List<String> list){  
        System.out.println("List String");  
        return 1;  
    }  
    public boolean method(List<Integer> list){  
        System.out.println("List Int");  
        return true;  
    }  
}  

發現這時編譯可以通過了(注意:Java語言中true和1沒有關聯,二者屬於不同的類型,不能相互轉換,不存在C語言中整數值非零即真的情況)。兩個不同類型的返回值的加入,使得方法的重載成功了。這是爲什麼呢?

我們知道,Java代碼中的方法特徵簽名只包括了方法名稱、參數順序和參數類型,並不包括方法的返回值,因此方法的返回值並不參與重載方法的選擇,這樣看來重載方法加入返回值貌似是多餘的。對於重載方法的選擇來說,這確實是多餘的,但我們現在要解決的問題是讓上述代碼能通過編譯,讓兩個重載方法能夠合理地共存於同一個Class文件中,這就要看字節碼的方法特徵簽名,它不僅包括了Java代碼中方法特徵簽名中所包含的那些信息,還包括方法返回值及受查異常表。爲兩個重載方法加入不同的返回值後,因爲有了不同的字節碼特徵簽名,他們便可以共存於一個Class文件之中。

堆裏面的分區:Eden,survival from to,老年代,各自的特點。

對內存分配情況分析最常見的示例便是對象實例化:

Object obj = new Object();

此段代碼的執行會涉及java棧、Java堆、方法區三個最重要的內存區域。假設該語句出現在方法體中,及時對JVM虛擬機不瞭解的Java使用這,應該也知道obj會作爲引用類型(reference)的數據保存在Java棧的本地變量表中,而會在Java堆中保存該引用的實例化對象,但可能並不知道,Java堆中還必須包含能查找到此對象類型數據的地址信息(如對象類型、父類、實現的接口、方法等),這些類型數據則保存在方法區中。

另外,由於reference類型在Java虛擬機規範裏面只規定了一個指向對象的引用,並沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java堆中的對象的具體位置,因此不同虛擬機實現的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄池和直接使用指針。

GC的兩種判定方法:引用計數與引用鏈。

引用計數方式最基本的形態就是讓每個被管理的對象與一個引用計數器關聯在一起,該計數器記錄着該對象當前被引用的次數,每當創建一個新的引用指向該對象時其計數器就加1,每當指向該對象的引用失效時計數器就減1。當該計數器的值降到0就認爲對象死亡。

Java的內存回收機制可以形象地理解爲在堆空間中引入了重力場,已經加載的類的靜態變量和處於活動線程的堆棧空間的變量是這個空間的牽引對象。這裏牽引對象是指按照Java語言規範,即便沒有其它對象保持對它的引用也不能夠被回收的對象,即Java內存空間中的本原對象。當然類可能被去加載,活動線程的堆棧也是不斷變化的,牽引對象的集合也是不斷變化的。對於堆空間中的任何一個對象,如果存在一條或者多條從某個或者某幾個牽引對象到該對象的引用鏈,則就是可達對象,可以形象地理解爲從牽引對象伸出的引用鏈將其拉住,避免掉到回收池中。

GC的三種收集方法:標記清除、標記整理、複製算法的原理與特點,分別用在什麼地方,如果讓你優化收集方法,有什麼思路?

標記清除算法:是最基礎的收集算法,其他收集算法都是基於這種思想。標記清除算法分爲“標記”和“清除”兩個階段:首先標記出需要回收的對象,標記完成之後統一清除對象。它的主要缺點:①.標記和清除過程效率不高 。②.標記清除之後會產生大量不連續的內存碎片。

標記整理:標記操作和“標記-清除”算法一致,後續操作不只是直接清理對象,而是在清理無用對象完成後讓所有存活的對象都向一端移動,並更新引用其對象的指針。主要缺點:在標記-清除的基礎上還需進行對象的移動,成本相對較高,好處則是不會產生內存碎片。

複製算法:它將可用內存容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊用完之後,就將還存活的對象複製到另外一塊上面,然後在把已使用過的內存空間一次理掉。這樣使得每次都是對其中的一塊進行內存回收,不會產生碎片等情況,只要移動堆訂的指針,按順序分配內存即可,實現簡單,運行高效。主要缺點:內存縮小爲原來的一半。

Minor GC與Full GC分別在什麼時候發生?

Minor GC:通常是指對新生代的回收。指發生在新生代的垃圾收集動作,因爲 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快

Major GC:通常是指對年老代的回收。

Full GC:Major GC除併發gc外均需對整個堆進行掃描和回收。指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裏就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10倍以上。

類加載的五個過程:加載、驗證、準備、解析、初始化。

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括加載、驗證、準備、解析、初始化、使用、卸載

其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,因爲這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。

這裏簡要說明下Java中的綁定:綁定指的是把一個方法的調用與方法所在的類(方法主體)關聯起來,對java來說,綁定分爲靜態綁定和動態綁定:

  • 靜態綁定:即前期綁定。在程序執行前方法已經被綁定,此時由編譯器或其它連接程序實現。針對java,簡單的可以理解爲程序編譯期的綁定。java當中的方法只有final,static,private和構造方法是前期綁定的。
  • 動態綁定:即晚期綁定,也叫運行時綁定。在運行時根據具體對象的類型進行綁定。在java中,幾乎所有的方法都是後期綁定的。

“加載”(Loading)階段是“類加載”(Class Loading)過程的第一個階段,在此階段,虛擬機需要完成以下三件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在Java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區這些數據的訪問入口。

驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

準備階段是爲類的靜態變量分配內存並將其初始化爲默認值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

類初始化是類加載過程的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。



發佈了76 篇原創文章 · 獲贊 6 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章