JMM對於一個想要深刻了解java的程序猿來講是不可避免的一關,本文偏理論性,儘量說的通俗易懂,若有不對的地方但願多多指正。java
那咱們先說一下jvm的主內存分配c++
1 java虛擬機棧(java virtual stack)算法
虛擬機棧是線程私有的,每一個線程都有一個本身的虛擬機棧,是java方法執行的內存模型,每一個方法執行的時候都會在虛擬機棧上建立一個棧幀,棧幀是一個數據結構,主要存儲的是方法中的局部變量(基本類型,對象的引用,returnAddress類型(指向一條字節碼指令的地址)),操做棧(指的就是方法編譯後的操做指令的棧),動態連接,方法出口。一般所說的java內存分爲棧和堆,其中所說的棧就是指的虛擬機棧。但java的內存分配並無這麼簡單。spring
動態連接解釋以下:編程
每一個棧幀都包含一個執行運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接(Dynamic Linking)。數組
Class 文件中存放了大量的符號引用,字節碼中的方法調用指令就是以常量池中指向方法的符號引用做爲參數。這些符號引用一部分會在類加載階段或第一次使用時轉化爲直接引用,這種轉化稱爲靜態解析。另外一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態鏈接。緩存
方法出口的解釋以下:安全
虛擬機棧會出現兩種異常,一種就是常見的OOM 另外一種就是StackOverFlowError。StackOverflowError通常是遞歸調用所致使的,棧深度在虛擬機中也是有限制的,不然無限制的遞歸調用虛擬機會哭的。OOM就不用說了,當所請求的內存大於當前虛擬機棧所持有的就會出現OOM(虛擬機棧空間能夠動態擴展,但分配給jvm的內存也是有限的,因此虛擬機棧也不是無限擴展的)。數據結構
2 本地方法棧多線程
本地方法棧和虛擬機棧基本是相似的,只不過虛擬機棧中執行的是class字節碼,而本地方法棧中執行的就是本地方法的服務,其實就是調用一些由c或c++根據不一樣的os平臺所寫的同一個方法的不一樣的實現。
3 方法區(method area)
方法區是線程共享的區域,用於存儲已經被虛擬機加載的類信息(類的字節碼數據,這裏要注意若是你同時加載的類不少的話須要調大方法區的空間,不然會OOM,只是對於類較少的狀況下能夠那麼作。若是類特別多,那麼能夠用懶加載等機制進行處理,如spring的懶加載機制,儘可能避免同時加載過多的類),常量,靜態變量和即時編譯器(JIT)編譯後的代碼等數據。方法區其實就是咱們所說的永久代區域(只限於hotspot虛擬機的實現機制),之因此說是永久代,是此處的數據幾乎不多進行垃圾回收,緣由是加載的類並非一時半刻就會消亡,不少方法會根據類在堆中建立對象,而靜態變量通常是,gc的跟搜索算法的root節點,而常量是根本不會變的數據,因此都不多進行清理。
Java虛擬機規範對這個區域的限制也很是寬鬆,除了能夠是物理不連續的空間外,也容許固定大小和擴展性,還能夠不實現垃圾收集。相對而言,垃圾收集行爲在這個區域是比較少出現的(因此常量和靜態變量的定義要多注意)。方法區的內存收集仍是會出現,不過這個區域的內存收集主要是針對常量池的回收和對類型的卸載。
通常來講方法區的內存回收比較難以使人滿意。當方法區沒法知足內存分配需求時將拋出OutOfMemoryError異常。
4 運行時常量池
JDK1.6以前字符串常量池位於方法區之中。
JDK1.7字符串常量池已經被挪到堆之中。
java是一種動態鏈接的語言,常量池的做用很是重要,常量池中除了包含代碼中所定義的各類基本類型(如int、long等等)和對象型(如String及數組)的常量值還,還包含一些以文本形式出現的符號引用,好比:
類和接口的全限定名;
字段的名稱和描述符;
方法和名稱和描述符。
在C語言中,若是一個程序要調用其它庫中的函數,在鏈接時,該函數在庫中的位置(即相對於庫文件開頭的偏移量)會被寫在程序中,在運行時,直接去這個地址調用函數;
在Java語言中這樣,一切都是動態的。編譯時,若是發現對其它類方法的調用或者對其它類字段的引用的話,記錄進class文件中的,只能是一個文本形式的符號引用,在鏈接過程當中,虛擬機根據這個文本信息去查找對應的方法或字段。
因此,與Java語言中的所謂「常量」不一樣,class文件中的「常量」內容很非富,這些常量集中在class中的一個區域存放,一個緊接着一個,這裏就稱爲「常量池」。
java中的常量池技術,是爲了方便快捷地建立某些對象而出現的,當須要一個對象時,就能夠從池中取一個出來(若是池中沒有則建立一個),則在須要重複建立相等變量時節省了不少時間。常量池其實也就是一個內存空間,不一樣於使用new關鍵字建立的對象所在的堆空間。
整個常量池會被JVM的一個索引引用,如同數組裏面的元素集合按照索引訪問同樣,JVM針對這些常量池裏面存儲的信息也是按照索引方式進行,實際上常量池在Java程序的動態連接過程起到了一個相當重要的做用(上面有說到),下文摘自《深刻理解java虛擬機》。
Class文件中除了有類的版本,字段,方法,接口等信息之外,還有一項信息是常量池用於存儲編譯器生成的各類字面量和符號引用,這部分信息將在類加載後存放到方法區的運行時常量池中。Java虛擬機對類的每一部分(包括常量池)都有嚴格的規定,每一個字節用於存儲哪一種數據都必須有規範上的要求,這樣纔可以被虛擬機承認,裝載和執行。通常來講,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,Java虛擬機並不要求常量只能在編譯期產生,也就是並不是預置入Class文件常量池的內容才能進入方法區的運行時常量池中,運行期間也可將新的常量放入常量池中。
常量池是方法區的一部分,因此受到內存的限制,當沒法申請到足夠內存時會拋出OutOfMemoryError異常
5 堆(heap)
堆就是內存中最大的一塊區域,惟一用於存儲對象實例的地方。這個地方也是gc算法主要的戰場。不過隨着JIT(即時編譯)的發展和逃逸技術成熟,並非全部的對象都在堆上建立。下文摘自《深刻理解java虛擬機》。
在Java編程語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的字節碼(包括須要被解釋的指令的程序)轉換成能夠直接發送給處理器的指令的程序。當你寫好一個Java程序後,源語言的語句將由Java編譯器編譯成字節碼,而不是編譯成與某個特定的處理器硬件平臺對應的指令代碼(好比,Intel的Pentium微處理器或IBM的System/390處理器)。字節碼是能夠發送給任何平臺而且能在那個平臺上運行的獨立於平臺的代碼。
java的內存分配大體就是這個樣子,jvm中也配有不少的參數對上面的數據進行調節。這裏就不進行列舉,會在單獨的一篇gc相關的文章中進行詳細的說明。下面說一下在多核處理器的時代,jvm是如何處理併發帶來的問題的。
併發控制
多核的cpu能夠併發的執行多個線程,而每一個線程都有一個本身的本地工做區(其實就是分配給每一個核的系統緩存和寄存器),存儲從上面主內存獲取的數據做副本在工做區中運行,若是數據是多線程中共享的,並且線程之間是不能進行數據交換,這就涉及了共享變量數據不一致的問題。java經過sychronized volatile Lock鎖等機制控制共享變量的可見性。
synchronized和lock會有單獨的章節分別講解實現機制, 這兩個不用說在可見性和原子性上都得道了保障。而volatile僅保證了數據的可見性,僅當數據在read 和load的時候數據在其餘線程中改變會在當前線程中有所感知,若是過了這兩個階段,那隻能很差意思了,數據不一致,(其實volatile所作的就是避免使用緩存不將主存上的數據存儲到線程工做內存中,在read和load階段都是從主存中獲取數據這樣就可以感知到其餘線程對變量的修改)。volatile僅僅是在早期的jdk版本中,因爲synchronized的性能很差而出現的一個保證可見性的一個解決方案。如今的jdk版本的synchronized和lock都獲得了必定的優化,因此通常的狀況下是不建議採用volatile變量的,除非你知道你如今用volatile到底在幹什麼,由於它並不能保證併發的正確性。
read and load 從主存複製變量到當前工做內存,use and assign 執行代碼,改變共享變量值,store and write 用工做內存數據刷新主存相關內容,其中use and assign 能夠屢次出現。volitale適合一些冪等操做。這個會在lock的nofairsync的實現中解說。
說到這裏就不得不說一下happen before原則了。它是Java內存模型中定義的兩項操做之間的偏序關係,若是操做A先行發生於操做B,其意思就是說,在發生操做B以前,操做A產生的影響都能被操做B觀察到,「影響」包括修改了內存中共享變量的值、發送了消息、調用了方法等,它與時間上的前後發生基本沒有太大關係。這個原則特別重要,它是判斷數據是否存在競爭、線程是否安全的主要依據。
下面是Java內存模型中的八條可保證happen—before的規則,它們無需任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們進行隨機地重排序(jvm爲了可以充分的利用cpu,提升利用率,jvm會將先後無關的代碼或者說是操做進行重排序,讓那些須要等待IO或者其餘資源的操做排在後面,而其餘可以瞬間完成的操做放在前面先執行,充分的利用cpu的資源)。
一、程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操做happen—before(時間上)後執行的操做。
二、管理鎖定規則:一個unlock操做happen—before後面(時間上的前後順序,下同)對同一個鎖的lock操做。
三、volatile變量規則:對一個volatile變量的寫操做happen—before後面對該變量的讀操做。
四、線程啓動規則:Thread對象的start()方法happen—before此線程的每個動做。
五、線程終止規則:線程的全部操做都happen—before對此線程的終止檢測,能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
六、線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。
七、對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。
八、傳遞性:若是操做A happen—before操做B,操做B happen—before操做C,那麼能夠得出A happen—before操做C。