并發編程之Synchronized深入理解
前言
- 并發編程從操作系統底層工作的整體認識開始
- 深入理解Java內存模型(JMM)及volatile關鍵字
- 深入理解CPU緩存一致性協議(MESI)
在并發編程中存在線程安全問題,主要原因有:1.存在共享數據 2.多線程共同操作共享數據。關鍵字synchronized可以保證在同一時刻,只有一個線程可以執行某個方法或某個代碼塊,同時synchronized可以保證一個線程的變化可見(可見性),即可以代替volatile。
設計同步器的意義
多線程編程中,有可能會出現多個線程同時訪問同一個共享、可變資源的情況,這個資源我們稱之為 臨界資源 。這種資源可能是:對象、變量、文件等。
- 共享:資源可以由多個線程同時訪問;
- 可變:資源可以在其生命周期內被修改。
引出的問題:由于線程執行的過程是不可控的,所以需要采用同步機制來協調對對象可變狀態的訪問。
如何解決線程并發安全問題?
實際上,所有的并發模式在解決線程安全問題時,采用的方案都是 序列化訪問臨界資源 。即在同一時刻,只能有一個線程訪問臨界資源,也稱作 同步互斥訪問 。
Java 中,提供了兩種方式來實現同步互斥訪問:synchronized 和 Lock
同步器的本質就是加鎖。加鎖目的:序列化訪問臨界資源,即同一時刻只能有一個線程訪問臨界資源(同步互斥訪問)
不過有一點需要區別的是:當多個線程執行一個方法時,該方法內部的局部變量并不是臨界資源,因為這些局部變量是在每個線程的私有棧中,引出不具有共享性,不會導致線程安全問題。
synchronized 原理分析
synchronized 內在鎖是一種對象鎖(鎖的是對象而非引用),作用粒度是對象,可以用來實現對臨界資源的同步互斥訪問,是可重入的。
加鎖的方式:
1.同步實例方法,鎖是當前實例對象
synchronized修飾非靜態方法 鎖定的是該類的實例, 同一實例在多線程中調用才會觸發同步鎖定 所以多個被synchronized修飾的非靜態方法在同一實例下 只能多線程同時調用一個。
- public class Juc_LockOnThisObject {
- private Integer stock = 10;
- public synchronized void decrStock() {
- --stock;
- System.out.println(ClassLayout.parseInstance(this).toPrintable());
- }
- }
2.同步類方法,鎖是當前類對象
synchronized修飾靜態方法 鎖定的是類本身,而不是實例, 同一個類中的所有被synchronized修飾的靜態方法, 只能多線程同時調用一個。
- public class Juc_LockOnClass {
- private static int stock;
- public static synchronized void decrStock(){
- System.out.println(--stock);
- }
- }
3.同步代碼塊,鎖是括號里面的對象
synchronized塊 直接鎖定指定的對象,該對象在多個地方的同步鎖定塊,只能多線程同時執行其中一個。
- public class Juc_LockOnObject {
- public static Object object = new Object();
- private Integer stock = 10;
- public void decrStock() {
- //T1,T2
- synchronized (object) {
- --stock;
- if (stock <= 0) {
- System.out.println("庫存售罄");
- return;
- }
- }
- }
- }
synchronized底層原理
synchronized 是基于 JVM內置鎖 實現,通過內部對象Monitor(監視器鎖) 實現,基于進入與退出 Monitor 對象實現方法與代碼塊同步,監視器鎖的實現依賴底層操作系統的 Mutex lock (互斥鎖)實現,它是一個重量級鎖性能較低。當然,JVM 內置鎖在1.5之后版本做了大量的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Eliminaction)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,內置鎖的并發性能已經基本與 Lock 持平。
synchronized 關鍵字被編譯成字節碼后會被翻譯成 monitorenter 和 monitorexit 兩條指令分別在同步塊邏輯代碼的起始位置與結束位置。

