12.24 JAVA虛擬機JVM的簡單認識

以前面試的時候經常會被問到Java虛擬機的問題,面試官會問到:

------------你知道虛擬機嗎?

------------虛擬機內存機構是什麼樣的?

------------棧區和堆區的區別你知道嗎?

------------虛擬機的類加載機制你知道嗎?

------------虛擬機的雙親委派機制你知道嗎?

------------java虛擬機的垃圾回收你知道嗎?

------------JVM的優化你知道嗎?內存優化你知道嗎?

等等。。。。。。。。。。不知道!。。。。好,那就先這樣吧,我們會過兩天通知你面試結果,先回家等著吧。就這樣等到了天荒地老,也沒有等到面試的結果和通知。

如果你看完這篇文章還不瞭解虛擬機,面試的時候還被虛擬機的問題問住,我只能說,你沒有認真看。

以上內容純屬廢話,請看下面:

--------------------------------------------------------------------------------------------

一、java虛擬機的認知

1、JVM是Java Virtual Machine(Java虛擬機)的縮寫。

虛擬機,字面理解就是虛擬的計算機。計算機可以安裝操作系統吧,所以這個虛擬的計算機裡面亦可以安裝操作系統,比如Windows、Linux。就可理解成,虛擬機就是一款軟件,這個軟件裡面可以安裝操作系統,然後安裝各種軟件,安裝JDK、安裝Tomcat、MySQL。

2、虛擬機是不是很吊。那我可以在不同的電腦上安裝虛擬機,然後在安裝好的虛擬機裡面裝上自己想要的操作系統,在安裝一些軟件,這就是相當於你在自己的電腦裡面模擬了一臺計算機。所以根據這些特性,我們的java程序就可以一次編寫,到處運行了。以前我們經常習慣把虛擬機叫做java虛擬機,殊不知,現在很多編程語言的程序都開始用使用虛擬機來運行,這樣,我的程序就可以在不同操作系統上跑起來,所以發展到現在,虛擬機已經不能再叫java虛擬機了。

二、虛擬機的結構

一張圖帶你認識虛擬機,這是JDK1.7規範的虛擬機內存圖

JAVA虛擬機JVM的簡單認識

我這裡有一張完善一點的圖,看起來比上面這個好理解

JAVA虛擬機JVM的簡單認識

簡單的介紹下這個圖裡面的東西:

1、類加載器:

------ java文件經過javac編譯成class文件,在JVM啟動時或者在類運行時將需要的class加載到JVM中。

類加載的順序:

JAVA虛擬機JVM的簡單認識

總結:啟動jvm調用loadClass(類加載器)。classloader是加載類的入口,此方法負責加載指定名字的類,ClassLoader的實現方法為先從已經加載的類中尋找,如沒有則繼續從父ClassLoader中尋找,如仍然沒找到,則從BootstrapClassLoader中尋找(BootStrapClassLoader。它是最頂層的類加載器,是由C++編寫而成, 已經內嵌到JVM中了。在JVM啟動時會初始化該ClassLoader,它主要用來讀取Java的核心類庫JRE/lib/rt.jar中所有的class文件,這個jar文件中包含了java規範定義的所有接口及實現)。這個過程如下圖可以便於理解:

JAVA虛擬機JVM的簡單認識

幾種類加載器:

ExtensionClassLoader:它是用來讀取Java的一些擴展類庫,如讀取JRE/lib/ext/.jar中的包等

AppClassLoader:它是用來讀取classpath下指定的所有jar包或目錄的類文件,一般情況下這個就是程序中默認的類加載器

CustomClassLoader:它是用戶自定義編寫的,它用來讀取指定類文件 。基於自定義的ClassLoader可用於加載非Classpath中(如從網絡上下載的jar或二進制)的jar及目錄、還可以在加載前對class文件優一些動作,如解密、編碼等。

*–根據類的加載機制,延伸出了大家常說的雙親委派機制:

雙親委派機制:

某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,(每個ClassLoader實例都有一個父類加載器的引用【不是繼承的關係,是一個包含的關係】)依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。(意思就是說老爹幹不了的事情,才會自己去幹,我們叫他雙親委派,太不孝了)。

