注意區分Java內存模型(Java Memory Model,簡稱JMM)與Jvm內存結構,前者與多線程相關,后者與JVM內部存儲相關。本文會對兩者進行簡單介紹。
一、JAVA內存模型(JMM)
1. 概念
說來話長,由于在不同硬件廠商和不同操作系統之間內存訪問有一定差異,所以會使得相同代碼在不同平臺上運行結果可能不一致。為了使java程序在各種平臺下達成一致的運行效果,所以JMM屏蔽掉各種硬件和操作系統的內存訪問差異。
JMM規定除局部變和方法參數以外的所有變量都存儲在主內存中。從線程角度,其基本工作方式是:工作內存保存了線程用到的變量和主內存的副本,只能修改工作內存的值然后刷回主存,不能直接讀寫主內存中的變量。
一般問到Java內存模型都是想問多線程,Java并發相關的問題。
2. 內存屏障
現代計算機CPU多為多核,每核有自己的高速緩存,易導致內存數據讀寫不一致,產生指令亂序和不可見性問題。內存屏障確保指令順序執行和內存操作的全局可見性,防止重排序,并即時更新和展示內存數據給其他CPU核,解決讀寫延遲問題。讀屏障清除緩存,確保后續讀取最新數據;寫屏障刷新緩存數據到內存,使其對其他核可見。JMM針對讀load寫store提出了針對這兩個操作的四種組合來覆蓋度讀寫的所有情況。
LoadLoad 屏障:確保所有之前的讀操作都完成后再執行之后的讀操作。
StoreStore 屏障:確保所有之前的寫操作都完成后再執行之后的寫操作。
LoadStore 屏障:確保所有之前的讀操作都完成后再執行之后的寫操作。
StoreLoad 屏障:確保所有之前的寫操作都完成并對其他處理器可見后,再執行之后的讀操作。
3.原子性 可見性 有序性
3.1原子性
原子性指的是一個操作是不可分割,不可中斷的,一個線程在執行時不會被其他線程干擾。i++不是原子操作,因為它是先讀取到i,再加1,是兩步操作不保證原子性。代表性的是synchronized關鍵字,該關鍵字修飾的方法或代碼塊可保證原子性。
3.2 可見性
可見性是指一個線程修改了某個變量的值,這個改動能立即被其他線程感知。volatile關鍵字可以保證變量的可見性,當變量被該關鍵字修飾時,這個變量的改動會被立即刷新到內存,其他線程會在主內存中讀取該變量的新值。final和synchronized也可保證可見性。
<happens-before>
happens-before是指前一個操作的結果對后續操作是可見的,并不是指前面一個操作一定發生在后面一個操作的前面。在不改變程序執行結果的前提下,編譯器和處理器可以自由優化程序執行順序,因為程序員只關心程序執行的語義是否正確。
3.3 有序性
在Java中,volatile和synchronized都能維護多線程操作的有序性。volatile通過內存屏障禁止指令重排,而synchronized則通過鎖定機制,確保同一時間只有一個線程可以執行被其保護的代碼塊,從而實現有序性。
4.?synchronezid?volatile關鍵字
4.1?synchronezid?
4.1.1 基本使用
synchronezid可以修飾方法、類和代碼塊。修飾實例方法鎖住的是對象,即對象鎖;修飾靜態方法鎖住的是類,即類鎖;修飾代碼塊,指定加鎖對象,對給定對象加鎖,也是對象鎖。
對象鎖可以有多個,new幾個對象就有幾個對象鎖,但是類鎖只有一把。
//修飾方法
public synchronized void add(){i++;
}
//修飾類
public static synchronized void add(){i++;
}
//修飾代碼塊
public void add() {synchronized (this) {i++;}
}
4.1.2 底層原理
查看上面代碼的字節碼
//修飾代碼塊
public void add();Code:0: aload_01: dup2: astore_13: monitorenter // synchronized關鍵字的入口4: getstatic #2 // Field i:I7: iconst_18: iadd9: putstatic #2 // Field i:I12: aload_113: monitorexit // synchronized關鍵字的出口14: goto 2217: astore_218: aload_119: monitorexit // synchronized關鍵字的出口20: aload_221: athrow22: return
通過字節碼文件看出synchronized修飾代碼塊使用monitorenter和monitorexit指令。monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置。每個對象有一個監視器鎖(monitor)。當monitor被占用時就會處于鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,設置計數器值為1。執行monitorexit指令,將釋放 monitor(鎖)并設置計數器值為0。monitor存儲于對象頭信息中,每個對象都存在一個monitor與之關聯。
//修飾方法
public synchronized void add();descriptor: ()Vflags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field i:I5: iconst_16: iadd7: putfield #2 // Field i:I10: returnLineNumberTable:line 5: 0line 6: 10
synchronized修飾實例方法對應的字節碼沒有 monitorenter和monitorexit ,卻額外多了 ACC_SYNCHRONIZED。因為整個方法都是同步代碼,因此就不需要標記同步代碼的入口和出口了。當線程線程執行到這個方法時會判斷是否有這個ACC_SYNCHRONIZED標志,如果有的話則會嘗試獲取monitor對象鎖。如果有異常發生,線程自動釋放鎖。
4.2?volatile
能保證變量的可見性,禁止指令重排序。
可見性原理
每個線程都有一個Jvm棧,棧內保存線程運行時的變量信息。當線程訪問對象的屬性時,首先會找到堆內對象存的變量值,再將其保存為棧內的一個副本,之后會直接修改副本中屬性的值。修改完后不會立即將修改的值更新到堆中,這就導致某些線程讀取到的還是舊值。volatile就是當副本中屬性的值被修改后保證其能立即同步到堆中,從而其他線程讀取到該值,也是新的值。
禁止指令重排序原理
通過插入內存屏障禁止指令重排序。插入內存屏障,相當于告訴CPU和編譯器先于這個命令的必須先執行,后于這個命令的必須后執行。volatile寫操作的前面插入一個StoreStore屏障,后面插入一個SotreLoad屏障。
<volatile不能保證線程安全,可見性不能保證原子操作>
?
二、JVM內存結構
1. 組成
JVM的內存劃分為5部分,Java棧,本地方法棧,堆,程序計數器和方法區。
1-JAVA棧 即虛擬機棧
根據線程創建而創建,所以每個線程都有一個虛擬機棧。虛擬機棧存儲的是棧幀,每個棧幀對應一個方法,且都有自己的局部變量表,操作數棧、動態鏈接和返回地址等。
局部變量表存放了編譯器可知的各種基本數據類型(int、short、byte、char、double、float、long、boolean)、對象引用(reference類型,它不等同于對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型(指向了一跳字節碼指令的地址)。
JVM規范中,Java虛擬機棧部分規定了兩種異常:StackOverflowError
發生在遞歸調用過深時,由于程序設計的錯誤,如遞歸無終止條件;OutOfMemoryError
發生在JVM內存不足或設置過小,導致無法為新線程分配棧空間。
2-本地方法棧
java虛擬機棧為虛擬機執行Java方法服務。本地方法棧則為虛擬機使用的native方法服務。native方法是用C語言實現的底層方法。
3-堆
生命周期與進程相同,被所有線程所共享的內存區域。該區域存放的是對象實例。堆同時也是GC的主要區域。通常情況下,它占用的空間是所有內存區域中最大的,但如果無節制地創建大量對象,也容易消耗完所有的空間;堆的內存空間既可以固定大小,也可運行時動態地調整,通過參數-Xms設定初始值、-Xmx設定最大值。
4-程序計數器
它是一塊極小的內存空間。記錄了當前線程執行到的字節碼行號。每個線程都有自己的程序計數器,互不影響。native方法計數器為空。
5-方法區
被線程共享,儲存已被虛擬機加載的類信息、常量、靜態變量、jit編譯后的代碼等數據。
Java源代碼編譯成Java Class文件后通過類加載器ClassLoader加載到JVM中
類存放在方法區中
類創建的對象存放在堆中
堆中對象的調用方法時會使用到虛擬機棧,本地方法棧,程序計數器
方法執行時每行代碼由解釋器逐行執行
熱點代碼由JIT編譯器即時編譯
垃圾回收機制回收堆中資源
和操作系統打交道需要調用本地方法接口
2. 類加載過程
2.1 加載
加載指的是將類的class文件讀入到內存中,并為之創建一個java.lang.Class對象。 類加載階段可以使用系統提供的類加載器(ClassLoader)來完成,也可以使用用戶自定義的類加載器(繼承ClassLoader)完成。
2.2?連接
2.2.1 驗證
驗證被加載的類文件符合JVM規范,保證載入的類不會危害JVM。
文件格式驗證→元數據驗證→字節碼驗證→符號引用驗證
2.2.1.1 文件格式驗證
2.2.1.2 元數據驗證
2.2.1.3?字節碼驗證
2.2.1.4?符號引用驗證
2.2.2?準備
在方法區中為類變量(被static修飾的變量)分配內存,并將其初始化為默認值。
對于 public static int value = 123;變量value在準備階段過后的初始值為0而不是123,初始化時才會將value值賦為123。 如果類字段的字段屬性表中存在ConnstantValue屬性,那在準備階段value就會被初始化為ConstantValue屬性所指定的值,如:public static final int value = 123;編譯時Javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值為123。
2.2.3?解析
將類中的符號引用轉化為直接引用。編譯的時候每個java類都會被編譯成一個class文件,但在編譯的時候虛擬機并不知道所引用類的地址,所以就用符號引用來代替。符號引用以一組符號來描述所引用的目標。直接引用可以是直接指向目標的指針。
2.3?初始化
執行類的初始化方法(<clinit>()
方法)來初始化類的靜態變量(程序設置值)和執行靜態代碼塊。
2.4?使用
2.5?卸載
3. 類加載機制
1、全盤負責 類加載器加載某個類時,該類所依賴和引用其它的類也由該類加載器載入。
2、雙親委派 先讓父加載器加載該類,父加載器無法加載時才考慮自己加載。 如果父加載器還存在其父加載器,則進一步向上委托,如果父類加載器可以完成父加載任務,就成功返回,如果父加載器無法完成加載任務,子加載器才會嘗試自己去加載,可避免重復加載。
3、緩存機制 緩存機制保證所有加載過的class都會被緩存,當程序中需要某個類時,先從緩存區中搜索,如果不存在,才會讀取該類對應的二進制數據,并將其轉換成class對象,存入緩存區中。 這就是為什么修改了class后,必須重啟JVM,程序所做的修改才會生效的原因。
4. 反射
Java 的反射機制是指在運行狀態中,對于任意一個類都能夠知道這個類所有的屬性和方法; 并且對于任意一個對象,都能夠調用它的任意一個方法;這種動態獲取信息以及動態調用對象方法的功能成為Java語言的反射機制。
4.1 實例化方式
Date date=new Date();
//方式1
Class<?> date =Class.forName("java.util.Date");
//方式2
System.out.println(date.getClass());
//方式3
System.out.println(Date.class);
4.2 實例化對象
//通過反射機制,獲取Class,通過Class來實例化對象
Class<?> cl=Class.forName("java.util.Date");
//newInstance() 這個方法會調用Date這個類的無參數構造方法,完成對象的創建。
// 重點是:newInstance()調用的是無參構造,必須保證無參構造是存在的!
Object object=cl.newInstance();
5.GC
(待施工)