示例:
- public class Juc_LockOnObject {
- public static Object object = new Object();
- private Integer stock = 10;
- public void decrStock() {
- //T1,T2
- synchronized (object) {
- --stock;
- if (stock <= 0) {
- System.out.println("庫存售罄");
- return;
- }
- }
- }
- }
反編譯結果如下:

Monitor 監視器鎖
每個同步對象都有一個自己的 Monitor(監視器鎖),加鎖過程如下所示:

任何一個對象都有一個 Monitor 與之關聯,當且一個 Monitor 被持有后,它將處于鎖定狀態。
Synchronized 在JVM里面的實現都是 基于進入和退出 **Monitor 對象 **來實現方法同步和代碼塊同步,雖然具體實現細節不一樣,但是都可以通過成對的 MonitorEnter 和 MonitorExit 指令來實現。
- monitorenter :每個對象都是一個監視器鎖(monitor)。當 monitor 被占用時就會處于鎖定狀態,線程執行 monitorenter 指令時嘗試獲取 monitor 的所有權,其過程如下:如果 monitor 的進入數為0,則該線程進入monitor,然后將進入數設置為1,該線程即為monitor的所有者;如果線程已經占有該 monitor,只是重新進入,則進入 monitor 的進入數加1;如果其他線程已經占有 monitor,則該線程進入阻塞狀態,直到 monitor 的進入數為0,再重新嘗試獲取 monitor 的所有權。
- monitorexit :執行 monitorexit 的線程必須是 objectref 所對應的 monitor 的所有者。指令執行時,monitor的進入數減1,如果減1后進入數為0,那么線程退出 monitor,不再是這個 monitor 的所有者。其他被這個 monitor 阻塞的線程可以嘗試去獲取這個 monitor 的所有權。
- monitorexit,指令出現了兩次,第1次為同步正常退出釋放鎖;第2次為發生異常退出釋放鎖;
通過上面兩段描述,我們應該能很清楚地看出 Synchronized 的實現原理,Synchronized 的語義底層是通過一個 monitor 的對象來完成,其實 wait/notify 等方法也依賴于 monitor 對象,這就是為什么只有在同步的塊或者方法中才能調用 wait/nofity 等方法,否則會拋出 java.lang.IllegalMonitorStateException 的異常原因。
示例:看一個同步方法
- package com.niuh;
- public class SynchronizedMethod {
- public synchronized void method() {
- System.out.println("Hello Word!");
- }
- }
反編譯結果:

從編譯的結果來看,方法的同步并沒有通過指令 monitorenter 和 monitorexit 來完成(理論上其實也可以通過這兩條指令來實現),不過相對于普通方法,其常量池多了 ACC_SYNCHRONIZED 標識符。
JVM 就是根據該標識符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標識是否被設置,如果設置了,執行線程將先獲取 monitor,獲取成功之后才能執行方法體,方法執行完后再釋放 monitor。在方法執行期間,其他任何線程都無法獲得同一個 monitor 對象。
兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過字節碼來完成。兩個指令的執行是 JVM 通過調用操作系統的互斥原語mutex 來實現,被阻塞的線程會被掛起,等待重新調度,會導致“用戶態和內核態”兩個態直接來回切換,對性能有較大的影響。
什么是 Monitor ?
可以把它理解為 一個同步工具,也可以描述為 一種同步機制,它通常被描述為一個對象。與一切皆對象一樣,所有的Java對象是天生的 Monitor,每一個Java 對象都有成為 Monitor 的潛質,因為在Java的設計中,每一個Java對象自打娘胎里出來就帶了一把看不見的鎖,它叫做內部鎖或者 Monitor鎖。也就是通常說的 Synchronized 的對象鎖,MarkWord 鎖標識位為10,其中指針指向的是 Monitor 對象的起始地址。在 Java 虛擬機(HotSpot)中,Monitor 是由 ObjectMonitor 實現的,其主要數據結構如下(位于 HotSpot 虛擬機源碼 ObjectMonitor.hpp 文件,C++實現的):
- ObjectMonitor() {
- _header = NULL;
- _count = 0; // 記錄個數
- _waiters = 0,
- _recursions = 0;
- _object = NULL;
- _owner = NULL;
- _WaitSet = NULL; // 處于wait狀態的線程,會被加入到_WaitSet
- _WaitSetLock = 0 ;
- _Responsible = NULL ;
- _succ = NULL ;
- _cxq = NULL ;
- FreeNext = NULL ;
- _EntryList = NULL ; // 處于等待鎖block狀態的線程,會被加入到該列表
- _SpinFreq = 0 ;
- _SpinClock = 0 ;
- OwnerIsThread = 0 ;
- }
ObjectMonitor 中有兩個隊列,_WaitSet 和 _EntryList,用來保存 ObjectWaiter 對象列表(每個等待鎖的線程都會被封裝成 ObjectWaiter 對象),_owner 指向持有 ObjectWaiter 對象的線程,當多個線程同時訪問一段同步代碼時:
- 首先會先進入 _EntryList 集合,當線程獲取到對象的 monitor 后,進入 _owner 區域并把 monitor 中的 _owner 變量設置為當前線程,同時 monitor 中的計數器 count 加1;
- 若線程調用 wait() 方法,將釋放當前持有的 monitor,owner 變量恢復為 null,count 自減1,同時該線程進入 _WaitSet 集合中等待被喚醒;
- 若當前線程執行完畢,也架構釋放 monitor(鎖) 并復位 count 的值,以便其他線程進入獲取 monitor(鎖);
同時,Monitor 對象存在于每個Java對象的對象頭 Mark Word 中(存儲的指針的指向),Synchronized 鎖便是通過這種方式獲取鎖的,也是為什么 Java 中任意對象可以作為鎖的原因,同時 notify/notifyAll/wait 等方法會使用到 Monitor 鎖對象,所以必須在同步代碼塊中使用。監視器 Monitor 有兩種同步方式:互斥與協作。多線程環境下線程之間如果需要共享數據,需要解決互斥訪問數據的問題,監視器可以確保監視器上的數據在同一時刻只會有一個線程在訪問。
那么有個問題來了,我們知道 synchronized 加鎖加在對象上,對象是如何記錄鎖狀態的呢?答案是鎖狀態被記錄在每個對象的對象頭(Mark Word)中,下面一起來認識下對象的內存布局。
對象的內存布局
HostSpot 虛擬機中,對象在內存中存儲的布局可以分為三塊區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)。
- 對象頭:比如 hash 碼,對象所屬的年代,對象鎖,鎖狀態標識,偏向鎖(線程)ID,偏向時間,數組長度(數組對象)等。Java 對象頭一般占用2個機器碼(在32位虛擬機中,1個機器碼等于4字節,也就是32bit,在64位虛擬機中,1個機器碼是8個字節,也就是64big),但是 如果對象是數組類型,則需要3個機器碼,因為 JVM 虛擬機可以通過 Java 對象的元數據信息確定Java 對象的大小,但是無法從數組的元數據來確認數組的大小,所以用一塊記錄數組長度。
- 實例數據:存放類的屬性數據信息,包括父類的屬性信息;
- 對齊填充:由于虛擬機要求 對象起始位置必須是8字節的整數倍。填充數據不是必須存在的,僅僅是為了字節對齊;