所以我們這裡會有一個疑問,為什麼要是使用雙親委派機制,為什麼類進來不直接加載,搞這麼麻煩幹什麼?

為什麼使用雙親委派機制:

java中存在3種類型的類加載器:引導類加載器,擴展類加載器和系統類加載器。三者是的關係是:引導類加載器是擴展類加載器的父類,擴展類加載器是系統類加載器的父類。----->

引導類加載器(BootStrap):

主要負責加載jvm自身所需要的類,該加載器由C++實現,加載的是<java>/lib下的class文件,或-Xbootclasspath參數指定的路徑下的jar包加載到內存中,注意必由於虛擬機是按照文件名識別加載jar包的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出於安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類)---->/<java>

拓展類加載器(Extension):

擴展類加載器是指Sun公司(已被Oracle收購)實現的sun.misc.Launcher$ExtClassLoader類,由Java語言實現的,是Launcher的靜態內部類,它負責加載<java>/lib/ext目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者可以直接使用標準擴展類加載器。/<java>

系統類加載器:

也稱應用程序加載器是指 Sun公司實現的sun.misc.Launcher$AppClassLoader。它負責加載系統類路徑java -classpath或-D java.class.path指定路徑下的類庫,也就是我們經常用到的classpath路徑,開發者可以直接使用系統類加載器,一般情況下該類加載是程序中默認的類加載器,通過ClassLoade.getSystemClassLoader()方法可以獲取到該類加載器。

所以虛擬機是如何確保兩個對象是屬於同一個類呢:

首先確定這倆對象是由同名的類完成實例化的。然後在確實是由同一個類加載器加載的。如果兩個類名相同,一個是由系統類加載器加載,一個是由擴展類加載器加載的,那他們的對象會被識別成兩個不同對象。

所以如果我們自定義一個Object類可以嗎?答案是不可以的,為了不讓我們寫System類,類加載採用委託機制,這樣可以保證爸爸們優先,爸爸們能找到的類,兒子就沒有機會加載。而System類是Bootstrap加載器加載的,就算自己重寫,也總是使用Java系統提供的System,自己寫的System類根本沒有機會得到加載。

但是,我們可以自己定義一個類加載器來達到這個目的,為了避免雙親委託機制,這個類加載器也必須是特殊的。由於系統自帶的三個類加載器都加載特定目錄下的類,如果我們自己的類加載器加載一個特殊的目錄,那麼系統的加載器就無法加載,也就是最終還是由我們自己的加載器加載。

(看到這裡,如果不從頭開始理解內存圖,虛擬機是什麼,可能都已經懵逼了。這裡我用一句話總結為什麼使用雙親委派機制:

*-

如類java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都會委派給啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果用戶自己寫了一個名為java.lang.Object的類,並放在程序的Classpath中,那系統中將會出現多個不同的Object類,java類型體系中最基礎的行為也無法保證,應用程序也會變得一片混亂。)

大家都知道,我們java中的中級父類,Object,java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都會委派給啟動類加載器進行加載,

2、運行時數據區:

------ 就是咱們常說的內存,只有程序運行的時候才會加載到內存。

3、棧區(jvm stack)

走進運行時數據區,首先映入眼簾的是java虛擬機棧區

JVM棧是線程私有的,每個線程創建的同時都會創建JVM棧,棧中存的是基本數據類型和堆中對象的引用(java中定義的八種基本類 型:boolean、char、byte、short、int、long、float、double),由於JVM棧是線程私有的,因此其在內存分配上非常高效,並且當線程運行完畢後,這些內存也就被自動回收。所以這裡時自動回收,咱們常說的垃圾回收是不會發生在這裡的。

4、堆區Heap(java堆)

是大家最為熟悉的區域,它是JVM用來存儲對象實例以及數組值的區域,可以認為Java中所有通過new創建的對象的內存都在此分配,Heap中 的對象的內存需要等待GC進行回收,Heap在32位的操作系統上最大為2G,在64位的操作系統上則沒有限制,其大小通過-Xms和-Xmx來控制。

