1. 程式人生 > >針對《面試心得與總結—BAT、網易、蘑菇街》一文中出現的技術問題的收集與整理(3)

針對《面試心得與總結—BAT、網易、蘑菇街》一文中出現的技術問題的收集與整理(3)

JVM

1. 記憶體模型以及分割槽,需要詳細到每個區放什麼

JVM記憶體區域模型

JVM記憶體模型

1.方法區

也稱”永久代” 、“非堆”,  它用於儲存虛擬機器載入的類資訊、常量、靜態變數、是各個執行緒共享的記憶體區域。預設最小值為16MB,最大值為64MB,可以通過-XX:PermSize 和 -XX:MaxPermSize 引數限制方法區的大小。

執行時常量池:是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯器生成的各種符號引用,這部分內容將在類載入後放到方法區的執行時常量池中。

2.虛擬機器棧

描述的是Java 方法執行的記憶體模型:每個方法被執行的時候 都會建立一個“棧幀”用於儲存區域性變量表(包括引數)、操作棧、方法出口等資訊。每個方法被呼叫到執行完的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。宣告週期與執行緒相同,是執行緒私有的。

區域性變量表存放了編譯器可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(引用指標,並非物件本身),其中64位長度的long和double型別的資料會佔用2個區域性變數的空間,其餘資料型別只佔1個。區域性變量表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的區域性變數是完全確定的,在執行期間棧幀不會改變區域性變量表的大小空間。

3.本地方法棧

與虛擬機器棧基本類似,區別在於虛擬機器棧為虛擬機器執行的java方法服務,而本地方法棧則是為Native方法服務。

4.堆

也叫做java 堆、GC堆是java虛擬機器所管理的記憶體中最大的一塊記憶體區域,也是被各個執行緒共享的記憶體區域,在JVM啟動時建立。該記憶體區域存放了物件例項及陣列(所有new的物件)。其大小通過-Xms(最小值)和-Xmx(最大值)引數設定,-Xms為JVM啟動時申請的最小記憶體,預設為作業系統實體記憶體的1/64但小於1G,-Xmx為JVM可申請的最大記憶體,預設為實體記憶體的1/4但小於1G,預設當空餘堆記憶體小於40%時,JVM會增大Heap到-Xmx指定的大小,可通過-XX:MinHeapFreeRation=來指定這個比列;當空餘堆記憶體大於70%時,JVM會減小heap的大小到-Xms指定的大小,可通過XX:MaxHeapFreeRation=來指定這個比列,對於執行系統,為避免在執行時頻繁調整Heap的大小,通常-Xms與-Xmx的值設成一樣。

由於現在收集器都是採用分代收集演算法,堆被劃分為新生代和老年代。新生代主要儲存新建立的物件和尚未進入老年代的物件。老年代儲存經過多次新生代GC(Minor GC)任然存活的物件。

新生代:

程式新建立的物件都是從新生代分配記憶體,新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱S0和S1或From和To)構成,可通過-Xmn引數來指定新生代的大小,也可以通過-XX:SurvivorRation來調整Eden Space及Survivor Space的大小。

老年代:

用於存放經過多次新生代GC任然存活的物件,例如快取物件,新建的物件也有可能直接進入老年代,主要有兩種情況:①.大物件,可通過啟動引數設定-XX:PretenureSizeThreshold=1024(單位為位元組,預設為0)來代表超過多大時就不在新生代分配,而是直接在老年代分配。②.大的陣列物件,切陣列中無引用外部物件。

老年代所佔的記憶體大小為-Xmx對應的值減去-Xmn對應的值。

5.程式計數器

是最小的一塊記憶體區域,它的作用是當前執行緒所執行的位元組碼的行號指示器,在虛擬機器的模型裡,位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、異常處理、執行緒恢復等基礎功能都需要依賴計數器完成。

2. 堆裡面的分割槽:Eden,survival from to,老年代,各自的特點。