對象頭
HotSpot 虛擬機的 對象頭 包括兩部分信息
第一部分是 “Mark Word”,用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標識、線程持有的鎖、偏向鎖ID、偏向時間戳等等,它是實現輕量級鎖和偏向鎖的關鍵。
這部分數據的長度在32位和64位的虛擬機(暫不考慮開啟壓縮指針的場景)中分別為32個和64個Bits,官方稱它為“Mark Word”。對象需要存儲的運行時的數據很多,其實已經超出了 32、64 位Bitmap 結構所能記錄的限度,但是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率, Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存存儲盡量多的信息,它會根據對象的狀態復用到自己的存儲空間。
例如在32位的HotSpot虛擬機中對象未被鎖定的狀態下,Mark Word 的32位Bits空間中的:
- 25Bits用于存儲對象哈希碼(HashCode)
- 4Bits用于存儲對象分代年齡
- 2Bits用于存儲鎖標識位,
- 1Bits固定為0
- 其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下的對象存儲內容如下表所示
但是如果對象是數組類型,則需要三個機器碼,因為JVM虛擬機可以通過Java 對象的元數據信息確定Java對象的大小,但是無法從數組的元數據來確認數組的大小,所以用一塊記錄數組的長度。
對象頭信息是與對象自身定義的數據無關的額外存儲成本,但是考慮到虛擬機的空間效率,Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存存儲盡量多的數據,它會根據對象的狀態復用自己的存儲空間,也就是說,Mark Word會隨著程序的運行發生變化。變化狀態如下:
32位虛擬機

