Java內存區域與Java內存模型

Java虛擬機在運行程序時把其自動管理的內存劃分爲以下幾個區域。這個區域裏的一些數據在JVM啓動的時候創建,在JVM退出的時候銷燬。而其他的數據依賴於每一個線程,在線程創建時創建,在線程退出時銷燬。

  1. 方法區(Method Area):

方法區又稱Non-Heap(非堆),主要用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。簡單說方法區用來存儲類型的元數據信息,一個.class文件是類被java虛擬機使用之前的表現形式,一旦這個類要被使用,java虛擬機就會對其進行裝載、連接(驗證、準備、解析)和初始化。而裝載(後的結果就是由.class文件轉變爲方法區中的一段特定的數據結構。這個數據結構會存儲如下信息:

類型信息
這個類型的全限定名
這個類型的直接超類的全限定名
這個類型是類類型還是接口類型
這個類型的訪問修飾符
任何直接超接口的全限定名的有序列表
字段信息
字段名
字段類型
字段的修飾符
方法信息
方法名
方法返回類型
方法參數的數量和類型(按照順序)
方法的修飾
其他信息
除了常量以外的所有類(靜態)變量
一個指向ClassLoader的指針
一個指向Class對象的指針
常量池(常量數據以及對其他類型的符號引用)
JVM爲每個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer,和floating point常量)和對類型,域和方法的符號引用。池中的數據項象數組項一樣,是通過索引訪問的。

每個類的這些元數據,無論是在構建這個類的實例還是調用這個類某個對象的方法,都會訪問方法區的這些元數據。

構建一個對象時,JVM會在堆中給對象分配空間,這些空間用來存儲當前對象實例屬性以及其父類的實例屬性(而這些屬性信息都是從方法區獲得),注意,這裏並不是僅僅爲當前對象的實例屬性分配空間,還需要給父類的實例屬性分配,到此其實我們就可以回答第一個問題了,即實例化父類的某個子類時,JVM也會同時構建父類的一個對象。從另外一個角度也可以印證這個問題:調用當前類的構造方法時,首先會調用其父類的構造方法直到Object,而構造方法的調用意味着實例的創建,所以子類實例化時,父類肯定也會被實例化。

類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。這些變量只與類相關,所以在方法區中,它們成爲類數據在邏輯上的一部分。在JVM使用一個類之前,它必須在方法區中爲每個non-final類變量分配空間。

方法區主要有以下幾個特點:

1、方法區是線程安全的。由於所有的線程都共享方法區,所以,方法區裏的數據訪問必須被設計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區中的同一個類,而這個類還沒有被裝入JVM,那麼只允許一個線程去裝載它,而其它線程必須等待
2、方法區的大小不必是固定的,JVM可根據應用需要動態調整。同時,方法區也不一定是連續的,方法區可以在一個堆(甚至是JVM自己的堆)中自由分配。
3、方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集
可以通過-XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。

對於習慣在HotSpot 虛擬機上開發和部署程序的開發者來說,很多人願意把方法區稱爲“永久代”(PermanentGeneration),本質上兩者並不等價,僅僅是因爲HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

  1. JVM堆(Java Heap):

Java 堆也是屬於線程共享的內存區域,它在虛擬機啓動時創建,是Java 虛擬機所管理的內存中最大的一塊,主要用於存放對象實例,幾乎所有的對象實例都在這裏分配內存,注意Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做GC 堆,如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。

堆的大小可以通過-Xms(最小值)和-Xmx(最大值)參數設置,-Xms爲JVM啓動時申請的最小內存,默認爲操作系統物理內存的1/64但小於1G,-Xmx爲JVM可申請的最大內存,默認爲物理內存的1/4但小於1G,默認當空餘堆內存小於40%時,JVM會增大Heap到-Xmx指定的大小,可通過-XX:MinHeapFreeRation=來指定這個比列;當空餘堆內存大於70%時,JVM會減小heap的大小到-Xms指定的大小,可通過XX:MaxHeapFreeRation=來指定這個比列,對於運行系統,爲避免在運行時頻繁調整Heap的大小,通常-Xms與-Xmx的值設成一樣。

如果從內存回收的角度看,由於現在收集器基本都是採用的分代收集算法,所以Java 堆中還可以細分爲:新生代和老年代;

新生代:程序新創建的對象都是從新生代分配內存,新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱S0和S1或From和To)構成,可通過-Xmn參數來指定新生代的大小,也可以通過-XX:SurvivorRation來調整Eden Space及SurvivorSpace的大小。

