1. 程式人生 > >jvm內存模型-回收算法-和內存分配以及jdk、jre、jvm是什麽關系(阿裏,美團,京東面試題)

jvm內存模型-回收算法-和內存分配以及jdk、jre、jvm是什麽關系(阿裏,美團,京東面試題)

ati alt 字面量 可用內存 硬件 都是 escape 物理 nap

1.什麽是jvm?
(1)jvm是一種用於計算設備的規範,它是一個虛構出來的機器,是通過在實際的計算機上仿真模擬各種功能實現的。
(2)jvm包含一套字節碼指令集,一組寄存器,一個棧,一個垃圾回收堆和一個存儲方法域。
(3)JVM屏蔽了與具體操作系統平臺相關的信息,使Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。
JVM在執行字節碼時,實際上最終還是把字節碼解釋成具體平臺上的機器指令執行。

2.jdk、jre、jvm是什麽關系?
(1)JRE(Java Runtime Environment),也就是java平臺。所有的java程序都要在JRE環境下才能運行。
(2)JDK(Java Development Kit),是開發者用來編譯、調試程序用的開發包。JDK也是JAVA程序需要在JRE上運行。
(3)JVM(Java Virtual Machine),是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。
JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。
Java語言最重要的特點就是跨平臺運行。使用JVM就是為了支持與操作系統無關,實現跨平臺。

3.JVM原理
(1)jvm是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器,可在上面執行字節碼程序。
(2)java編譯器只要面向jvm,生成jvm能理解的字節碼文件。java源文件經編譯成字節碼程序,通過jvm將每條指令翻譯成不同的機器碼
,通過特定平臺運行。(JIT即時編譯器)

技術分享

4. JVM執行程序的過程
1) 加載.class文件
2) 管理並分配內存
3) 執行垃圾收集
JRE(java運行時環境)由JVM構造的java程序的運行環,也是Java程序運行的環境,但是他同時一個操作系統的一個應用程序一個進程,
因此他也有他自己的運行的生命周期,也有自己的代碼和數據空間。
JVM在整個jdk中處於最底層,負責於操作系統的交互,用來屏蔽操作系統環境,
提供一個完整的Java運行環境,因此也就虛擬計算機。

操作系統裝入JVM是通過jdk中Java.exe來完成,
通過下面4步來完成JVM環境:
1) 創建JVM裝載環境和配置
2) 裝載JVM.dll
3) 初始化JVM.dll並掛界到JNIENV(JNI調用接口)實例
4) 調用JNIEnv實例裝載並處理class類。

5. JVM的生命周期


1) JVM實例對應了一個獨立運行的java程序它是進程級別
a) 啟動。啟動一個Java程序時,一個JVM實例就產生了,任何一個擁有public static void
main(String[] args)函數的class都可以作為JVM實例運行的起點
b) 運行。main()作為該程序初始線程的起點,任何其他線程均由該線程啟動。JVM內部有兩種線程:守護線程和非守護線程,main()屬於非守護線程,守護線程通常由JVM自己使用,java程序也可以表明自己創建的線程是守護線程
c) 消亡。當程序中的所有非守護線程都終止時,JVM才退出;若安全管理器允許,程序也可以使用Runtime類或者System.exit()來退出
2) JVM執行引擎實例則對應了屬於用戶運行程序的線程它是線程級別的

6、JVM內存模型

(1)java代碼具體執行過程如下圖,

技術分享

(2)運行時數據區,即jvm內存結構圖如下圖

技術分享

(3)運行時數據區存儲了哪些數據?

  a) 程序計數器(PC寄存器)

    由於在JVM中,多線程是通過線程輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,

  因此,為了能夠使得每個線程都在線程切換後能夠恢復在切 換 之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,並且不能互相被幹擾,

  否則就會影響到程序的正常執行次序。因此,可以這麽說,程序計數器是每個線程所私有的。由於程序計數器中存儲的數據所占空間的大小不會隨程序的執行而發生改變,

  因此,對於程序計數器是不會發生內存溢出現象(OutOfMemory)的。

  b) java棧

    Java棧中存放的是一個個的棧幀,每個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)操作數棧(Operand Stack)

    指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、

    方法返回地址(Return Address)和一些額外的附加信息。當線程執行一個方法時,就會隨之創建一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。 

