Java虛擬機詳解----JVM常見問題總結

【正文】html

聲明:本文只是作一個總結,有關jvm的詳細知識能夠參考本人以前的系列文章,尤爲是那篇:Java虛擬機詳解04----GC算法和種類。那篇文章和本文是面試時的重點。java

面試必問關鍵詞:JVM垃圾回收、類加載機制面試

 

先把本文的目錄畫一個思惟導圖:(圖的源文件在本文末尾)算法

 

1、Java引用的四種狀態:數據庫

強引用:編程

  用的最廣。咱們平時寫代碼時,new一個Object存放在堆內存,而後用一個引用指向它,這就是強引用。緩存

  若是一個對象具備強引用,那垃圾回收器毫不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足的問題。服務器

軟引用:網絡

  若是一個對象只具備軟引用,則內存空間足夠時,垃圾回收器就不會回收它;若是內存空間不足了,就會回收這些對象的內存。(備註:若是內存不足,隨時有可能被回收。)數據結構

  只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。

弱引用:

  弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期

  每次執行GC的時候,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象

虛引用:

  「虛引用」顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收器回收

  虛引用主要用來跟蹤對象被垃圾回收器回收的活動。

注:關於各類引用的詳解,能夠參考這篇博客:

http://zhangjunhd.blog.51cto.com/113473/53092

 

2、Java中的內存劃分:

Java程序在運行時,須要在內存中的分配空間。爲了提升運算效率,就對數據進行了不一樣空間的劃分,由於每一片區域都有特定的處理數據方式和內存管理方式。

1dca5ecf-0959-46f9-98c0-f92acc31005f

上面這張圖就是jvm運行時的狀態。具體劃分爲以下5個內存空間:(很是重要)

  • 程序計數器:保證線程切換後能恢復到原來的執行位置
  • 虛擬機棧:(棧內存)爲虛擬機執行java方法服務:方法被調用時建立棧幀-->局部變量表->局部變量、對象引用
  • 本地方法棧:爲虛擬機執使用到的Native方法服務
  • 堆內存:存放全部new出來的東西
  • 方法區:存儲被虛擬機加載的類信息、常量、靜態常量、靜態方法等。
  • 運行時常量池(方法區的一部分)

GC對它們的回收:

內存區域中的程序計數器、虛擬機棧、本地方法棧這3個區域隨着線程而生,線程而滅棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧的操做,每一個棧幀中分配多少內存基本是在類結構肯定下來時就已知的。在這幾個區域不須要過多考慮回收的問題,由於方法結束或者線程結束時,內存天然就跟着回收了。

GC回收的主要對象:而Java堆和方法區則不一樣,一個接口中的多個實現類須要的內存可能不一樣,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間時才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,GC關注的也是這部份內存,後面的文章中若是涉及到「內存」分配與回收也僅指着一部份內存。

 

一、程序計數器:(線程私有)

每一個線程擁有一個程序計數器,在線程建立時建立,

指向下一條指令的地址

執行本地方法時,其值爲undefined

說的通俗一點,咱們知道,Java是支持多線程的,程序先去執行A線程,執行到一半,而後就去執行B線程,而後又跑回來接着執行A線程,那程序是怎麼記住A線程已經執行到哪裏了呢?這就須要程序計數器了。所以,爲了線程切換後可以恢復到正確的執行位置,每條線程都有一個獨立的程序計數器,這塊兒屬於「線程私有」的內存。

 

二、Java虛擬機棧:(線程私有)

每一個方法被調用的時候都會建立一個棧幀,用於存儲局部變量表、操做棧、動態連接、方法出口等信息。局部變量表存放的是:編譯期可知的基本數據類型、對象引用類型。

    每一個方法被調用直到執行完成的過程,就對應着一個棧幀在虛擬機中從入棧到出棧的過程。

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

  (1)若是線程請求的棧深度太深,超出了虛擬機所容許的深度,就會出現StackOverFlowError(好比無限遞歸。由於每一層棧幀都佔用必定空間,而 Xss 規定了棧的最大空間,超出這個值就會報錯)

  (2)虛擬機棧能夠動態擴展,若是擴展到沒法申請足夠的內存空間,會出現OOM

 