HotSpot虛擬機器的分代收集,分為一個Eden區、兩個Survivor去以及Old Generation/Tenured區,其中Eden以及Survivor共同組成New Generatiton/Young space。通常將對New Generation進行的回收稱為Minor GC;對Old Generation進行的回收稱為Major GC,但由於Major GC除併發GC外均需對整個堆以及Permanent Generation進行掃描和回收,因此又稱為Full GC。

  • Eden區是分配物件的區域。
  • Survivor是minor/younger gc後儲存存活物件的區域。
  • Tenured區域儲存長時間存活的物件

1.Eden區

Eden區位於Java堆的年輕代,是新物件分配記憶體的地方,由於堆是所有執行緒共享的,因此在堆上分配記憶體需要加鎖。而Sun JDK為提升效率,會為每個新建的執行緒在Eden上分配一塊獨立的空間由該執行緒獨享,這塊空間稱為TLAB(Thread Local Allocation Buffer)。在TLAB上分配記憶體不需要加鎖,因此JVM在給執行緒中的物件分配記憶體時會盡量在TLAB上分配。如果物件過大或TLAB用完,則仍然在堆上進行分配。如果Eden區記憶體也用完了,則會進行一次Minor GC(young GC)。

2.Survival from to

Survival區與Eden區相同都在Java堆的年輕代。Survival區有兩塊,一塊稱為from區,另一塊為to區,這兩個區是相對的,在發生一次Minor GC後,from區就會和to區互換。在發生Minor GC時,Eden區和Survival from區會把一些仍然存活的物件複製進Survival to區,並清除記憶體。Survival to區會把一些存活得足夠舊的物件移至年老代。

3.年老代

年老代裡存放的都是存活時間較久的,大小較大的物件,因此年老代使用標記整理演算法。當年老代容量滿的時候,會觸發一次Major GC(full GC),回收年老代和年輕代中不再被使用的物件資源。
3. 物件建立方法,物件的記憶體分配,物件的訪問定位。

Java物件的建立大致上有以下幾個步驟:
  1. 類載入檢查:檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,那必須先執行相應的類的載入過程
  2. 為物件分配記憶體:物件所需記憶體的大小在類載入完成後便完全確定,為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。由於堆被執行緒共享,因此此過程需要進行同步處理(分配在TLAB上不需要同步)
  3. 記憶體空間初始化:虛擬機器將分配到的記憶體空間都初始化為零值(不包括物件頭),記憶體空間初始化保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
  4. 物件設定:JVM對物件頭進行必要的設定,儲存一些物件的資訊(指明是哪個類的例項,雜湊碼,GC年齡等)
  5. init:執行完上面的4個步驟後,對JVM來說物件已經建立完畢了,但對於Java程式來說,我們還需要對物件進行一些必要的初始化。

物件的記憶體分配

Java物件的記憶體分配有兩種情況,由Java堆是否規整來決定(Java堆是否規整由所採用的垃圾收集器是否帶有壓縮整理功能決定):
  1. 指標碰撞(Bump the pointer):如果Java堆中的記憶體是規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,分配記憶體也就是把指標向空閒空間那邊移動一段與記憶體大小相等的距離
  2. 空閒列表(Free List):如果Java堆中的記憶體不是規整的,已使用的記憶體和空閒的記憶體相互交錯,就沒有辦法簡單的進行指標碰撞了。虛擬機器必須維護一張列表,記錄哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄

物件的訪問定位

物件的訪問形式取決於虛擬機器的實現,目前主流的訪問方式有使用控制代碼和直接指標兩種:

使用控制代碼:

如果使用控制代碼訪問,Java堆中將會劃分出一塊記憶體來作為控制代碼池,引用中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊:
物件訪問控制代碼
優勢:引用中儲存的是穩定的控制代碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中的例項資料指標,而引用本身不需要修改。

直接指標:

如果使用直接指標訪問物件,那麼物件的例項資料中就包含一個指向物件型別資料的指標,引用中存的直接就是物件的地址: 物件訪問直接指標
優勢:速度更快,節省了一次指標定位的時間開銷,積少成多的效應非常可觀。 4. GC的兩種判定方法:引用計數與引用鏈。
基於引用計數與基於引用鏈這兩大類別的自動記憶體管理方式最大的不同之處在於:前者只需要區域性資訊,而後者需要全域性資訊

