天天看點

熬了一通宵!你竟然都沒有弄懂陌陌面試官問的Java虛拟機記憶體?

前言

什麼叫運作時資料區呢,看下圖就知道了,今天的重點就圍繞這張圖講。

一、運作時資料區

熬了一通宵!你竟然都沒有弄懂陌陌面試官問的Java虛拟機記憶體?

1、程式計數器(寄存器)

目前線程所執行的位元組碼行号訓示器

位元組碼解釋器工作依賴計數器控制完成

通過執行線程行号記錄,讓線程輪流切換各條線程之間計數器互不影響

線程私有,生命周期與線程相同,随JVM啟動而生,JVM關閉而死

Java學習筆記共享位址:JVM虛拟機面試題答案解析

線程執行Java方法時,記錄其正在執行的虛拟機位元組碼指令位址

線程執行Nativan方法時,計數器記錄為空(Undefined)

唯一在Java虛拟機規範中沒有規定任何OutOfMemoryError情況區域

在這其中,很多不了解的沒關系,我們學過多線程,有兩個線程,其中一個線程可以暫停使用,讓其他線程運作,然後等自己獲得cpu資源時,又能從暫停的地方開始運作,那麼為什麼能夠記住暫停的位置的,這就依靠了程式計數器, 通過這個例子,大概了解一下程式計數器的功能。

2、本地方法棧

不知道大家看過源碼沒有,看過的都應該知道,很多的算法或者一個功能的實作,都被java封裝到了本地方法中,程式直接通過調用本地的方法就行了,本地方法棧就是用來存放這種方法的,實作該功能的代碼可能是C也可能是C++,反正不一定就是java實作的。

上面兩個不是我們所要學習的重點,接下來三個才是重點。

3、虛拟機棧

這個大家都應該有所了解,現在來細講它,虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用來存放存儲局部變量表、操作數表、動态連接配接、方法出口等資訊,每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中從入棧到出棧的過程。 這個話怎麼了解呢?比如執行一個類(類中有main方法)時,執行到main方法,就會把為main方法建立一個棧幀,然後在加到虛拟機棧中,棧幀中會存放這main方法中的各種局部變量,對象引用等東西。如圖

熬了一通宵!你竟然都沒有弄懂陌陌面試官問的Java虛拟機記憶體?

當在main方法中調用别的方法時,就會有另一個方法的棧幀入虛拟機棧,當該方法調用完了之後,彈棧,然後main方法處于棧頂,就繼續執行,直到結束,然後main方法棧幀也彈棧,程式就結束了。總之虛拟機棧中就是有很多個棧幀的入棧出棧,棧幀中存放的都市一些變量名等東西,是以我們平常說棧中存放的是一些局部變量,因為局部變量就是在方法中。也就是在棧幀中,就是這樣說過來的。

以上說的三個都是線程不共享的,也就是這部分記憶體,每個線程獨有,不會讓别的線程通路到,接下來的兩個就是線程共享了,也就會出現線程安全問題。

4、堆

所有線程共享的一塊記憶體區域。Java虛拟機所管理的記憶體中最大的一塊,因為該記憶體區域的唯一目的就是存放對象執行個體。幾乎所有的對象執行個體度在這裡配置設定記憶體,也就是通常我們說的new對象,該對象就會在堆中開辟一塊記憶體來存放對象中的一些資訊,比如屬性呀什麼的。同時堆也是垃圾收集器管理的主要區域。是以很多時候被稱為"GC堆",虛拟機的垃圾回收機制等下一篇文章來講解。 在上一點講的棧中存放的局部引用變量所指向的大多數度會在堆中存放。

5、方法區和其中的運作時常量池