技術分享

  c)本地方法棧

  本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的

  d)堆

  Java中的堆是用來存儲對象本身的以及數組(數組引用是存放在Java棧中的)。堆是被所有線程共享的,在JVM中只有一個堆。

  e)方法區

  與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。

  在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。

  在方法區中有一個非常重要的部分就是運行時常量池,它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM後,

  對應的運行時常量池就被創建出來。當然並非Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,比如String的intern方法。

  7、JVM內存溢出的情況

  技術分享

  a) 程序計數器(Program Counter Register)

  每條線程都有一個獨立的的程序計數器,各線程間的計數器互不影響,因此該區域是線程私有的。該內存區域是唯一一個在Java虛擬機規範中沒有規定任何OOM(內存溢出:OutOfMemoryError)情況的區域。

  b)Java虛擬機棧(Java Virtual Machine Stacks)

    在Java虛擬機規範中,對這個區域規定了兩種異常情況:

   1、如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常。

  2、如果虛擬機在動態擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

   這兩種情況存在著一些互相重疊的地方:當棧空間無法繼續分配時,到底是內存太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。

     在單線程的操作中,無論是由於棧幀太大,還是虛擬機棧空間太小,當棧空間無法分配時,虛擬機拋出的都是StackOverflowError異常,而不會得到OutOfMemoryError異常。

   而在多線程環境下,則會拋出OutOfMemoryError異常。

  c)堆Java Heap

    Java Heap是Java虛擬機所管理的內存中最大的一塊,它是所有線程共享的一塊內存區域。幾乎所有的對象實例和數組都在這類分配內存。Java Heap是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。

  根據Java虛擬機規範的規定,Java堆可以處在物理上不連續的內存空間中,只要邏輯上是連續的即可。如果在堆中沒有內存可分配時,並且堆也無法擴展時,將會拋出OutOfMemoryError異常。

  d)方法區域,又被稱為“永久代”,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

2、垃圾對象如何確定

Java堆中存放著幾所所有的對象實例,垃圾收集器在對堆進行回收前,首先需要確定哪些對象還"活著",哪些已經"死亡",也就是不會被任何途徑使用的對象。

引用計數法

引用計數法實現簡單,效率較高,在大部分情況下是一個不錯的算法。其原理是:給對象添加一個引用計數器,每當有一個地方引用該對象時,計數器加1,當引用失效時,計數器減1,當計數器值為0時表示該對象不再被使用。需要註意的是:引用計數法很難解決對象之間相互循環引用的問題,主流Java虛擬機沒有選用引用計數法來管理內存。

可達性分析算法

這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如圖所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。

在Java語言中,可作為GC Roots的對象包括下面幾種:

虛擬機棧(棧幀中的本地變量表)中引用的對象。

方法區中類靜態屬性引用的對象。

方法區中常量引用的對象。

本地方法棧中JNI(即一般說的Native方法)引用的對象。

現在問題來了,可達性分析算法會不會出現對象間循環引用問題呢?答案是肯定的,那就是不會出現對象間循環引用問題。GC Root在對象圖之外,是特別定義的“起點”,不可能被對象圖內的對象所引用。

對象生存還是死亡(To Die Or Not To Die)

即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finapze()方法。當對象沒有覆蓋finapze()方法,或者finapze()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。程序中可以通過覆蓋finapze()來一場"驚心動魄"的自我拯救過程,但是,這只有一次機會呦。

技術分享
/**
 * 此代碼演示了兩點:
 * 1.對象可以在被GC時自我拯救。
 * 2.這種自救的機會只有一次,因為一個對象的finapze()方法最多只會被系統自動調用一次 
 * @author zzm
 */
