Java內存模型和JVM內存管理
一、Java內存模型:
1、主內存和工作內存(即是本地內存):
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與Java編程里面的變量有所不同步,它包含了實例字段、靜態字段和構成數組對象的元素,但不包含局部變量和方法參數,因為后者是線程私有的,不會共享,當然不存在數據競爭問題(如果局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。為了獲得較高的執行效能,Java內存模型並沒有限制執行引起使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。
JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。
如下圖可以很好的反應主內存與線程工作內存(即是本地內存)之間的關系:

當上圖中線程A與線程B要進行數據交互時,將要經歷:
1.線程A把本地內存B中更新過的共享變量刷新到主內存中去。
2.線程B到主內存中去讀取線程A刷新過的共享變量,然后copy一份到本地內存B 中去。
2、三大特性:原子性、可見性和有序性
Java內存模型就是圍繞着並發編程中的這三個特性來建立的。
原子性(Atomicity):
一個操作不能被打斷,要么全部執行完畢,要么不執行。在這點上有點類似於事務操作,要么全部執行成功,要么回退到執行該操作之前的狀態。基本類型數據的訪問大都是原子操作。
可見性:
一個線程對共享變量做了修改之后,其他的線程立即能夠看到(感知到)該變量這種修改(變化)。
Java內存模型是通過將在工作內存中的變量修改后的值同步到主內存,在讀取變量前從主內存刷新最新值到工作內存中,這種依賴主內存的方式來實現可見性的。
有序性:
對於一個線程的代碼而言,我們總是以為代碼的執行是從前往后的,依次執行的。這么說不能說完全不對,在單線程程序里,確實會這樣執行;但是在多線程並發時,程序的執行就有可能出現亂序。用一句話可以總結為:在本線程內觀察,操作都是有序的;如果在一個線程中觀察另外一個線程,所有的操作都是無序的。前半句是指“線程內表現為串行語義(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”現象和“工作內存和主內存同步延遲”現象。
Java提供了兩個關鍵字volatile和synchronized來保證多線程之間操作的有序性,volatile關鍵字本身通過加入內存屏障來禁止指令的重排序,而synchronized關鍵字通過一個變量在同一時間只允許有一個線程對其進行加鎖的規則來實現。
在單線程程序中,不會發生“指令重排”和“工作內存和主內存同步延遲”現象,只在多線程程序中出現。
3、happens-before原則:
Java內存模型中定義的兩項操作之間的次序關系,如果說操作A先行發生於操作B,操作A產生的影響能被操作B觀察到,“影響”包含了修改了內存中共享變量的值、發送了消息、調用了方法等。
如果兩個操作之間的關系不在此列,並且無法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對它們進行隨意地重排序。
l 程序次序規則(Pragram Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在后面的操作。准確地說應該是控制流順序而不是程序代碼順序,因為要考慮分支、循環結構。
l 管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於后面對同一個鎖的lock操作。這里必須強調的是同一個鎖,而”后面“是指時間上的先后順序。
l volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生於后面對這個變量的讀取操作,這里的”后面“同樣指時間上的先后順序。
l d.線程啟動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作。
l 線程終於規則(Thread Termination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過Thread.join()方法結束,Thread.isAlive()的返回值等作段檢測到線程已經終止執行。
l 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否有中斷發生。
l 對象終結規則(Finalizer Rule):一個對象初始化完成(構造方法執行完成)先行發生於它的finalize()方法的開始。
l 傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。
一個操作”時間上的先發生“不代表這個操作會是”先行發生“,那如果一個操作”先行發生“是否就能推導出這個操作必定是”時間上的先發生 “呢?也是不成立的,一個典型的例子就是指令重排序。所以時間上的先后順序與happens-before原則之間基本沒有什么關系,所以衡量並發安全問題一切必須以happens-before 原則為准。
二、JVM內存管理
JVM在執行Java程序的過程中,會把它管理的內存划分為幾個不同的數據區域,這些區域都有各自的用途、創建時間、銷毀時間。
Java運行時數據區分為下面幾個內存區域:(如上圖所示)
1.PC寄存器/程序計數器:
嚴格來說是一個數據結構,用於保存當前正在執行的程序的內存地址,由於Java是支持多線程執行的,所以程序執行的軌跡不可能一直都是線性執行。當有多個線程交叉執行時,被中斷的線程的程序當前執行到哪條內存地址必然要保存下來,以便用於被中斷的線程恢復執行時再按照被中斷時的指令地址繼續執行下去。為了線程切換后能恢復到正確的執行位置,每個線程都需要有一個獨立的程序計數器,各個線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存,這在某種程度上有點類似於“ThreadLocal”,是線程安全的。
2.Java棧 Java Stack:
Java棧總是與線程關聯在一起的,每當創建一個線程,JVM就會為該線程創建對應的Java棧,在這個Java棧中又會包含多個棧幀(Stack Frame),這些棧幀是與每個方法關聯起來的,每運行一個方法就創建一個棧幀,每個棧幀會含有一些局部變量、操作棧和方法返回值等信息。每當一個方法執行完成時,該棧幀就會彈出棧幀的元素作為這個方法的返回值,並且清除這個棧幀,Java棧的棧頂的棧幀就是當前正在執行的活動棧,也就是當前正在執行的方法,PC寄存器也會指向該地址。只有這個活動的棧幀的本地變量可以被操作棧使用,當在這個棧幀中調用另外一個方法時,與之對應的一個新的棧幀被創建,這個新創建的棧幀被放到Java棧的棧頂,變為當前的活動棧。同樣現在只有這個棧的本地變量才能被使用,當這個棧幀中所有指令都完成時,這個棧幀被移除Java棧,剛才的那個棧幀變為活動棧幀,前面棧幀的返回值變為這個棧幀的操作棧的一個操作數。
由於Java棧是與線程對應起來的,Java棧數據不是線程共有的,所以不需要關心其數據一致性,也不會存在同步鎖的問題。
在Java虛擬機規范中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。在Hot Spot虛擬機中,可以使用-Xss參數來設置棧的大小。棧的大小直接決定了函數調用的可達深度。

3.堆 Heap:
堆是JVM所管理的內存中國最大的一塊,是被所有Java線程鎖共享的,不是線程安全的,在JVM啟動時創建。堆是存儲Java對象的地方,這一點Java虛擬機規范中描述是:所有的對象實例以及數組都要在堆上分配。Java堆是GC管理的主要區域,從內存回收的角度來看,由於現在GC基本都采用分代收集算法,所以Java堆還可以細分為:新生代和老年代;新生代再細致一點有Eden空間、From Survivor空間、To Survivor空間等。
4.方法區Method Area:
方法區存放了要加載的類的信息(名稱、修飾符等)、類中的靜態常量、類中定義為final類型的常量、類中的Field信息、類中的方法信息,當在程序中通過Class對象的getName.isInterface等方法來獲取信息時,這些數據都來源於方法區。方法區是被Java線程鎖共享的,不像Java堆中其他部分一樣會頻繁被GC回收,它存儲的信息相對比較穩定,在一定條件下會被GC,當方法區要使用的內存超過其允許的大小時,會拋出OutOfMemory的錯誤信息。方法區也是堆中的一部分,就是我們通常所說的Java堆中的永久區 Permanet Generation,大小可以通過參數來設置,可以通過-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。
5.常量池Constant Pool:
常量池本身是方法區中的一個數據結構。常量池中存儲了如字符串、final變量值、類名和方法名常量。常量池在編譯期間就被確定,並保存在已編譯的.class文件中。一般分為兩類:字面量和應用量。字面量就是字符串、final變量等。類名和方法名屬於引用量。引用量最常見的是在調用方法的時候,根據方法名找到方法的引用,並以此定為到函數體進行函數代碼的執行。引用量包含:類和接口的權限定名、字段的名稱和描述符,方法的名稱和描述符。
6.本地方法棧Native Method Stack:
本地方法棧和Java棧所發揮的作用非常相似,區別不過是Java棧為JVM執行Java方法服務,而本地方法棧為JVM執行Native方法服務。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。