*---------------------------------------------------------------------------------為了便於理解,這裡再將虛擬機的結構圖粘貼一便,看圖說事兒:

JAVA虛擬機JVM的簡單認識

5、方法區(Method Area):

用於存儲類結構信息的地方(就是一個類裡面有的東西都會放在這裡),包括常量池、靜態變量、構造函數等。雖然JVM規範把方法區描述為堆的一個邏輯部分, 但它卻有個別名non-heap(非堆),所以大家不要搞混淆了。方法區還包含一個運行時常量池(就是放常量的池子,final,你懂的)。

6、程序計數器(PC Register):

用於保存當前線程執行的內存地址。由於JVM程序是多線程執行的(線程輪流切換),所以為了保證線程切換回來後,還能恢復到原先狀態,就需要一個獨立的計數器,記錄之前中斷的地方,可見程序計數器也是線程私有的。

(舉個例子把,一個單核的CPU他運行軟件的時候是時間片切換執行的把,你看電影,聽音樂,看似是一遍放電影,一遍放音樂,其實CPU在執行的時候是放完電影在馬上切換音樂,然後在切換到電影,輪流執行的,這其中的時間片非常短,短到你感知不到他們是輪流執行的,而是感覺是同時執行的,所以我在看電影和聽音樂輪流切換執行的時候是怎麼才能找到之前那個放電影的那個點,然後在切換到聽音樂的那個點呢?程序計數器就是幹這個的,能確保我能找到切換的時候這個點在哪兒,你懂的。)

7、本地方法棧(Native Method Stack):

和java棧的作用差不多,只不過是為JVM使用到的(本地)native方法服務的。(本地方法棧)

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

二、虛擬機的內存分配:

首先:Java虛擬機是先一次性分配一塊較大的空間,然後每次new時都在該空間上進行分配和釋放,減少了系統調用的次數,節省了一定的開銷,這有點類似於內存池的概念;

java一般內存申請有兩種:靜態內存和動態內存

靜態內存:編譯時就能夠確定的內存就是靜態內存,即內存是固定的,系統一次性分配。

動態內存:動態內存分配就是在程序執行時才知道要分配的存儲空間大小,比如java對象的內存空間。根據上面我們知道,java棧、程序計數器、本地方法棧都是線程私有的,線程生就生,線程滅就滅,棧中的棧幀隨著方法的結束也會撤銷,內存自然就 跟著回收了。所以這幾個區域的內存分配與回收是確定的,我們不需要管的。但是java堆和方法區則不一樣,我們只有在程序運行期間才知道會創建哪些對象, 所以這部分內存的分配和回收都是動態的。一般我們所說的垃圾回收也是針對的這一部分。

總之(棧區)Stack的內存管理是順序分配的,而且定長,不存在內存回收問題;而Heap 則是為java對象的實例隨機分配內存,不定長度,所以存在內存分配和回收的問題;

三、垃圾檢測:

垃圾收集器一般必須完成兩件事:檢測出垃圾;回收垃圾

引用計數法:給一個對象添加引用計數器,每當有個地方引用它,計數器就加1;引用失效就減1。

好了,問題來了,如果我有兩個對象A和B,互相引用,除此之外,沒有其他任何對象引用它們,實際上這兩個對象已經無法訪問,即是我們說的垃圾對象。但是互相引用,計數不為0,導致無法回收,所以還有另一種方法:

可達性分析算法:以根集對象為起始點進行搜索,如果有對象不可達的話,即是垃圾對象。這裡的根集一般包括java棧中引用的對象、方法區常良池中引用的對象

四、垃圾回收:

1.標記-清除(Mark-sweep)

算法和名字一樣,分為兩個階段:標記和清除。標記所有需要回收的對象,然後統一回收。這是最基礎的算法,後續的收集算法都是基於這個算法擴展的。

不足:效率低;標記清除之後會產生大量碎片。

2.複製(Copying)