和堆一樣,是各個線程共享的記憶體區域,用于存儲已被虛拟機加載的類資訊、常量、靜态變量、和編譯器編譯後的代碼(也就是存儲位元組碼檔案。.class)等資料,這裡可以看到常量也會在方法區中,是因為方法區中有一個運作時常量池,為什麼叫運作時常量池,因為在編譯後期生成的是各種字面量(字面量的意思就是值,比如int i=3,這個3就是字面量的意思)和符号引用,這些是存放在一個叫做常量池(這個常量池是在位元組碼檔案中)的地方,當類加載進入方法區時,就會把該常量池中的内容放入運作時常量池中。這裡要注意,運作時常量池和常量池,不要搞混淆了,位元組碼檔案中也有常量池,在後面的章節會詳細講解這個東西。現在隻需要知道方法區中有一個運作時常量池,就是用來存放常量的。還有一點,運作時常量池不一定就一定要從位元組碼常量池中拿取常量,可能在程式運作期間将新的常量放入池中,比如String.intern()方法,這個方法的作用就是:先從方法區的運作時常量池中查找看是否有該值,如果有,則傳回該值的引用,如果沒有,那麼就會将該值加入運作時常量池中。

二、練習。畫記憶體圖。

平常分析中用到的最多還是堆、虛拟機棧和方法區。

例如:看下面這段程式,然後畫出記憶體分析圖

熬了一通宵!你竟然都沒有弄懂陌陌面試官問的Java虛拟機記憶體?

最主要是看我的分析過程,這個圖由于要顯示出動态彈棧畫不了,是以隻能夠那樣畫一下了。

1、首先運作程式,Demo1_car.java就會變為Demo1_car.class,将Demo1_car.class加入方法區,檢查是否位元組碼檔案常量池中是否有常量值,如果有,那麼就加入運作時常量池

2、遇到main方法,建立一個棧幀,入虛拟機棧,然後開始運作main方法中的程式

3、Car c1 = new Car(); 第一次遇到Car這個類,是以将Car.java編譯為Car.class檔案,然後加入方法區,跟第一步一樣。然後new Car()。就在堆中建立一塊區域,用于存放建立出來的執行個體對象,位址為0X001.其中有兩個屬性值 color和num。預設值是null 和 0

4、然後通過c1這個引用變量去設定color和num的值,

5、調用run方法,然後會建立一個棧幀,用來裝run方法中的局部變量的,入虛拟機棧,run方法中就列印了一句話,結束之後,該棧幀出虛拟機棧。又隻剩下main方法這個棧幀了

6、接着又建立了一個Car對象,是以又在堆中開辟了一塊記憶體,之後就是跟之前的步驟一樣了。

這樣就分析結束了,在腦袋中就應該有一個大概的認識對堆、虛拟機棧、和方法區。注意這個方法區的名字,并不是就單單裝方法的,能裝很多東西。

這個隻是一個簡單的分析,可以再講具體一點,1、建立對象,在堆中開辟記憶體時是如何配置設定記憶體的?2、對象引用是如何找到我們在堆中的對象執行個體的?通過這兩個問題來加深我們的了解。

1、建立對象,在堆中開辟記憶體時是如何配置設定記憶體的?

兩種方式:指針碰撞和空閑清單。我們具體使用的哪一種,就要看我們虛拟機中使用的是什麼了。

指針碰撞:假設Java堆中記憶體是絕對規整的,所有用過的記憶體度放一邊,空閑的記憶體放另一邊,中間放着一個指針作為分界點的訓示器,所配置設定記憶體就僅僅是把哪個指針向空閑空間那邊挪動一段與對象大小相等的舉例,這種配置設定方案就叫指針碰撞

空閑清單:有一個清單,其中記錄中哪些記憶體塊有用,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,然後更新清單中的記錄。這就叫做空閑清單

2、對象引用是如何找到我們在堆中的對象執行個體的?

這個問題也可以稱為對象的通路定位問題,也有兩種方式。句柄通路和直接指針通路。 畫兩張圖就明白了。

句柄通路:Java堆中會劃分出一塊記憶體來作為句柄池,引用變量中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料和類型資料各自的具體位址資訊

熬了一通宵!你竟然都沒有弄懂陌陌面試官問的Java虛拟機記憶體?

解釋圖:在棧中有一個引用變量指向句柄池中一個句柄的位址,這個句柄又包含了兩個位址,一個對象執行個體資料,一個是對象類型資料(這個在方法區中,因為類位元組碼檔案就放在方法區中),

直接指針通路:引用變量中存儲的就直接是對象位址了,如圖所示

熬了一通宵!你竟然都沒有弄懂陌陌面試官問的Java虛拟機記憶體?