三、本地方法棧:

(1)本地方法棧與java虛擬機棧做用很是相似,其區別是:java虛擬機棧是爲虛擬機執行java方法服務的,而本地方法棧則爲虛擬機執使用到的Native方法服務

(2)Java虛擬機沒有對本地方法棧的使用和數據結構作強制規定,Sun HotSpot虛擬機就把java虛擬機棧和本地方法棧合二爲一。

(3)本地方法棧也會拋出StackOverFlowError和OutOfMemoryError。

 

四、Java堆:即堆內存(線程共享)

(1)堆是java虛擬機所管理的內存區域中最大的一塊,java堆是被全部線程共享的內存區域,在java虛擬機啓動時建立,堆內存的惟一目的就是存放對象實例幾乎全部的對象實例都在堆內存分配。

(2)堆是GC管理的主要區域,從垃圾回收的角度看,因爲如今的垃圾收集器都是採用的分代收集算法,所以java堆還能夠初步細分爲新生代和老年代

(3)Java虛擬機規定,堆能夠處於物理上不連續的內存空間中,只要邏輯上連續的便可。在實現上既能夠是固定的,也能夠是可動態擴展的。若是在堆內存沒有完成實例分配,而且堆大小也沒法擴展,就會拋出OutOfMemoryError異常。

 

五、方法區:(線程共享)

(1)用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

(2)Sun HotSpot虛擬機把方法區叫作永久代(Permanent Generation),方法區中最終要的部分是運行時常量池。

 

六、運行時常量池:

(1)運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時就會拋出OutOfMemoryError異常。 

注:關於本段的詳細內容,能夠參考本人的另一篇博客:Java虛擬機詳解02----JVM內存結構

 

3、Java對象在內存中的狀態:

可達的/可觸及的:

  Java對象被建立後,若是被一個或多個變量引用,那就是可達的。即從根節點能夠觸及到這個對象。

  其實就是從根節點掃描,只要這個對象在引用鏈中,那就是可觸及的。

可恢復的:

  Java對象再也不被任何變量引用就進入了可恢復狀態。

  在回收該對象以前,該對象的finalize()方法進行資源清理。若是在finalize()方法中從新讓變量引用該對象,則該對象再次變爲可達狀態,不然該對象進入不可達狀態

不可達的:

  Java對象不被任何變量引用,且系統在調用對象的finalize()方法後依然沒有使該對象變成可達狀態(該對象依然沒有被變量引用),那麼該對象將變成不可達狀態。

  當Java對象處於不可達狀態時,系統纔會真正回收該對象所佔有的資源。

 

4、判斷對象死亡的兩種經常使用算法:

    當對象不被引用的時候,這個對象就是死亡的,等待GC進行回收。

一、引用計數算法

概念:

  給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。

可是:

  主流的java虛擬機並無選用引用計數算法來管理內存,其中最主要的緣由是:它很難解決對象之間相互循環引用的問題

優勢:

  算法的實現簡單,斷定效率也高,大部分狀況下是一個不錯的算法。不少地方應用到它

缺點:

引用和去引用伴隨加法和減法,影響性能

致命的缺陷:對於循環引用的對象沒法進行回收

二、根搜索算法:(jvm採用的算法)

概念:

  設立若干種根對象,當任何一個根對象(GC Root)到某一個對象均不可達時,則認爲這個對象是能夠被回收的。

注:這裏提到,設立若干種根對象,當任何一個根對象到某一個對象均不可達時,則認爲這個對象是能夠被回收的。咱們在後面介紹標記-清理算法/標記整理算法時,也會一直強調從根節點開始,對全部可達對象作一次標記,那什麼叫作可達呢?

可達性分析:

  從根(GC Roots)的對象做爲起始點,開始向下搜索,搜索所走過的路徑稱爲「引用鏈」,當一個對象到GC Roots沒有任何引用鏈相連(用圖論的概念來說,就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。

83fbdb3a-6da2-4307-9cc5-63663080c2ce

如上圖所示,ObjectD和ObjectE是互相關聯的,可是因爲GC roots到這兩個對象不可達,因此最終D和E仍是會被當作GC的對象,上圖如果採用引用計數法,則A-E五個對象都不會被回收。

 

根(GC Roots):

說到GC roots(GC根),在JAVA語言中,能夠當作GC roots的對象有如下幾種:

一、(棧幀中的本地變量表)中引用的對象

二、方法區中的靜態成員。

三、方法區中的常量引用的對象(全局變量)

四、本地方法棧中JNI(通常說的Native方法)引用的對象。

注:第一和第四種都是指的方法的本地變量表,第二種表達的意思比較清晰,第三種主要指的是聲明爲final的常量值。

在根搜索算法的基礎上,現代虛擬機的實現當中,垃圾蒐集的算法主要有三種,分別是標記-清除算法複製算法標記-整理算法。這三種算法都擴充了根搜索算法,不過它們理解起來仍是很是好理解的。

 

5、垃圾回收算法:

一、標記-清除算法:

概念:

標記階段:先經過根節點,標記全部從根節點開始的可達對象。所以,未被標記的對象就是未被引用的垃圾對象;

清除階段:清除全部未被標記的對象。

缺點:

標記和清除的過程效率不高(標記和清除都須要從頭遍歷到尾)

標記清除後會產生大量不連續的碎片

二、複製算法:(新生代的GC)

概念:

  將原有的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象複製到未使用的內存塊中,而後清除正在使用的內存塊中的全部對象。

優勢:

這樣使得每次都是對整個半區進行回收,內存分配時也就不用考慮內存碎片等狀況

只要移動堆頂指針,按順序分配內存便可,實現簡單,運行效率高

缺點:空間的浪費

  從以上描述不難看出,複製算法要想使用,最起碼對象的存活率要很是低才行。

  如今的商業虛擬機都採用這種收集算法來回收新生代,新生代中的對象98%都是「朝生夕死」的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊比較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是說,每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的空間會被浪費。

固然,98%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用時,須要依賴於老年代進行分配擔保,因此大對象直接進入老年代。整個過程以下圖所示:

37e28257-008e-40ad-a07f-1a45a38d4be2

 

 

三、標記-整理算法:(老年代的GC)

    複製算法在對象存活率高的時候要進行較多的複製操做,效率將會下降,因此在老年代中通常不能直接選用這種算法。

概念:

標記階段:先經過根節點,標記全部從根節點開始的可達對象。所以,未被標記的對象就是未被引用的垃圾對象

整理階段:將全部的存活對象壓縮到內存的一端;以後,清理邊界外全部的空間

優勢:

  不會產生內存碎片。

缺點:

  在標記的基礎之上還須要進行對象的移動,成本相對較高,效率也不高。

 

它們的區別以下:(>表示前者要優於後者,=表示二者效果同樣)

(1)效率:複製算法 > 標記/整理算法 > 標記/清除算法(此處的效率只是簡單的對比時間複雜度,實際狀況不必定如此)。

(2)內存整齊度:複製算法=標記/整理算法>標記/清除算法。

(3)內存利用率:標記/整理算法=標記/清除算法>複製算法。

注1:標記-整理算法不只能夠彌補標記-清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價。

注2:能夠看到標記/清除算法是比較落後的算法了,可是後兩種算法倒是在此基礎上創建的。

注3:時間與空間不可兼得。

 

四、分代收集算法:

  當前商業虛擬機的GC都是採用的「分代收集算法」,這並非什麼新的思想,只是根據對象的存活週期的不一樣將內存劃分爲幾塊兒。通常是把Java堆分爲新生代和老年代:短命對象歸爲新生代,長命對象歸爲老年代

  • 存活率低:少許對象存活,適合複製算法:在新生代中,每次GC時都發現有大批對象死去,只有少許存活(新生代中98%的對象都是「朝生夕死」),那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成GC。
  • 存活率高:大量對象存活,適合用標記-清理/標記-整理:在老年代中,由於對象存活率高、沒有額外空間對他進行分配擔保,就必須使用「標記-清理」/「標記-整理」算法進行GC。

注:老年代的對象中,有一小部分是由於在新生代回收時,老年代作擔保,進來的對象;絕大部分對象是由於不少次GC都沒有被回收掉而進入老年代

 

6、垃圾收集器:

若是說收集算法時內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

雖然咱們在對各類收集器進行比較,但並不是爲了挑出一個最好的收集器。由於直到如今位置尚未最好的收集器出現,更加沒有萬能的收集器,因此咱們選擇的只是對具體應用最合適的收集器

一、Serial收集器:(串行收集器)

這個收集器是一個單線程的收集器,但它的單線程的意義並不只僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工做,更重要的是在它進行垃圾收集時,必須暫停其餘全部的工做線程(Stop-The-World:將用戶正常工做的線程所有暫停掉),直到它收集結束。收集器的運行過程以下圖所示:

9012f0bf-fe1b-4e19-a7ec-b5c0e6fb7174

上圖中:

  • 新生代採用複製算法,Stop-The-World
  • 老年代採用標記-整理算法,Stop-The-World

當它進行GC工做的時候,雖然會形成Stop-The-World,但它存在有存在的緣由:正是由於它的簡單而高效(與其餘收集器的單線程比),對於限定單個CPU的環境來講,沒有線程交互的開銷,專心作GC,天然能夠得到最高的單線程手機效率。因此Serial收集器對於運行在client模式下是一個很好的選擇(它依然是虛擬機運行在client模式下的默認新生代收集器)。

 

二、ParNew收集器:Serial收集器的多線程版本(使用多條線程進行GC)

  ParNew收集器是Serial收集器的多線程版本。

  它是運行在server模式下的首選新生代收集器,除了Serial收集器外,目前只有它能與CMS收集器配合工做。CMS收集器是一個被認爲具備劃時代意義的併發收集器,所以若是有一個垃圾收集器能和它一塊兒搭配使用讓其更加完美,那這個收集器必然也是一個不可或缺的部分了。收集器的運行過程以下圖所示:

852249d2-1be3-4126-8cb7-24909c404e17

上圖中:

  • 新生代採用複製算法,Stop-The-World
  • 老年代採用標記-整理算法,Stop-The-World

 

三、ParNew Scanvenge收集器

  相似ParNew,但更加關注吞吐量。目標是:達到一個可控制吞吐量的收集器。

停頓時間和吞吐量不可能同時調優。咱們一方買但願停頓時間少,另一方面但願吞吐量高,其實這是矛盾的。由於:在GC的時候,垃圾回收的工做總量是不變的,若是將停頓時間減小,那頻率就會提升;既然頻率提升了,說明就會頻繁的進行GC,那吞吐量就會減小,性能就會下降。

吞吐量:CPU用於用戶代碼的時間/CPU總消耗時間的比值,即=運行用戶代碼的時間/(運行用戶代碼時間+垃圾收集時間)。好比,虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

 

四、G1收集器:

  是當今收集器發展的最前言成果之一,知道jdk1.7,sun公司才認爲它達到了足夠成熟的商用程度。

優勢:

  它最大的優勢是結合了空間整合,不會產生大量的碎片,也下降了進行gc的頻率。

  二是可讓使用者明確指定指定停頓時間。(能夠指定一個最小時間,超過這個時間,就不會進行回收了)

它有了這麼高效率的緣由之一就是:對垃圾回收進行了劃分優先級的操做,這種有優先級的區域回收方式保證了它的高效率。

若是你的應用追求停頓,那G1如今已經能夠做爲一個可嘗試的選擇;若是你的應用追求吞吐量,那G1並不會爲你帶來什麼特別的好處。

注:以上全部的收集器當中,當執行GC時,都會stop the world,可是下面的CMS收集器卻不會這樣。

 

五、CMS收集器:(老年代收集器)

CMS收集器(Concurrent Mark Sweep:併發標記清除)是一種以獲取最短回收停頓時間爲目標的收集器。適合應用在互聯網站或者B/S系統的服務器上,這類應用尤爲重視服務器的響應速度,但願系統停頓時間最短。

CMS收集器運行過程:(着重實現了標記的過程)

(1)初始標記

  根能夠直接關聯到的對象

  速度快

(2)併發標記(和用戶線程一塊兒)

  主要標記過程,標記所有對象

(3)從新標記

  因爲併發標記時,用戶線程依然運行,所以在正式清理前,再作修正

(4)併發清除(和用戶線程一塊兒)

  基於標記結果,直接清理對象

整個過程以下圖所示:

89af7bbc-5331-4c62-ab7e-18e93350f826

上圖中,初始標記和從新標記時,須要stop the world。整個過程當中耗時最長的是併發標記和併發清除,這兩個過程均可以和用戶線程一塊兒工做。

 

優勢:

  併發收集,低停頓

缺點:

(1)致使用戶的執行速度下降。

(2)沒法處理浮動垃圾。由於它採用的是標記-清除算法。有可能有些垃圾在標記以後,須要等到下一次GC纔會被回收。若是CMS運行期間沒法知足程序須要,那麼就會臨時啓用Serial Old收集器來從新進行老年代的手機。

(3)因爲採用的是標記-清除算法,那麼就會產生大量的碎片。每每會出現老年代還有很大的空間剩餘,可是沒法找到足夠大的連續空間來分配當前對象,不得不提早觸發一次full GC

 

疑問:既然標記-清除算法會形成內存空間的碎片化,CMS收集器爲何使用標記清除算法而不是使用標記整理算法:

答案:

  CMS收集器更加關注停頓,它在作GC的時候是和用戶線程一塊兒工做的(併發執行),若是使用標記整理算法的話,那麼在清理的時候就會去移動可用對象的內存空間,那麼應用程序的線程就頗有可能找不到應用對象在哪裏

7、Java堆內存劃分:

根據對象的存活率(年齡),Java對內存劃分爲3種:新生代、老年代、永久代:

一、新生代:

好比咱們在方法中去new一個對象,那這方法調用完畢後,對象就會被回收,這就是一個典型的新生代對象。 

如今的商業虛擬機都採用這種收集算法來回收新生代,新生代中的對象98%都是「朝生夕死」的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊比較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是說,每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的空間會被浪費。

固然,98%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用時,須要依賴於老年代進行分配擔保,因此大對象直接進入老年代。同時,長期存活的對象將進入老年代(虛擬機給每一個對象定義一個年齡計數器)。

來看下面這張圖:

ef34e1c5-c6e1-4108-a939-87c9f5de0fff

Minor GC和Full GC:

GC分爲兩種:Minor GC和Full GC

Minor GC:

  Minor GC是發生在新生代中的垃圾收集動做,採用的是複製算法。

對象在Eden和From區出生後,在通過一次Minor GC後,若是對象還存活,而且可以被to區所容納,那麼在使用複製算法時這些存活對象就會被複制到to區域,而後清理掉Eden區和from區,並將這些對象的年齡設置爲1,之後對象在Survivor區每熬過一次Minor GC,就將對象的年齡+1,當對象的年齡達到某個值時(默認是15歲,能夠經過參數 --XX:MaxTenuringThreshold設置),這些對象就會成爲老年代。

但這也是不必定的,對於一些較大的對象(即須要分配一塊較大的連續內存空間)則是直接進入老年代

Full GC:

  Full GC是發生在老年代的垃圾收集動做,採用的是標記-清除/整理算法。

老年代裏的對象幾乎都是在Survivor區熬過來的,不會那麼容易死掉。所以Full GC發生的次數不會有Minor GC那麼頻繁,而且作一次Full GC要比作一次Minor GC的時間要長。

另外,若是採用的是標記-清除算法的話會產生許多碎片,此後若是須要爲較大的對象分配內存空間時,若沒法找到足夠的連續的內存空間,就會提早觸發一次GC。

 

二、老年代:

    在新生代中經歷了N次垃圾回收後仍然存活的對象就會被放到老年代中。並且大對象直接進入老年代。

 

三、永久代:

    即方法區。

 

8、類加載機制:

    虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

類加載的過程:

    包括加載、連接(含驗證、準備、解析)、初始化

以下圖所示:

一、加載:

  類加載指的是將類的class文件讀入內存,併爲之建立一個java.lang.Class對象,做爲方法區這個類的數據訪問的入口

也就是說,當程序中使用任何類時,系統都會爲之創建一個java.lang.Class對象。具體包括如下三個部分:

(1)經過類的全名產生對應類的二進制數據流。(根據early load原理,若是沒找到對應的類文件,只有在類實際使用時纔會拋出錯誤)

(2)分析並將這些二進制數據流轉換爲方法區方法區特定的數據結構

(3)建立對應類的java.lang.Class對象,做爲方法區的入口(有了對應的Class對象,並不意味着這個類已經完成了加載連接)

 

經過使用不一樣的類加載器,能夠從不一樣來源加載類的二進制數據,一般有以下幾種來源:

(1)從本地文件系統加載class文件,這是絕大部分程序的加載方式

(2)從jar包中加載class文件,這種方式也很常見,例如jdbc編程時用到的數據庫驅動類就是放在jar包中,jvm能夠從jar文件中直接加載該class文件

(3)經過網絡加載class文件

(4)把一個Java源文件動態編譯、並執行加載

 

二、連接:

    連接指的是將Java類的二進制文件合併到jvm的運行狀態之中的過程。在連接以前,這個類必須被成功加載。

類的連接包括驗證、準備、解析這三步。具體描述以下:

2.1  驗證:

    驗證是用來確保Java類的二進制表示在結構上是否徹底正確(如文件格式、語法語義等)。若是驗證過程出錯的話,會拋出java.lang.VertifyError錯誤。

主要驗證如下內容:

  • 文件格式驗證
  • 元數據驗證:語義驗證
  • 字節碼驗證

2.2  準備:

  準備過程則是建立Java類中的靜態域(static修飾的內容),並將這些域的值設置爲默認值,同時在方法區中分配內存空間。準備過程並不會執行代碼。

注意這裏是作默認初始化,不是作顯式初始化。例如:

public static int value = 12;

上面的代碼中,在準備階段,會給value的值設置爲0(默認初始化)。在後面的初始化階段纔會給value的值設置爲12(顯式初始化)。

2.3  解析:

  解析的過程就是確保這些被引用的類能被正確的找到(將符號引用替換爲直接引用)。解析的過程可能會致使其它的Java類被加載。

 

三、初始化:

  初始化階段是類加載過程的最後一步。到了初始化階段,才真正執行類中定義的Java程序代碼(或者說是字節碼)。

在如下幾種狀況中,會執行初始化過程:

(1)建立類的實例

(2)訪問類或接口的靜態變量(特例:若是是用static final修飾的常量,那就不會對類進行顯式初始化。static final 修改的變量則會作顯式初始化

(3)調用類的靜態方法

(4)反射(Class.forName(packagename.className))

(5)初始化類的子類。注:子類初始化問題:知足主動調用,即父類訪問子類中的靜態變量、方法,子類纔會初始化;不然僅父類初始化。

(6)java虛擬機啓動時被標明爲啓動類的類

代碼舉例1:

咱們對上面的第(5)種狀況作一個代碼舉例。

(1)Father.java:

複製代碼
1 public class Father {
2 
3     static {
4         System.out.println("*******father init");
5     }
6     public static int a = 1;
7 }
複製代碼

 

(2)Son.java:

1 public class Son extends Father {
2     static {
3         System.out.println("*******son init");
4     }
5     public static int b = 2;
6 }

 

(3)JavaTest.java:

1 public class JavaTest {
2     public static void main(String[] args) {
3         System.out.println(Son.a);
4     }
5 }

 

上面的測試類中,雖然用上了Son這個類,可是並無調用子類裏的成員,因此並不會對子類進行初始化。因而運行效果是:

b6de0ab4-1502-46dd-9193-578d1c83ca20

 

若是把JavaTest.java改爲下面這個樣子:

1 public class JavaTest {
2     public static void main(String[] args) {
3         System.out.println(Son.a);
4         System.out.println(Son.b);
5     }
6 }

 

運行效果:

b4757ac3-7476-49ff-84a4-659dca412324

 

 

若是把JavaTest.java改爲下面這個樣子:

JavaTest.java:

1 public class JavaTest {
2     public static void main(String[] args) {
3         System.out.println(Son.b);
4     }
5 }

 

運行效果:

e88cc820-e004-4764-9ab3-403d5d660a8a

 

 

代碼舉例2:

咱們對上面的第(2)種狀況作一個代碼舉例。即:若是是用static final修飾的常量,則不會進行顯式初始化。代碼舉例以下:

(1)Father.java:

1 public class Father {
2     static {
3         System.out.println("*******father init");
4     }
5     public static int a = 1;
6 }

 

(2)Son.java:

複製代碼
1 public class Son extends Father {
2     static {
3         System.out.println("*******son init");
4     }
5 
6     public static int b = 2;
7     public static final int c = 3;
8 }
複製代碼

 

這裏面的變量c是一個靜態常量。

(3)JavaTest.java:

1 public class JavaTest {
2     public static void main(String[] args) {
3         System.out.println(Son.c);
4     }
5 }

 

d04e2783-18db-41c4-958d-e9bbee011854

上面的運行效果顯示,因爲c是final static修飾的靜態常量,因此根本就沒有調用靜態代碼塊裏面的內容,也就是說,沒有對這個類進行顯式初始化

如今,保持Father.java的代碼不變。將Son.java代碼作以下修改:

複製代碼
1 public class Son extends Father {
2     static {
3         System.out.println("*******son init");
4     }
5 
6     public static int b = 2;
7     public static final int c = new Random().nextInt(3);
8 }
複製代碼

 

JavaTest.java:

1 public class JavaTest {
2     public static void main(String[] args) {
3         System.out.println(Son.c);
4     }
5 }

 

運行效果以下:

235bc0d9-0b08-436c-9b6d-380702f4a8c7

 

 

代碼舉例3:(很容易出錯)

咱們來下面這段代碼的運行結果是什麼:

複製代碼
 1 public class TestInstance {
 2 
 3     public static TestInstance instance = new TestInstance();
 4     public static int a;
 5     public static int b = 0;
 6 
 7     public TestInstance() {
 8         a++;
 9         b++;
10     }
11 
12     public static void main(String[] args) {
13         System.out.println(TestInstance.a);
14         System.out.println(TestInstance.b);
15     }
16 }
複製代碼

 

運行結果:

7281067e-8c73-4929-b23a-5b0992f2deff

之因此有這樣的運行結果,這裏涉及到類加載的順序:

(1)在加載階段,加載類的信息

(2)在連接的準備階段給instance、a、b作默認初始化並分配空間,此時a和b的值都爲0

(3)在初始化階段,執行構造方法,此時a和b的值都爲1

(4)在初始化階段,給靜態變量作顯式初始化,此時b的值爲0

 

咱們改一下代碼的執行順序,改爲下面這個樣子:

複製代碼
 1 public class TestInstance {
 2 
 3     public static int a;
 4     public static int b = 0;
 5     public static TestInstance instance = new TestInstance();
 6 
 7     public TestInstance() {
 8         a++;
 9         b++;
10     }
11 
12     public static void main(String[] args) {
13         System.out.println(TestInstance.a);
14         System.out.println(TestInstance.b);
15 
16     }
17 }
複製代碼

 

運行效果是:

e71a9e60-be2c-4072-9602-3fa591e0b940

之因此有這樣的運行結果,這裏涉及到類加載的順序:

(1)在加載階段,加載類的信息

(2)在連接的準備階段給instance、a、b作默認初始化並分配空間,此時a和b的值都爲0

(3)在初始化階段,給靜態變量作顯式初始化,此時b的值仍爲0

(4)在初始化階段,執行構造方法,此時a和b的值都爲1

 

注意,這裏涉及到另一個相似的知識點不要搞混了。知識點以下。

知識點:類的初始化過程(重要)

Student s = new Student();在內存中作了哪些事情?

  • 加載Student.class文件進內存
  • 棧內存爲s開闢空間
  • 堆內存爲學生對象開闢空間
  • 對學生對象的成員變量進行默認初始化
  • 對學生對象的成員變量進行顯示初始化
  • 經過構造方法對學生對象的成員變量賦值
  • 學生對象初始化完畢,把對象地址賦值給s變量
相關文章
相關標籤/搜索