老年代:用於存放經過多次新生代GC仍然存活的對象,例如緩存對象,新建的對象也有可能直接進入老年代,主要有兩種情況:1、大對象,可通過啓動參數設置-XX:PretenureSizeThreshold=1024(單位爲字節,默認爲0)來代表超過多大時就不在新生代分配,而是直接在老年代分配。2、大的數組對象,且數組中無引用外部對象。

老年代所佔的內存大小爲-Xmx對應的值減去-Xmn對應的值。

如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。

  1. 虛擬機棧(Java Virtual Machine Stacks):

線程私有,它的生命週期與線程相同。虛擬機棧描述的是Java 方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲 局部變量表、操作棧、動態鏈接、方法出口 等信息。

動畫是由一幀一幀圖片連續切換結果的結果而產生的,其實虛擬機的運行和動畫也類似,每個在虛擬機中運行的程序也是由許多的幀的切換產生的結果,只是這些幀裏面存放的是方法的局部變量,操作數棧,動態鏈接,方法返回地址和一些額外的附加信息組成。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。對於執行引擎來說,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法。執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。

  1. 本地方法棧(Native Method Stacks):

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

  1. 程序計數器(Program Counter Register):

程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

由於Java 虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則爲空(Undefined)。

此內存區域是唯一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

Java內存模型

Java內存模型(Java Memory Model,簡稱JMM)的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。此處的變量與Java編程時所說的變量不一樣,指包括了實例字段、靜態字段和構成數組對象的元素,但是不包括局部變量與方法參數,後者是線程私有的,不會被共享。

Java內存模型中規定:

線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量
不同線程之間無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要在主內存來完成

這裏的主內存、工作內存與Java內存區域的Java堆、棧、方法區不是同一層次內存劃分,這兩者基本上是沒有關係的,如果兩者一不定要勉強對就起來,那從變量,主內存,工作內存的定義來看,主內存對應Java堆中的對象實例數據部分,工作內存對應於虛擬機棧中的部分區域。

重排序

在執行程序時爲了提高性能,編譯器和處理器經常會對指令進行重排序。重排序分成三種類型:

編譯器優化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執行順序。
指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
內存系統的重排序。由於處理器使用緩存和讀寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。
as-if-serial語義

as-if-serial語義的意思是,所有的操作均可以爲了優化而被重排序,但是你必須要保證重排序後執行的結果不能被改變,編譯器、runtime、處理器都必須遵守as-if-serial語義。注意as-if-serial只保證單線程環境,多線程環境下無效。重排序不會影響單線程環境的執行結果,但是會破壞多線程的執行語義。

原子性、可見性與有序性

原子性:一個操作或者多個操作要麼全部執行要麼全部不執行;

可見性:當多個線程同時訪問一個共享變量時,如果其中某個線程更改了該共享變量,其他線程應該可以立刻看到這個改變;

有序性:程序的執行要按照代碼的先後順序執行;

happens-before原則

Java內存模型中定義的兩項操作之間的次序關係,如果說操作A先行發生於操作B,操作A產生的影響能被操作B觀察到,“影響”包含了修改了內存中共享變量的值、發送了消息、調用了方法等。

下面是Java內存模型下一些”天然的“happens-before關係,這些happens-before關係無須任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對它們進行隨意地重排序。

a.程序次序規則(Pragram Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流順序而不是程序代碼順序,因爲要考慮分支、循環結構。
b.管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裏必須強調的是同一個鎖,而”後面“是指時間上的先後順序。
c.volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生於後面對這個變量的讀取操作,這裏的”後面“同樣指時間上的先後順序。
d.線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作。
e.線程終於規則(Thread Termination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過Thread.join()方法結束,Thread.isAlive()的返回值等作段檢測到線程已經終止執行。
f.線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否有中斷髮生。
g.對象終結規則(Finalizer Rule):一個對象初始化完成(構造方法執行完成)先行發生於它的finalize()方法的開始。
g.傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。
一個操作”時間上的先發生“不代表這個操作會是”先行發生“,那如果一個操作”先行發生“是否就能推導出這個操作必定是”時間上的先發生 “呢?也是不成立的,一個典型的例子就是指令重排序。所以時間上的先後順序與happens-before原則之間基本沒有什麼關係,所以衡量併發安全問題一切必須以happens-before 原則爲準。

需要java學習路線圖的私信筆者“java”領取哦!另外喜歡這篇文章的可以給筆者點個贊,關注一下,每天都會分享Java相關文章!還有不定時的福利贈送,包括整理的學習資料,面試題,源碼等~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章