64位虛擬機
現在我們虛擬機基本是64位的,而64位的對象頭有點浪費空間,JVM默認會開啟指針壓縮,所以基本上也是按32位的形式記錄對象頭的。
- 手動設置-XX:+UseCompressedOops
哪些信息會被壓縮?
- 對象的全局靜態變量(即類屬性)
- 對象頭信息:64位平臺下,原生對象頭大小為16字節,壓縮后為12字節
- 對象的引用類型:64位平臺下,引用類型本身大小為8字節,壓縮后為4字節
- 對象數組類型:64位平臺下,數組類型本身大小為24字節,壓縮后16字節
在Scott oaks寫的《java性能權威指南》第八章8.22節提到了當heap size堆內存大于32GB是用不了壓縮指針的,對象引用會額外占用20%左右的堆空間,也就意味著要38GB的內存才相當于開啟了指針壓縮的32GB堆空間。
這是為什么呢?看下面引用中的紅字(來自openjdk wiki:https://wiki.openjdk.java.net/display/HotSpot/CompressedOops)。32bit最大尋址空間是4GB,開啟了壓縮指針之后呢,一個地址尋址不再是1byte,而是8byte,因為不管是32bit的機器還是64bit的機器,java對象都是8byte對齊的,而類是java中的基本單位,對應的堆內存中都是一個一個的對象。
Compressed oops represent managed pointers (in many but not all places in the JVM) as 32-bit values which must be scaled by a factor of 8 and added to a 64-bit base address to find the object they refer to. This allows applications to address up to four billion objects (not bytes), or a heap size of up to about 32Gb. At the same time, data structure compactness is competitive with ILP32 mode.
對象頭分析工具
運行時對象頭鎖狀態分析工具JOL,他是OpenJDK開源工具包,引入下方maven依賴
- <dependency>
- <groupId>org.openjdk.jol</groupId>
- <artifactId>jol-core</artifactId>
- <version>0.10</version>
- </dependency>
打印markword
- System.out.println(ClassLayout.parseInstance(object).toPrintable());
- //object為我們的鎖對象
鎖的膨脹升級過程
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。從JDK1.6中默認是開啟偏向鎖和輕量級鎖的,可以通過 -XX:-UseBiasedLocking 來禁用偏向鎖。下圖為鎖的升級全過程:

偏向鎖
偏向鎖 是Java 6 之后加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為來減少同一線程獲取鎖(會涉及到一些 CAS 操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時 Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對于沒有競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對于鎖競爭畢竟激烈的場合,偏向鎖就失效了,因為這樣的場合極有可能每次申請鎖的線程都是不同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。
- 默認開啟偏向鎖
- 開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 關閉偏向鎖:-XX:-UseBiasedLocking
輕量級鎖
倘若偏向鎖失敗,虛擬機并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之后加入的),此時 Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程序性能的依據是:“對絕大部分的鎖,在整個同步周期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
自旋鎖
輕量級鎖失敗后,虛擬機為了避免線程真實地操作系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基于在大多數情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的線程可以獲得鎖,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(這也是稱為自旋的原因),一般不會太久,可能是50個循環或100個循環,在經過若干次循環后,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最后沒有辦法也就只能升級為重量級鎖了。
鎖粗化
通常情況下,為了保證多線程間的有效并發,會要求每個線程持有鎖的時間盡可能短,但是在某些情況下,一個程序對同一個鎖不間斷、高頻地請求、同步與釋放,會消耗掉一定的系統資源,因為鎖的講求、同步與釋放本身會帶來性能損耗,這樣高頻的鎖請求就反而不利于系統性能的優化了,雖然單次同步操作的時間可能很短。鎖粗化就是告訴我們任何事情都有個度,有些情況下我們反而希望把很多次鎖的請求合并成一個請求,以降低短時間內大量鎖請求、同步、釋放帶來的性能損耗。
一種極端的情況如下:
- public void doSomethingMethod(){
- synchronized(lock){
- //do some thing
- }
- //這是還有一些代碼,做其它不需要同步的工作,但能很快執行完畢
- synchronized(lock){
- //do other thing
- }
- }
上面的代碼是有兩塊需要同步操作的,但在這兩塊需要同步操作的代碼之間,需要做一些其它的工作,而這些工作只會花費很少的時間,那么我們就可以把這些工作代碼放入鎖內,將兩個同步代碼塊合并成一個,以降低多次鎖請求、同步、釋放帶來的系統性能消耗,合并后的代碼如下:
- public void doSomethingMethod(){
- //進行鎖粗化:整合成一次鎖請求、同步、釋放
- synchronized(lock){
- //do some thing
- //做其它不需要同步但能很快執行完的工作
- //do other thing
- }
- }
注意:這樣做是有前提的,就是中間不需要同步的代碼能夠很快速地完成,如果不需要同步的代碼需要花很長時間,就會導致同步塊的執行需要花費很長的時間,這樣做也就不合理了。
另一種需要鎖粗化的極端的情況是:
- for(int i=0;i<size;i++){
- synchronized(lock){
- }
- }
上面代碼每次循環都會進行鎖的請求、同步與釋放,看起來貌似沒什么問題,且在jdk內部會對這類代碼鎖的請求做一些優化,但是還不如把加鎖代碼寫在循環體的外面,這樣一次鎖的請求就可以達到我們的要求,除非有特殊的需要:循環需要花很長時間,但其它線程等不起,要給它們執行的機會。
鎖粗化后的代碼如下:
- synchronized(lock){
- for(int i=0;i<size;i++){
- }
- }
鎖消除
鎖消除是虛擬機另外一種鎖的優化,這種優化更徹底,Java虛擬機在JIT編譯時(可以簡單立即為當某段代碼即將第一次被執行時進行編譯,又稱為即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下 StringBuffer 的 append 是一個同步方法,但是在 add 方法中的 StringBuffer 屬于一個局部變量,并且不會被其他線程所使用,因此 StringBuffer 不可能在共享資源競爭的情景,JVM 會自動將其鎖消除。鎖消除的依據是逃逸分析的數據支持。
鎖消除,前提是java必須運行在 server模式(server模式會比client模式做更多的優化),同時必須開啟逃逸分析
- -XX:+DoEscapeAnalysis 開啟逃逸分析
- -XX:+EliminateLoacks 表示開啟鎖消除
鎖消除是發生在編譯器級別的一種鎖優化方式,有時候我們寫的代碼完全不需要加鎖,卻執行了加鎖操作。比如,StringBuffer 類的 append 操作
- @Override
- public synchronized StringBuffer append(String str) {
- toStringCache = null;
- super.append(str);
- return this;
- }
從源碼中可以看出,append 方法使用了 synchronized 關鍵字,它是線程安全的。但我們可能僅在線程內部把 StringBuffer 當作局部變量使用:
- package com.niuh;
- public class Juc_LockAppend {
- public static void main(String[] args) {
- long start = System.currentTimeMillis();
- int size = 10000;
- for (int i = 0; i < size; i++) {
- createStringBuffer("一角錢技術", "為分享技術而生");
- }
- long timeCost = System.currentTimeMillis() - start;
- System.out.println("createStringBuffer:" + timeCost + " ms");
- }
- public static String createStringBuffer(String str1, String str2) {
- StringBuffer sBuf = new StringBuffer();
- sBuf.append(str1);// append方法是同步操作
- sBuf.append(str2);
- return sBuf.toString();
- }
- }
代碼中 createStringBuffer 方法中的局部對象 sBuf ,就只在該方法內的作用域有效,不同線程同時調用 createStringBuffer() 方法時,都會創建不同的 sBuf 對象,因此此時的 append 操作若是使用同步操作,就是白白浪費系統資源。
這時我們可以通過編譯器將其優化,將鎖消除 ,前提是 Java 必須運行在 server 模式(server模式會比client模式作更多的優化),同時必須開啟逃逸分析:
- -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
逃逸分析:比如上面的代碼,它要看 sBuf 是否可能逃出它的作用域?如果將 sBuf 作為方法的返回值進行返回,那么它在方法外部可能被當作一個全局對象使用,就有可能發生線程安全問題,這時就可以說 sBuf 這個對象發生逃逸了,因而不應將 append 操作的鎖消除,但我們上面的代碼沒有發生鎖逃逸,鎖消除就可以帶來一定的性能提升。
逃逸分析
使用逃逸分析,編譯器可以對代碼做如下優化:
- 同步省略。如果一個對象被發現只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。
- 將堆分配轉化為棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
- 分離對象或標量替換。有的對象可能不需要作為一個連續的內存結構也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存,而是存儲在CPU寄存器中。
是不是所有的對象和數組都會在堆內存分配空間?答案是:不一定
在Java代碼運行時,通過 JVM 參數可以指定釋放開啟逃逸分析:
- -XX:+DoEscapeAnalysis 表示開啟逃逸分析
- -XX:-DoEscapeAnalysis 表示關閉逃逸分析
從JDK1.7 開始已經默認開啟逃逸分析,如需關閉,需要指定 -XX:-DoEscapeAnalysis
逃逸分析案例:循環創建50W次 NiuhStudent 對象
- package com.niuh;
- public class T0_ObjectStackAlloc {
- /**
- * 進行兩種測試
- * 關閉逃逸分析,同時調大堆空間,避免堆內GC的發生,如果有GC信息將會被打印出來
- * VM運行參數:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- *
- * 開啟逃逸分析
- * VM運行參數:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- *
- * 執行main方法后
- * jps 查看進程
- * jmap -histo 進程ID
- */
- public static void main(String[] args) {
- long start = System.currentTimeMillis();
- for (int i = 0; i < 500000; i++) {
- alloc();
- }
- long end = System.currentTimeMillis();
- //查看執行時間
- System.out.println("cost-time " + (end - start) + " ms");
- try {
- Thread.sleep(100000);
- } catch (InterruptedException e1) {
- e1.printStackTrace();
- }
- }
- private static NiuhStudent alloc() {
- //Jit對編譯時會對代碼進行 逃逸分析
- //并不是所有對象存放在堆區,有的一部分存在線程棧空間
- NiuhStudent student = new NiuhStudent();
- return student;
- }
- static class NiuhStudent {
- private String name;
- private int age;
- }
- }
第一種情況:關閉逃逸分析,同時調大堆空間,避免堆內GC的發生,如果有GC信息將會被打印出來
- 設置VM運行參數:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
如下所示創建了50W個實例對象
第二種情況:開啟逃逸分析
- 設置VM運行參數:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
如下未創建50W個實例對象

PS:以上代碼提交在 Github :
https://github.com/Niuh-Study/niuh-juc-final.git