jvm内存结构_聊聊JVM内存结构
起因
我們經常會在面試的時候被問到JVM的內存結構,很多人會覺得這東西真的有用嗎?也就是面試造火箭,入職擰螺絲。問這個就是純粹來刁難人的吧。
但實際上,我們細想一下。
?假設你不知道局部變量實際上屬于線程棧中的而非堆里面的,那么你就可能會擔心是不是我這樣寫會有并發問題;?假設你不知道對象實例是分配在堆中,屬于JVM共用的,那么你根本也就不會考慮在進行某些訪問時進行加鎖;?還有很多高并發的問題,比如volatile的原理,如果不知道JMM,怎么知道volatile的實現原理呢;... 這一系列的問題,如果你對JVM的內存結構不了解的話,都很難去回答這些問題。
內存結構是什么
JVM的內存結構是JVM底層內存管理的一個虛擬架構,你可以對比理解為操作系統中的內存模型,而對于JVM來說,JMM就相當于是它的內存管理模型。包括高并發等都依賴了它,并且還依賴后續的JMM(Java Memory Model)——JVM的內存模型。
內存結構長什么樣
既然內存結構這么重要,那么我們來看一下它是長啥樣的
圖片轉自https://www.chenxun.wiki/2017/01/16/thread-02/
我們看到里面有兩個東西,Stack(棧)和Heap(堆)。寫過程序的同學基本都知道Stack是啥東西,我們的每一個方法(函數)在執行的時候,都會有一個棧在維護它的執行順序,比如A方法調用B方法,那么棧的順序是這樣的:
棧滿足后入先出的原則,對于方法調用來說是非常合適的,比如前面的A調用B,那肯定B結束后要恢復A的執行上下文,所以B彈出棧,這個是很直觀的事情。
程序計數器——PC
這里的PC不是指的Personal Computer啊,當然不是啦。
程序計數器這個很好理解,它實際上就是記錄你當前的代碼執行行號,因為Java作為面向對象語言,實際上它的執行順序肯定不會是一路向下的,所以在執行過程中的跳轉就需要由PC去做控制。這個跟下面的JVM Stack和Native Stack是緊密結合在一起的。
虛擬機棧——JVM Stack
虛擬機的信息沒那么簡單,但實際上也差不多。每一個線程有自己的棧,而棧里面分成一個個的棧楨(Stack Frame,大家可以理解為我們上面的A方法和B方法),每個棧楨就相當于是當前線程執行的上下文,它包含了當前函數執行需要的信息,包括:局部變量,參數,返回值等。當一個棧楨(方法)執行完成后,它會從棧中被彈出。這里上面的PC就會把值更新為當前棧頂即將要執行的代碼行數,而增加一個棧楨(調用另外一個方法)也是類似的,會構建一個新的棧楨,并把當前的棧楨加入到棧頂,再執行正常的調用操作。
這其中,實際上我們會留意到,棧楨和棧楨之前多少都會有一些聯系,比如上一個棧楨的返回值是下一個棧楨的參數。類似這樣的,實際上棧楨在處理完成的時候也會把當前的一些返回值作為結果push到棧中,供下一個棧使用。
我們再關注一下我們的圖中,我們可以看到JVM Stack是線程隔離的,這就意味著實際上每個線程都有自己的棧,在這里面的我們并不需要考慮多線程問題。
但沒有多線程問題不代表沒有任何問題,Stack既然是棧,肯定也有棧的固有限制,JVM虛擬機會限制棧的大小(可以通過參數-Xss進行設置),當超過棧大小時會拋出StackOverflowError。
本地方法?!狽ative Method Stack
本地方法棧類似JVM Stack,區別最主要的就是JVM是Java內部調用的棧,而Native Method是調用的本地方法的,這種一般是通過JNI調用的,當然還有一些其他語言的,比如JPython等基于JVM的腳本語言。
方法區——Method Area
方法區是虛擬機中所有線程共用的一塊區域,它保存了被當前JVM加載的類信息、常量、靜態變量、編譯后產生的代碼等數據。當我們通過Class對象獲取類的相關信息時,都是通過這里去返回的。這里一般也被劃分作為堆的一部分,但實際上它并不屬于堆,有些資料把它稱為Non-Heap以作為區別。
常量池——Constant Pool
我們在上面方法區介紹的時候說過它會保存當前被JVM加載的常量,而我們這里說的常量池也是方法區的一塊。它保存了如字符串常量,final變量值,類名,方法名。常量池各細的可以分為兩種:
?Class常量池
這種實際上就是我們平常用的最多的,比如類名,方法名,final變量值等
?運行期常量池
運行期常量池從名字來看就是說在程序的運行期間是可以修改的。一般情況下,我們在運行時的所謂字符串字面量,就是比如String str = "helloworld"類似的定義,直接確認String值的定義。另外我們看的最多的就是String.intern(這個也是面試的一個常見點,比如String的對比啊,以后的文章再展開講),它也可以把字符串加入到常量池中。
堆——Heap
進入到我們最主要的重頭戲了。堆是我們JVM最麻煩的區域,麻煩就麻煩在這里是GC的最主要戰場——垃圾收集的最前線。在JVM中實例化的對象及數組(數組為什么要特殊拿出來呢?大家可以思考下)都是存儲在堆上的,這也就意味著它是所有線程共用的,并且是會有高并發的問題,因為共用一份,這也就導致了某個線程對對象進行修改的時候,其他線程有可能不能拿到最新的值——高并發的內容我們會后面涉及。
我們繼續來看看堆的邏輯結構
圖片轉自https://juejin.im/post/5dc0e88df265da4d461ebfd0
我們可以看到,堆分為兩個大塊:
新生代——Young Generation
從新生代的名字就可以看到,這是一些比較年輕的對象存放的位置,年輕是指對象從創建到當前的時間都比較短,當然,再年輕過了一段時間也會變老,就會晉升到后面的老年代。這里涉及到了一些GC的算法問題,我們后面再學習。暫時先有一個概念即可。新生代又區分為幾個:
?Eden區 新創建的對象都會先在該區(前提是能放得下)?Survivor0區 Eden區的存活對象會遷移到該?Survivor1區 和Survivor1區互為備份,兩個間的對象會互相遷移,最后會以對象的存活次數決定是否晉升到老年代。
老年代——Old Generation
從名字也可以看出,老年代當然是比較老的對象呆的地方了。但多老才算老呢,這個會由前面的新生代晉升的存活次數來決定。
永久代——PermGen Space
其實嚴格來說,永久代并不屬于堆里面的,但由于它也叫代,那么為了好看,我們也在這里一起描述。雖然說叫永久代,但實際上它是屬于方法區的。在JSP時代,我們經常會遇到一個錯誤叫java.lang.OutOfMemoryError: PermGen space,這一般是由于我們加載的文件太多,導致方法區超出限制了。而在JDK 8中,HotSpot已經把永久代改為元空間(MetaSpace)。我們可以看一下下面這個小例子,看看怎么用MetaSpace的相關參數。
public class MetaSpaceTest { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); IntStream.range(0, Integer.MAX_VALUE).forEach(idx -> { sb.append("str" + idx); }); }}我們這里設置最大的MetaSpace為:-XX:MaxMetaspaceSize=128m?這里不斷構造新的String,運行幾秒后我們會看到這樣的錯誤:
我們可以看到現在已經是Heap size的錯誤了。這實際上意味著方法區已經被修改為元空間了。一般情況下它受限于當前機器的物理內存。如果說它為了解決什么問題,最直接還是之前的OOM錯誤吧,修改后就可以減少很多OOM的錯誤。
JMM是什么
大家一看到JMM,第一眼想到的是啥——這東西該不會又是啥網絡用語簡稱吧。難道是姐妹們,加美眉,加貓貓類似的,我只能說你想多了。
那么我們就來開謎了,JMM確實是簡稱,但是Java Memory Model——Java內存模型的簡稱。
JMM長啥樣
我們直接來看看圖
轉自https://www.cnblogs.com/kukri/p/9109639.html
這跟我們上面的內存結構看著是不是好像有點關聯的樣子。我們可以看到每個線程都有自己的工作內存,雖然工作內存也是屬于線程的,但它跟我們上面的內存結構關系不大,它們只是主內存映射到特定線程的一個虛擬概念罷了。當然,如果為了幫助理解,我們可以這樣理解。工作內存實際上也是屬于JVM Stack的,只是它獨立于JVM Stack,不歸屬于內存結構。
我們可以看到圖中,工作內存和主內存的操作是通過JMM來控制的。那具體是怎么控制的呢?
我們先把問題放下,先理一下內存操作有哪些。讀、寫,還有呢?沒了,就這兩個。那么JMM實際上要解決的就是讀和寫的問題。這里又涉及到CPU的MESI協議,這個我們大概了解一下先,后面我們講到volatile和高并發相關的知識的時候我們再細說。
?Modify——修改
CPU可以發送標志告訴其他CPU說這個變量我已經修改了,你們不要再用自己的值了,來主內存中拿。
?Exclusive——獨占
這種標記情況下表明當前工作內存中的值是獨占的,其他的CPU并沒有相應的緩存值。
?Shared——共享
表明當前的值各CPU上都有
?Invalid——失效表明當前CPU中的值已經不能再使用了,需要從主存加載。
為什么需要JMM
我們可以看到,由于每個線程都會有自己的工作內存——即緩存。對于共享變量來說,某一個CPU修改了,其他CPU不一定能及時發現,或者更極端的是一直發現不了。所以我們需要有一個協調者幫我們去做這樣的事情。
共享變量如果被其中一個CPU改了,那么要通知我,我就不用自己的了,我去主內存中重新加載放到工作內存中。當然JMM的作用不止于此,它還有一些約束讀寫,禁止重排序等功能。這些我們留待后面的文章再細說。
回到前面的問題
說了這么多,那么我們前面的問題的答案是啥呢?
訪問局部變量要不要加鎖呢?
回顧我們前面說的堆和棧的情況。如果你說局部變量是保存在JVM棧中的,而JVM棧中是線程隔離的,所以可以不加鎖。那么,你肯定是沒有仔細看堆那一塊的相關知識。變量包含兩種,一種是基礎變量,一種是引用變量,對于基礎變量,它直接分配在棧上,這是線程隔離的,所以如果是基礎變量,隨便來,不需要加鎖;而對于引用變量,我們知道,只有這個變量是在棧上的,而它真正的對象是在堆上的,而堆上的則是需要進行加鎖的。我們來看一個例子:
public class ConcurrentTest { public static void main(String[] args) { int id = 11; PrimitiveClass primitiveClass = new PrimitiveClass(id); PrimitiveClass primitiveClass2 = new PrimitiveClass(id); primitiveClass.print(); primitiveClass2.print(); ComplexClass complexClass = new ComplexClass(primitiveClass); ComplexClass complexClass2 = new ComplexClass(primitiveClass); complexClass.print(); complexClass2.print(); } static class PrimitiveClass { private long id; public PrimitiveClass(long id) { this.id = id; } public void print() { System.out.println("here is id:" + id); } } static class ComplexClass { private PrimitiveClass primitiveClass; public ComplexClass(PrimitiveClass primitiveClass) { this.primitiveClass = primitiveClass; } public void print() { System.out.println("here is shared:" + primitiveClass); } }}我們來看看結果:我們看到PrimitiveClass實際上看不出任何區別,因為它是基本類型,傳入的值實際上就是該值本身,并沒有太多需要特殊處理的地方。而ComplexClass就比較特殊了,它里面有一個引用類型,而且我們看到引用的值實際上是同一個,所以就意味著,我們對當前的類做的改動,都會直接體現到該對象身上。
我們來看看內存示意圖:
雖然我們沒用多個線程,但我們new了多次,可以間接理解為我們是用多個線程的,意會一下。我們可以看到,對于對象,我們都是指向堆中的實例值,而對于基本類型,它們卻是保存在棧中的。這也就解釋了我們上面的,局部變量什么時候需要加鎖,又什么時候不需要加鎖。
對象實例分配在堆中,那是不是都要加鎖呢?
其實這個答案我們在上面分析的時候也順便回答了。如果你的對象是一個可變對象,什么叫可變對象呢,就是內有可變屬性。這個可變狀態這里不細說哈,有興趣的同學可以找找其他文章了解一下。當你的對象擁有可變屬性,那么當多線程訪問該對象時,就需要進行加鎖。現在流行的函數式編程在高并發下有原生的優勢就是因為它去掉了可變屬性,所有結果都只是一個中間變量,用完即丟。
那么volatile的實現原理呢?
回到我們上面的對JMM的分析上,MESI實際上就是volatile實現的基礎。當然還有一些更細的一些東西,比如happens-before原則,禁止重排序等,這些就留到我們后面再來講。
總結
今天我們一起了解了JVM內存結構和JMM,知道了內存結構中分成哪幾部分,并且哪些是線程獨占,哪些又是共用的;而JMM我們就了解了它出現的原因和基本的MESI協議。當然,很多東西要細講的話可以講很多,我們暫時先這樣,留待后面繼續。
參考文章:http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
總結
以上是生活随笔為你收集整理的jvm内存结构_聊聊JVM内存结构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 感知机数据算法的对偶形式
- 下一篇: bp