pubpc class FinapzeEscapeGC {
  
 pubpc static FinapzeEscapeGC SAVE_HOOK = null;
  
 pubpc void isApve() {
  System.out.println("yes, i am still apve :)");
 }
  
 @Override
 protected void finapze() throws Throwable {
  super.finapze();
  System.out.println("finapze mehtod executed!");
  FinapzeEscapeGC.SAVE_HOOK = this;
 }
  
 pubpc static void main(String[] args) throws Throwable {
  SAVE_HOOK = new FinapzeEscapeGC();
  
  //對象第一次成功拯救自己
  SAVE_HOOK = null;
  System.gc();
  //因為finapze方法優先級很低,所以暫停0.5秒以等待它
  Thread.sleep(500);
  if (SAVE_HOOK != null) {
SAVE_HOOK.isApve();
  } else {
System.out.println("no, i am dead :(");
  }
  
  //下面這段代碼與上面的完全相同,但是這次自救卻失敗了
  SAVE_HOOK = null;
  System.gc();
  //因為finapze方法優先級很低,所以暫停0.5秒以等待它
  Thread.sleep(500);
  if (SAVE_HOOK != null) {
SAVE_HOOK.isApve();
  } else {
System.out.println("no, i am dead :(");
  }
 }
}
技術分享

運行結果為:

finapze mehtod executed! 
yes, i am still apve :)
no, i am dead :(

接著說引用

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判定對象是否存活都與“引用”有關。在JDK 1.2以前,Java中的引用的定義很傳統:如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表著一個引用。在JDK 1.2之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

? 強引用就是指在程序代碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

? 軟引用是用來描述一些還有用但並非必需的對象。對於軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。

? 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2之後,提供了WeakReference類來實現弱引用。

? 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類來實現虛引用。

軟引用使用示例:

技術分享
package jvm;
 
import java.lang.ref.SoftReference;
 
class Node {
pubpc String msg = "";
}
 
pubpc class Hello {
pubpc static void main(String[] args) {
Node node1 = new Node(); // 強引用
node1.msg = "node1";
SoftReference<Node> node2 = new SoftReference<Node>(node1); // 軟引用
node2.get().msg = "node2";
 
System.out.println(node1.msg);
System.out.println(node2.get().msg);
}
}
技術分享

出結果為:

node2
node2

3、典型的垃圾回收算法

1.Mark-Sweep(標記-清除)算法

這是最基礎的垃圾回收算法,之所以說它是最基礎的是因為它最容易實現,思想也是最簡單的。標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間。具體過程如下圖所示:

從圖中可以很容易看出標記-清除算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。

2. Copying(復制)算法

為了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。具體過程如下圖所示:

這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。

很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那麽Copying算法的效率將會大大降低。

3. Mark-Compact(標記-整理)算法

為了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。具體過程如下圖所示:

4.Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存劃分為若幹個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation)老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麽就可以根據不同代的特點采取最適合的收集算法

目前大部分垃圾收集器對於新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數較少,但是實際中並不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間(一般為8:1:1),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象復制到另一塊Survivor空間中,然後清理掉Eden和剛才使用過的Survivor空間

而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

5 新生代和老年代的區別(阿裏面試官的題目):

**所謂的新生代和老年代是針對於分代收集算法來定義的,新生代又分為Eden和Survivor兩個區。加上老年代就這三個區。數據會首先分配到Eden區 當中(當然也有特殊情況,如果是大對象那麽會直接放入到老年代(大對象是指需要大量連續內存空間的java對象)。),當Eden沒有足夠空間的時候就會 觸發jvm發起一次Minor GC。如果對象經過一次Minor GC還存活,並且又能被Survivor空間接受,那麽將被移動到Survivor空 間當中。並將其年齡設為1,對象在Survivor每熬過一次Minor GC,年齡就加1,當年齡達到一定的程度(默認為15)時,就會被晉升到老年代 中了,當然晉升老年代的年齡是可以設置的。

其實新生代和老年代就是針對於對象做分區存儲,更便於回收等等**


技術分享
技術分享
技術分享
技術分享

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
參考知乎回復
1 類加載後,類信息放在方法區,也就是所謂的PermGen的,如果真像題主說的無限類,必然出現OOM

2 其他參考
技術分享
技術分享

參考資料:http://www.cnblogs.com/fubaizhaizhuren/p/4976839.html

參考:http://blog.csdn.net/steady_pace/article/details/51254740

jvm內存模型-回收算法-和內存分配以及jdk、jre、jvm是什麽關系(阿裏,美團,京東面試題)