此算法把內存空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另外一個區域中。此算法每 次只處理正在使用中的對象,因此複製成本比較小,同時複製過去以後還能進行相應的內存整理,不會出現“碎片”問題。當然,此算法的缺點也是很明顯的,就是 需要兩倍內存空間。

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

此算法結合了“標記-清除”和“複製”兩個算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,把清除未標記 對象並且把存活對象“壓縮”到堆的其中一塊,按順序排放。此算法避免了“標記-清除”的碎片問題,同時也避免了“複製”算法的空間問題

4.分代收集算法

這是當前商業虛擬機常用的垃圾收集算法。分代的垃圾回收策略,是基於這樣一個事實:不同的對象的生命週期是不一樣的。因此,不同生命週期的對象可以採取不同的收集方式,以便提高回收效率。

(1)對新生代的對象的收集稱為minor GC;

(2)對舊生代的對象的收集稱為Full GC;

(3)程序中主動調用System.gc()強制執行的GC為Full GC。

為什麼要運用分代垃圾回收策略?在java程序運行的過程中,會產生大量的對象,因每個對象所能承擔的職責不同所具有 的功能不同所以也有著不一樣的生命週期,有的對象生命週期較長,比如Http請求中的Session對象,線程,Socket連接等;有的對象生命週期較 短,比如String對象,由於其不變類的特性,有的在使用一次後即可回收。試想,在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行 回收,那麼消耗的時間相對會很長,而且對於存活時間較長的對象進行的掃描工作等都是徒勞。因此就需要引入分治的思想,所謂分治的思想就是因地制宜,將對象 進行代的劃分,把不同生命週期的對象放在不同的代上使用不同的垃圾回收方式。

如何劃分?將對象按其生命週期的不同劃分成:年輕代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。其中持久代主要存放的是類信息,所以與java對象的回收關係不大,與回收息息相關的是年輕代和年老代。

“假設你是一個普通的 Java 對象,你出生在 Eden 區,在 Eden 區有許多和你差不多的小兄弟、小姐妹,可以把 Eden 區當成幼兒園,在這個幼兒園裡大家玩了很長時間。Eden 區不能無休止地放你們在裡面,所以當年紀稍大,你就要被送到學校去上學,這裡假設從小學到高中都稱為 Survivor(倖存[sə’vaɪvə]) 區。開始的時候你在 Survivor 區裡面劃分出來的的“From”區,讀到高年級了,就進了 Survivor 區的“To”區,中間由於學習成績不穩定,還經常來回折騰。直到你 18 歲的時候,高中畢業了,該去社會上闖闖了。於是你就去了年老代,年老代裡面人也很多。在年老代裡,你生活了 20 年 (每次 GC 加一歲),最後壽終正寢,被 GC 回收。有一點沒有提,你在年老代遇到了一個同學,他的名字叫愛德華 (慕光之城裡的帥哥吸血鬼),他以及他的家族永遠不會死,那麼他們就生活在永生代。”

對象怎樣有新生代轉到年老代

持久代:

用於存放靜態文件,如今java類、開發方法 等

五、JVM調優總結:

Jvm調優的重點是垃圾回收(gc,garbage collection)和內存管理。垃圾回收的時候會導致

整個虛擬機暫停服務。因此,應該儘可能地縮短垃圾回收的處理時間。

在JVM啟動參數中,可以設置跟內存、垃圾回收相關的一些參數設置,讓jvm獲得最佳性能.

1、開啟-server模式,(啟動雖然慢,但是運行效率高)

2、針對JVM堆的設置一般,可以通過-Xms -Xmx限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而產生額外的時間,我們通常把最大、最小設置為相同的值

3、年輕代和年老代將根據默認的比例(1:2)分配堆內存

年輕代和年老代設置多大才算合理?這個我問題毫無疑問是沒有答案的,否則也就不會有調優。(原則是是減少GC的頻率和Full GC的次數

4、在配置較好的機器上(比如多核、大內存),可以為年老代選擇並行收集算法: -XX:+UseParallelOldGC ,默認為Serial收集

5、線程堆棧的設置:每個線程默認會開啟1M的堆棧,用於存放棧幀、調用參數、局部變量等,對大多數應用而言這個默認值太了,一般256K就足用。理論上,在內存不變的情況下,減少每個線程的堆棧,可以產生更多的線程,但這實際上還受限於操作系統。

六、JVM 底層面試題及答案

1)你能保證 GC 執行嗎?(答案)