引用計數

引用計數顧名思義,就是記錄下一個物件被引用指向的次數。引用計數方式最基本的形態就是讓每個被管理的物件與一個引用計數器關聯在一起,該計數器記錄著該物件當前被引用的次數,每當建立一個新的引用指向該物件時其計數器就加1,每當指向該物件的引用失效時計數器就減1。當該計數器的值降到0就認為物件死亡。每個計數器只記錄了其對應物件的區域性資訊——被引用的次數,而沒有(也不需要)一份全域性的物件圖的生死資訊。由於只維護區域性資訊,所以不需要掃描全域性物件圖就可以識別並釋放死物件;但也因為缺乏全域性物件圖資訊,所以無法處理迴圈引用的狀況。

引用鏈

引用鏈需要記憶體的全域性資訊,當使用引用鏈進行GC時,從物件圖的“根”(GC Root,必然是活的引用,包括棧中的引用,類靜態屬性的引用,常量的引用,JNI的引用等)出發掃描出去,基於引用的可到達性演算法來判斷物件的生死。這使得物件的生死狀態能批量的被識別出來,然後批量釋放死物件。引用鏈不需要顯式維護物件的引用計數,只在GC使用可達性演算法遍歷全域性資訊的時候判斷物件是否被引用,是否存活。 5. GC的三種收集方法:標記清除、標記整理、複製演算法的原理與特點,分別用在什麼地方,如果讓你優化收集方法,有什麼思路?

標記清除

標記清除演算法分兩步執行:
  1. 暫停使用者執行緒,通過GC Root使用可達性演算法標記存活物件
  2. 清除未被標記的垃圾物件
標記清除演算法缺點如下:
  1. 效率較低,需要暫停使用者執行緒
  2. 清除垃圾物件後記憶體空間不連續,存在較多記憶體碎片
標記演算法如今使用的較少了

複製演算法

複製演算法也分兩步執行,在複製演算法中一般會有至少兩片的記憶體空間(一片是活動空間,裡面含有各種物件,另一片是空閒空間,裡面是空的):
  1. 暫停使用者執行緒,標記活動空間的存活物件
  2. 把活動空間的存活物件複製到空閒空間去,清除活動空間
複製演算法相比標記清除演算法,優勢在於其垃圾回收後的記憶體是連續的。 但是複製演算法的缺點也很明顯:
  1. 需要浪費一定的記憶體作為空閒空間
  2. 如果物件的存活率很高,則需要複製大量存活物件,導致效率低下
複製演算法一般用於年輕代的Minor GC,主要是因為年輕代的大部分物件存活率都較低

標記整理

標記整理演算法是標記清除演算法的改進,分為標記、整理兩步:
  1. 暫停使用者執行緒,標記所有存活物件
  2. 移動所有存活物件,按記憶體地址次序一次排列,回收末端物件以後的記憶體空間
標記整理演算法與標記清除演算法相比,整理出的記憶體是連續的;而與複製演算法相比,不需要多片記憶體空間。 然而標記整理演算法的第二步整理過程較為麻煩,需要整理存活物件的引用地址,理論上來說效率要低於複製演算法。 因此標記整理演算法一般引用於老年代的Major GC 參考博文:Java虛擬機器學習(2):垃圾收集演算法

6. GC收集器有哪些?CMS收集器與G1收集器的特點?

常見的GC收集器如下圖所示,連線代表可搭配使用: GC收集器

1.Serial收集器(序列收集器)

用於新生代的單執行緒收集器,收集時需要暫停所有工作執行緒(Stop the world)。優點在於:簡單高效,單個CPU時沒有執行緒互動的開銷,堆較小時停頓時間不長。常與Serial Old 收集器一起使用,示意圖如下所示: 序列收集器

2.ParNew收集器(parallel new 收集器,新生代並行收集器)

Serial收集器多執行緒版本,除了使用多執行緒外和Serial收集器一模一樣。常與Serial Old 收集器一起使用,示意圖如下: ParNew收集器

3.Parallel Scavenge收集器

與ParNew收集器一樣是一款多執行緒收集器,其特點在於關注點與別的GC收集器不同:一般的GC收集器關注於縮短工作執行緒暫停的時間,而該收集器關注於吞吐量,因此也被稱為吞吐量優先收集器。(吞吐量 = 使用者執行程式碼時間 /  (使用者執行程式碼時間 + 垃圾回收時間))高吞吐量與停頓時間短相比主要強調任務快完成,因此常和Parallel Old 收集器一起使用(沒有Parallel Old之前與Serial Old一起使用),示意圖如下: Parallel Old 收集器

4.Serial Old收集器

Serial收集器的年老代版本,不在贅述。

5.Parallel Old收集器

年老代的並行收集器,在JDK1.6開始使用。

6.CMS收集器(Concurrent Mark Sweep,併發標記清除收集器)

CMS收集器是一個年老代的收集器,是以最短回收停頓時間為目標的收集器,其示意圖如下所示: CMS收集器
CMS收集器基於標記清除演算法實現,主要分為4個步驟:
  1. 初始標記,需要stop the world,標記GC Root能關聯到的物件,速度快
  2. 併發標記,對GC Root執行可達性演算法
  3. 重新標記,需要stop the world,修復併發標記時因使用者執行緒執行而產生的標記變化,所需時間比初始標記長,但遠比並發標記短
  4. 併發清理
CMS收集器的缺點在於:
  1. 其對於CPU資源很敏感。在併發階段,雖然CMS收集器不會暫停使用者執行緒,但是會因為佔用了一部分CPU資源而導致應用程式變慢,總吞吐量降低。其預設啟動的回收執行緒數是(cpu數量+3)/4,當cpu數較少的時候,會分掉大部分的cpu去執行收集器執行緒
  2. 無法處理浮動垃圾,浮動垃圾即在併發清除階段因為是併發執行,還會產生垃圾,這一部分垃圾即為浮動垃圾,要等下次收集
  3. CMS收集器使用的是標記清除演算法,GC後會產生碎片

7.G1收集器(Garbage First收集器)

相比CMS收集器,G1收集器主要有兩處改進:
  1. 使用標記整理演算法,確保GC後不會產生記憶體碎片
  2. 可以精確控制停頓,允許指定消耗在垃圾回收上的時間
G1收集器可以實現在基本不犧牲吞吐量的前提下完成低停頓的記憶體回收,這是由於它能夠極力地避免全區域的垃圾收集,之前的收集器進行收集的範圍都是整個新生代或老年代,而G1將整個Java堆(包括新生代、老年代)劃分為多個大小固定的獨立區域(Region),並且跟蹤這些區域裡面的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域(這就是Garbage First名稱的來由)。區域劃分及有優先順序的區域回收,保證了G1收集器在有限的時間內可以獲得最高的收集效率。

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

Minor GC也叫Young GC,當年輕代記憶體滿的時候會觸發,會對年輕代進行GC Full GC也叫Major GC,當年老代滿的時候會觸發,當我們呼叫System.gc時也可能會觸發,會對年輕代和年老代進行GC

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

JVM把class檔案載入的記憶體,並對資料進行校驗、轉換解析和初始化,最終形成JVM可以直接使用的Java型別的過程就是載入機制。
類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的生命週期包括了:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、解除安裝(Unloading)七個階段,其中驗證、準備、解析三個部分統稱連結。

1.載入

在載入階段,虛擬機器需要完成以下事情:
  1. 通過一個類的許可權定名來獲取定義此類的二進位制位元組流
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  3. 在java堆中生成一個代表這個類的java.lang.Class物件,作為方法去這些資料的訪問入口

2.驗證

在驗證階段,虛擬機器主要完成:
  1. 檔案格式驗證:驗證class檔案格式規範
  2. 元資料驗證:這個階段是對位元組碼描述的資訊進行語義分析,以保證起描述的資訊符合java語言規範要求
  3. 位元組碼驗證:進行資料流和控制流分析,這個階段對類的方法體進行校驗分析,這個階段的任務是保證被校驗類的方法在執行時不會做出危害虛擬機器安全的行為
  4. 符號引用驗證:符號引用中通過字串描述的全限定名是否能找到對應的類、符號引用類中的類,欄位和方法的訪問性(private、protected、public、default)是否可被當前類訪問

3.準備

準備階段是正式為類變數(被static修飾的變數)分配記憶體並設定變數初始值(0值)的階段,這些記憶體都將在方法區中進行分配

4.解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程
常見的解析有四種:
  1. 類或介面的解析
  2. 欄位解析
  3. 類方法解析
  4. 介面方法解析

5.初始化

初始化階段才真正開始執行類中定義的java程式程式碼,初始化階段是執行類構造器<clinit>()方法的過程

10. 雙親委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader


上圖中所展示的類載入器之間的這種層次關係,就稱為類載入器的雙親委託模型。雙親委託模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係一般不會以繼承的關係來實現,而是使用組合關係來複用父載入器的程式碼。

public abstract class ClassLoader { 
 
    privatestatic nativevoid registerNatives(); 
    static{ 
        registerNatives(); 
    } 
 
    // The parent class loader for delegation 
    privateClassLoader parent; 
 
    // Hashtable that maps packages to certs 
    privateHashtable package2certs = newHashtable(11); 


雙親委託的工作過程:如果一個類載入器收到了一個類載入請求,它首先不會自己去載入這個類,而是把這個請求委託給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父類載入器反饋自己無法完成載入請求(它管理的範圍之中沒有這個類)時,子載入器才會嘗試著自己去載入。

使用雙親委託模型來組織類載入器之間的關係,有一個顯而易見的好處就是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,例如java.lang.Object存放在rt.jar之中,無論那個類載入器要載入這個類,最終都是委託給啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類,相反,如果沒有雙親委託模型,由各個類載入器去完成的話,如果使用者自己寫一個名為java.lang.Object的類,並放在classpath中,應用程式中可能會出現多個不同的Object類,java型別體系中最基本安全行為也就無法保證。

分派:靜態分派與動態分派

靜態分派

所有依賴靜態型別來定位方法執行版本的分派動作稱為靜態分派,其典型應用是方法過載(過載是通過引數的靜態型別而不是實際型別來選擇過載的版本的)
    class Car {}  
    class Bus extends Car {}  
    class Jeep extends Car {}  
    public class Main {  
        public static void main(String[] args) throws Exception {  
            // Car 為靜態型別,Car 為實際型別  
            Car car1 = new Car();  
            // Car 為靜態型別,Bus 為實際型別  
            Car car2 = new Bus();  
            // Car 為靜態型別,Jeep 為實際型別  
            Car car3 = new Jeep();  
              
            showCar(car1);  
            showCar(car2);  
            showCar(car3);  
        }  
        private static void showCar(Car car) {  
            System.out.println("I have a Car !");  
        }  
        private static void showCar(Bus bus) {  
            System.out.println("I have a Bus !");  
        }  
        private static void showCar(Jeep jeep) {  
            System.out.println("I have a Jeep !");  
        }  
    }  
結果
靜態分派過載

動態分派

與靜態分派類似,動態分派指在在執行期根據實際型別確定方法執行版本,其典型應用是方法重寫(即多型)。 舉例Java程式碼如下:

    class Car {  
        public void showCar() {  
            System.out.println("I have a Car !");  
        }  
    }  
    class Bus extends Car {  
        public void showCar() {  
            System.out.println("I have a Bus !");  
        }  
    }  
    class Jeep extends Car {  
        public void showCar() {  
            System.out.println("I have a Jeep !");  
        }  
    }  
    public class Main {  
        public static void main(String[] args) throws Exception {  
            // Car 為靜態型別,Car 為實際型別  
            Car car1 = new Car();  
            // Car 為靜態型別,Bus 為實際型別  
            Car car2 = new Bus();  
            // Car 為靜態型別,Jeep 為實際型別  
            Car car3 = new Jeep();  
              
            car1.showCar();  
            car2.showCar();  
            car3.showCar();  
        }  
    }  
動態分派重寫
可以看出來重寫是一個根據實際型別決定方法版本的動態分派過程。
參考連結:http://www.importnew.com/20438.html