不能,雖然你可以調用 System.gc() 或者 Runtime.gc(),但是沒有辦法保證 GC 的執行。

2)怎麼獲取 Java 程序使用的內存?堆使用的百分比?

可以通過 java.lang.Runtime 類中與內存相關方法來獲取剩餘的內存,總內存及最大堆內存。通過這些方法你也可以獲取到堆使用的百分比及堆內存的剩餘空間。 Runtime.freeMemory() 方法返回剩餘空間的字節數,Runtime.totalMemory() 方法總內存的字節數,Runtime.maxMemory() 返回最大內存的字節數。

3)Java 中堆和棧有什麼區別?(答案)

JVM 中堆和棧屬於不同的內存區域,使用目的也不同。棧常用於保存方法幀和局部變量,而對象總是在堆上分配。棧通常都比堆小,也不會在多個線程之間共享,而堆被整個 JVM 的所有線程共享。

4)棧區存放的是什麼數據?

基本數據類型和對堆中對象的引用。

5)棧區存放基本數據類型,那麼基本數據類型有哪幾種?

Java基本類型共有八種,基本類型可以分為四類,字符類型char,布爾類型boolean以及數值類型byte、short、int、long、float、double。數值類型又可以分為整數類型byte、short、int、long和浮點數類型float、double。JAVA中的數值類型不存在無符號的,它們的取值範圍是固定的,不會隨著機器硬件環境或者操作系統的改變而改變。實際上,JAVA中還存在另外一種基本類型void,它也有對應的包裝類 java.lang.Void,不過我們無法直接對它們進行操作。8 中類型表示範圍如下:

byte:8位,最大存儲數據量是255,存放的數據範圍是-128~127之間。

short:16位,最大數據存儲量是65536,數據範圍是-32768~32767之間。

int:32位,最大數據存儲容量是2的32次方減1,數據範圍是負的2的31次方到正的2的31次方減1。

long:64位,最大數據存儲容量是2的64次方減1,數據範圍為負的2的63次方到正的2的63次方減1。

float:32位,數據範圍在3.4e-45~1.4e38,直接賦值時必須在數字後加上f或F。

double:64位,數據範圍在4.9e-324~1.8e308,賦值時可以加d或D也可以不加。

boolean:只有true和false兩個取值。

char:16位,存儲Unicode碼,用單引號賦值。

Java決定了每種簡單類型的大小。這些大小並不隨著機器結構的變化而變化。這種大小的不可更改正是Java程序具有很強移植能力的原因之一。下表列出了Java中定義的簡單類型、佔用二進制位數及對應的封裝器類。

6)棧區存基本數據類型,基本數據類型他是有取值範圍,如果超出了這個範圍,會變成什麼?

會變成包裝類。(搞清楚包裝類和基本數據類型的區別,能延伸很多問題)

JVM常用調試工具:

jconCole – jconsole是基於JavaManagementExtensions (JMX)的實時圖形化監測工具,這個工具利用了內建到JVM裡面的JMX指令來提供實時的性能和資源的監控,包括了Java程序的內存使用,Heap size, 線程的狀態,類的分配狀態和空間使用等等

擴展

在JVM虛擬中還有一個內存既不是虛擬機運行時數據區的一部分,也不是虛擬機規範中定義餓內存區域,但是使用頻繁,也會產生OOM異常。在引入NIO時為了避免Java堆和Native堆中來回複製數據,從而直接分配的堆外內存。本機直接內存不會受到Java堆大小的限制,受本機總內存影響。在服務器管理員在配置虛擬機參數時,會根據實際配置-Xmx等參數信息,但經常忽略直接內存,會出現各個內存區域總和大於物理內存限制,從而在動態擴容時出現OOM的情況。


分享到:


相關文章: