JVM架构、JVM垃圾回收机制、垃圾回收算法、垃圾回收器、JMM(内存模型)
0 JVM和Java的關(guān)系
- JDK = JRE + Java開發(fā)工具(java,javac,javadoc,javap…)
- JRE = JVM + Java核心類庫
即: JDK = JVM + Java核心類庫 + Java開發(fā)工具
1 JVM架構(gòu)(JVM內(nèi)存區(qū)域)
1-1 JVM位置在哪里?
- 硬件——>OS——>JVM
- 即:JVM在操作系統(tǒng)之上。
1-2 JVM架構(gòu)
- 分三大塊:
- Class Loader Subsystem 類加載子系統(tǒng)
- Runtime Data Area 運行時數(shù)據(jù)區(qū)
- Execution Engine 執(zhí)行引擎
- 畫圖:
1-3 類加載器
- 位置:在類加載子系統(tǒng)。
- 類加載過程:加載——>鏈接(驗證、準備、解析)——>初始化
- 雙親委派機制:保證類加載和程序的安全。
- 1)類加載器收到類的請求
- 2)將這個請求向上委托給父類,一直向上委托(此時在最老)。(加載器:APP->EXC->BOOT)
- 3)啟動加載器檢查是否能加載當前類,能就結(jié)束,并使用當前加載器;不能就拋異常通知子加載器進行加載
- 4)重復(fù)3
- 總結(jié):在繼承關(guān)系中,最老的類優(yōu)先選擇。
1-4 沙箱安全機制
- 作用:Java安全的核心,限制程序運行環(huán)境。
1-5 棧stack
- 特點:先進后出,后進先出。
- 作用:主管程序的運行,生命周期和線程同步。線程結(jié)束棧內(nèi)存就釋放,不存在垃圾回收問題。
- 棧存什么?
- 八大基本數(shù)據(jù)類型
- 對象的引用
- 實例的方法
1-6 方法區(qū)
- 特點:被所有線程共享。
- 方法區(qū)存什么?
- 所有字段和方法字節(jié)碼。
- 靜態(tài)變量,常量,類信息,即時編譯器編譯后放入的代碼。
1-7 PC寄存器
- 每個線程都有一個程序計數(shù)器,是線程私有的,就會一個指針(我們接觸不到),指向方法區(qū)中的方法字節(jié)碼,在執(zhí)行引擎讀取下,加一。
- 它占用的內(nèi)容空間幾乎忽略不計。
1-8 堆heap(需要垃圾回收)
- 分區(qū):
- 1.8之前:新生區(qū)(伊甸園區(qū),幸存區(qū)),老年區(qū)
- 1.8之后:
- 堆中存什么?
- 對象實例和數(shù)組。
2 JVM垃圾回收機制——針對 堆
2-1 如何判斷對象是不是垃圾?
2-1-1 引用計數(shù)法
- 用計數(shù)器記錄對象的引用次數(shù),為0的就是垃圾。
- 缺點:難以解決對象之間相互循環(huán)引用問題,所以不用。
- PS:現(xiàn)在基本不用了。
2-2-2 可達性算法
- 將GC Roots對象作為起點,從這些節(jié)點向下搜索引用的對象,找到對象都標記為非垃圾對象,其余是垃圾對象。
- GC Roots根節(jié)點:線程棧的本地變量,靜態(tài)變量本地方法棧的變量等等。
- 優(yōu)點:解決了引用計數(shù)法的對象之間相互循環(huán)引用問題。
3 垃圾回收算法
3-1 復(fù)制算法
- gc時把依然存活的對象復(fù)制到兩個幸存區(qū)之一的to幸存區(qū)域(誰空誰是to)。當達到15次(默認)時,認為這個對象有很強的生命力,放到
- 缺點:浪費一個幸存區(qū)空間。
- 優(yōu)點:不產(chǎn)生碎片。
3-2 標記清除算法
- 直接回收垃圾對象,保留存活對象。
- 位置不連續(xù),產(chǎn)生碎片。
3-3 標記整理算法/標記壓縮算法
- 回收之后,整理空間。
- 優(yōu)點:不產(chǎn)生碎片。
- 缺點:效率變低。
4 垃圾回收器(10種…)
5 JMM(Java內(nèi)存模型)
- JMM定義了線程工作內(nèi)存與主內(nèi)存(線程之間的共享變量)的抽象關(guān)系。并不真實存在,它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個變量(包括實例字段,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式。
- JVM運行程序的實體是線程,而每個線程創(chuàng)建時JVM都會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為棧空間),用于存儲線程私有的數(shù)據(jù)。Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問。
- 首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對變量進行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝,前面說過,工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成。
- JMM 與 Java內(nèi)存區(qū)域 唯一相似點,都存在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域:
- 在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個程度上講應(yīng)該包括了堆和方法區(qū);而工作內(nèi)存數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個程度上講則應(yīng)該包括程序計數(shù)器、虛擬機棧以及本地方法棧。
- 此處為什么包括本地方法棧?本地方法棧中的本地方法是怎么被線程調(diào)用的?:Java虛擬機棧用于管理Java方法的調(diào)用,而本地方法棧用于管理本地方法的調(diào)用。本地方法棧,也是線程私有的。如果線程請求分配的棧容量超過本地方法棧允許的最大容量,Java虛擬機將會拋出一個 StackOverflowError 異常。如果本地方法棧可以動態(tài)擴展,并且在嘗試擴展的時候無法申請到足夠的內(nèi)存,或者在創(chuàng)建新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的本地方法棧,那么Java虛擬機將會拋出一個 OutOfMemoryError 異常。本地方法棧是線程私有就是因為 只有線程調(diào)用時,線程的本地方法棧才會登記本地方法。
- 它的具體做法是Native Method Stack中登記native方法,在Execution Engine 執(zhí)行時加載本地方法庫。
- 當某個線程調(diào)用一個本地方法時,它就進入了一個全新的并且不再受虛擬機限制的世界。它和虛擬機擁有同樣的權(quán)限。
- 本地方法可以通過本地方法接口來訪問虛擬機內(nèi)部的運行時數(shù)據(jù)區(qū)。
- 它甚至可以直接使用本地處理器中的寄存器。
- 直接從本地內(nèi)存的堆中分配任意數(shù)量的內(nèi)存。
- 或許在某些地方,我們可能會看見主內(nèi)存被描述為堆內(nèi)存,工作內(nèi)存被稱為線程棧,實際上他們表達的都是同一個含義。
個人理解:JMM只是用線程角度思考JVM;JVM架構(gòu)(JVM內(nèi)存區(qū)域)就是從物理角度。
- 在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個程度上講應(yīng)該包括了堆和方法區(qū);而工作內(nèi)存數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個程度上講則應(yīng)該包括程序計數(shù)器、虛擬機棧以及本地方法棧。
- 關(guān)于JMM中的主內(nèi)存和工作內(nèi)存說明如下:
- 主內(nèi)存 存什么?:主要存儲的是Java所有線程創(chuàng)建的實例對象(不管這個實例對象是成員變量還是方法中的本地變量(也叫局部變量)),當然也包括了類信息、常量、靜態(tài)變量。
- 工作內(nèi)存 存什么?:主要存儲當前方法的所有本地變量信息(針對本線程說)(主內(nèi)存中變量的拷貝副本),每個線程這能訪問自己的工作內(nèi)存(即線程中的本地變量對其他線程是不可見的,就算是兩個線程執(zhí)行同一段代碼,他們也會各自在自己的工作內(nèi)存中胡藏劍屬于當前線程的本地變量,當然也包括了字節(jié)碼行號指示器,相關(guān)Native方法的信息),由于工作內(nèi)存是每個線程的私有數(shù)據(jù),線程間無法相互訪問工作內(nèi)存,因此存儲在工作內(nèi)存的數(shù)據(jù)不存在線程安全問題。
- 作用:緩存一致性協(xié)議,用于定義數(shù)據(jù)讀寫的規(guī)則。
- 主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲類型以及操作方式:
- 對于一個實例對象中的成員方法而言,
- 如果方法中包含本地變量(局部變量)是基本數(shù)據(jù)類型(boolean,byte,short,char,int,long,float,double),將直接存儲在工作內(nèi)存的幀棧結(jié)構(gòu)中,但倘若本地變量(局部變量)是引用類型,那么該變量的引用會存儲在功能內(nèi)存的幀棧中,而對象實例(在這個方法中新建的對象實例)將存儲在主內(nèi)存(共享數(shù)據(jù)區(qū)域,堆)中。
- 但對于實例對象的成員變量而言,
- 不管它是基本數(shù)據(jù)類型或者包裝類型(Integer、Double等)還是引用類型,都會被存儲到堆區(qū)。
- 至于static變量以及類本身相關(guān)信息將會存儲在主內(nèi)存中。
- 需要注意的是,在主內(nèi)存中的實例對象可以被多線程共享,倘若兩個線程同時調(diào)用了同一個對象的同一個方法,那么兩條線程會將要操作的數(shù)據(jù)拷貝一份到自己的工作內(nèi)存中,執(zhí)行完成操作后才刷新到主內(nèi)存,簡單示意圖如下所示:
- 對于一個實例對象中的成員方法而言,
6 硬件內(nèi)存架構(gòu)與JMM
6-1 硬件內(nèi)存架構(gòu)
當一個CPU需要訪問主存時,會先讀取一部分主存數(shù)據(jù)到CPU緩存(當然如果CPU緩存中存在需要的數(shù)據(jù)就會直接從緩存獲取),進而在讀取CPU緩存到寄存器,當CPU需要寫數(shù)據(jù)到主存時,同樣會先刷新寄存器中的數(shù)據(jù)到CPU緩存,然后再把數(shù)據(jù)刷新到主內(nèi)存中。
6-2 Java線程與硬件處理器
6-2-1 JVM中線程的實現(xiàn)原理
在Window系統(tǒng)和Linux系統(tǒng)上,Java線程的實現(xiàn)是基于一對一的線程模型,所謂的一對一模型,實際上就是通過語言級別層面程序去間接調(diào)用系統(tǒng)內(nèi)核的線程模型(即我們在使用Java線程時,Java虛擬機內(nèi)部是轉(zhuǎn)而調(diào)用當前操作系統(tǒng)的內(nèi)核線程來完成當前任務(wù))。
這里需要了解一個術(shù)語,內(nèi)核線程(Kernel-Level Thread,KLT),它是由操作系統(tǒng)內(nèi)核(Kernel)支持的線程,這種線程是由操作系統(tǒng)內(nèi)核來完成線程切換,內(nèi)核通過操作調(diào)度器進而對線程執(zhí)行調(diào)度,并將線程的任務(wù)映射到各個**處理器(CPU)**上。
每個內(nèi)核線程可以視為內(nèi)核的一個分身,這也就是操作系統(tǒng)可以同時處理多任務(wù)的原因。由于我們編寫的多線程程序?qū)儆谡Z言層面的,程序一般不會直接去調(diào)用內(nèi)核線程,取而代之的是一種輕量級的進程(Light Weight Process),也是通常意義上的線程,由于每個輕量級進程都會映射到一個內(nèi)核線程,因此我們可以通過輕量級進程調(diào)用內(nèi)核線程,進而由操作系統(tǒng)內(nèi)核將任務(wù)映射到各個處理器,這種輕量級進程與內(nèi)核線程間1對1的關(guān)系就稱為一對一的線程模型。如下:
如圖所示,每個線程最終都會映射到CPU中進行處理,如果CPU存在多核,那么一個CPU將可以并行執(zhí)行多個線程任務(wù)。
6-2-2 Java內(nèi)存模型與硬件內(nèi)存架構(gòu)的關(guān)系
多線程的執(zhí)行最終都會映射到硬件處理器上進行執(zhí)行,但Java內(nèi)存模型和硬件內(nèi)存架構(gòu)并不完全一致。
對于硬件內(nèi)存來說只有寄存器、緩存內(nèi)存、主內(nèi)存的概念,并沒有工作內(nèi)存(線程私有數(shù)據(jù)區(qū)域)和主內(nèi)存(堆內(nèi)存)之分,也就是說Java內(nèi)存模型對內(nèi)存的劃分對硬件內(nèi)存并沒有任何影響,因為JMM只是一種抽象的概念,是一組規(guī)則,并不實際存在。
不管是工作內(nèi)存的數(shù)據(jù)還是主內(nèi)存的數(shù)據(jù),對于計算機硬件來說都會存儲在計算機主內(nèi)存中,當然也有可能存儲到CPU緩存或者寄存器中,因此總體上來說,Java內(nèi)存模型和計算機硬件內(nèi)存架構(gòu)是一個相互交叉的關(guān)系,是一種抽象概念劃分與真實物理硬件的交叉。(注意對于Java內(nèi)存區(qū)域劃分也是同樣的道理)。
7 JMM存在的必要性
由于JVM運行程序的實體是線程,而每個線程創(chuàng)建時JVM都會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為棧空間),用于存儲線程私有的數(shù)據(jù),線程與主內(nèi)存中的變量操作必須通過工作內(nèi)存間接完成,主要過程是將變量從主內(nèi)存拷貝的每個線程各自的工作內(nèi)存空間,然后對變量進行操作,操作完成后再將變量寫回主內(nèi)存。
如果存在兩個線程同時對一個主內(nèi)存中的實例對象的變量進行操作就有可能誘發(fā)線程安全問題。
為了解決這種問題,JVM定義了一組規(guī)則,通過這組規(guī)則來決定一個線程對共享變量的寫入何時對另一個線程可見,這組規(guī)則也稱為Java內(nèi)存模型(即JMM)。
JMM是圍繞著程序執(zhí)行的原子性、有序性、可見性展開的,下面我們看看這三個特性。
7-1 原子性:
原子性指的是一個操作是不可中斷的,即使是在多線程環(huán)境下,一個操作一旦開始就不會被其他線程影響。比如對于一個靜態(tài)變量int x,兩條線程同時對他賦值,線程A賦值為1,而線程B賦值為2,不管線程如何運行,最終x的值要么是1,要么是2,線程A和線程B間的操作是沒有干擾的,這就是原子性操作,不可被中斷的特點。
有點要注意的是,對于32位系統(tǒng)的來說,long類型數(shù)據(jù)和double類型數(shù)據(jù)(對于基本數(shù)據(jù)類型,byte,short,int,float,boolean,char讀寫是原子操作),它們的讀寫并非原子性的,也就是說如果存在兩條線程同時對long類型或者double類型的數(shù)據(jù)進行讀寫是存在相互干擾的,因為對于32位虛擬機來說,每次原子讀寫是32位的,而long和double則是64位的存儲單元,這樣會導(dǎo)致一個線程在寫時,操作完前32位的原子操作后,輪到B線程讀取時,恰好只讀取到了后32位的數(shù)據(jù),這樣可能會讀取到一個既非原值又不是線程修改值的變量,它可能是“半個變量”的數(shù)值,即64位數(shù)據(jù)被兩個線程分成了兩次讀取。但也不必太擔心,因為讀取到“半個變量”的情況比較少見,至少在目前的商用的虛擬機中,幾乎都把64位的數(shù)據(jù)的讀寫操作作為原子操作來執(zhí)行,因此對于這個問題不必太在意,知道這么回事即可。
7-2 指令重排:
7-2-1 內(nèi)容:
計算機在執(zhí)行程序時,為了提高性能,編譯器和處理器的常常會對指令做重排。
7-2-2 一般分以下3種
- 編譯器優(yōu)化的重排(編譯期重排)
編譯器在不改變線程程序的語義下,可以重新安排語義的執(zhí)行順序。 - 指令并行的重排(處理器重排)
現(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性(即后一個執(zhí)行的語句無需依賴前面執(zhí)行的語句的結(jié)果),處理器可以改變語句對應(yīng)的機器指令的執(zhí)行順序。 - 內(nèi)存系統(tǒng)的重排(處理器重排)
由于處理器使用緩存和讀寫緩存沖區(qū),這使得加載(load)和存儲(store)操作看上去可能是在亂序執(zhí)行,因為三級緩存的存在,導(dǎo)致內(nèi)存與緩存的數(shù)據(jù)同步存在時間差。
7-2-3 指令重排帶來的問題
編譯器重排
線程 1 線程 2 1: x2 = a ; 3: x1 = b ; 2: b = 1; 4: a = 2 ;兩個線程同時執(zhí)行,分別有1、2、3、4四段執(zhí)行代碼,其中1、2屬于線程1 , 3、4屬于線程2 。從程序的執(zhí)行順序上看,似乎不太可能出現(xiàn)x1 = 1 和x2 = 2 的情況,但實際上這種情況是有可能發(fā)現(xiàn)的,因為如果編譯器對這段程序代碼執(zhí)行重排優(yōu)化后,可能出現(xiàn)下列情況:
線程 1 線程 2 2: b = 1; 4: a = 2 ; 1:x2 = a ; 3: x1 = b ;這種執(zhí)行順序下就有可能出現(xiàn)x1 = 1 和x2 = 2 的情況,這也就說明在多線程環(huán)境下,由于編譯器優(yōu)化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的。
處理器指令重排
處理器指令重排是對CPU的性能優(yōu)化,從指令的執(zhí)行角度來說一條指令可以分為多個步驟完成,如下:
- 取址 IF
- 譯碼和取寄存器操作數(shù) ID
- 執(zhí)行或者有效地址計算 EX
- 存儲器訪問 MEM
- 寫回 WB
CPU在工作時,需要將上述指令分為多個步驟依次執(zhí)行(注意硬件不同有可能不一樣)。由于每一個步會使用到不同的硬件操作,比如取指時會只有PC寄存器和存儲器,譯碼時會執(zhí)行到指令寄存器組,執(zhí)行時會執(zhí)行ALU(算術(shù)邏輯單元)、寫回時使用到寄存器組。為了提高硬件利用率,CPU指令是按流水線技術(shù)來執(zhí)行的,如下:
雖然流水線技術(shù)可以大大提升CPU的性能,但不幸的是一旦出現(xiàn)流水中斷,所有硬件設(shè)備將會進入一輪停頓期,當再次彌補中斷點可能需要幾個周期,這樣性能損失也會很大,就好比工廠組裝手機的流水線,一旦某個零件組裝中斷,那么該零件往后的工人都有可能進入一輪或者幾輪等待組裝零件的過程。因此我們需要盡量阻止指令中斷的情況,指令重排就是其中一種優(yōu)化中斷的手段,我們通過一個例子來闡明指令重排是如何阻止流水線技術(shù)中斷的:
- LW指令 表示 load,其中LW R1,b表示把b的值加載到寄存器R1中
- LW R2,c 表示把c的值加載到寄存器R2中
- ADD 指令表示加法,把R1 、R2的值相加,并存入R3寄存器中。
- SW 表示 store 即將 R3寄存器的值保持到變量a中
- LW R4,e 表示把e的值加載到寄存器R4中
- LW R5,f 表示把f的值加載到寄存器R5中
- SUB 指令表示減法,把R4 、R5的值相減,并存入R6寄存器中。
- SW d,R6 表示將R6寄存器的值保持到變量d中
上述便是匯編指令的執(zhí)行過程,在某些指令上存在X的標志,X代表中斷的含義,也就是只要有X的地方就會導(dǎo)致指令流水線技術(shù)停頓,同時也會影響后續(xù)指令的執(zhí)行,可能需要經(jīng)過1個或幾個指令周期才可能恢復(fù)正常,那為什么停頓呢?
這是因為部分數(shù)據(jù)還沒準備好,如執(zhí)行ADD指令時,需要使用到前面指令的數(shù)據(jù)R1,R2,而此時R2的MEM操作沒有完成,即未拷貝到存儲器中,這樣加法計算就無法進行,必須等到MEM操作完成后才能執(zhí)行,也就因此而停頓了,其他指令也是類似的情況。
停頓會造成CPU性能下降,因此我們應(yīng)該想辦法消除這些停頓,這時就需要使用到指令重排了。如下圖,既然ADD指令需要等待,那我們就利用等待的時間做些別的事情,如把LW R4,e和LW R5,f移動到前面執(zhí)行,畢竟LW R4,e 和 LW R5,f執(zhí)行并沒有數(shù)據(jù)依賴關(guān)系,對他們有數(shù)據(jù)依賴關(guān)系的SUB R6,R5,R4指令在R4,R5加載完成后才執(zhí)行的,沒有影響,過程如下:
正如上圖所示,所有的停頓都完美消除了,指令流水線也無需中斷了,這樣CPU的性能也能帶來很好的提升,這就是處理器指令重排的作用。關(guān)于編譯器重排以及指令重排(這兩種重排我們后面統(tǒng)一稱為指令重排)相關(guān)內(nèi)容已闡述清晰了,我們必須意識到對于單線程而言指令重排幾乎不會帶來任何影響,畢竟重排的前提是保證串行語義執(zhí)行的一致性,但對于多線程環(huán)境而已,指令重排就可能導(dǎo)致嚴重的程序輪序執(zhí)行問題,如下:
class MixedOrder{int a = 0;boolean flag = false;public void writer(){a = 1;flag = true;}public void read(){if(flag){int i = a + 1;}} }如上述代碼,同時存在線程A和線程B對該實例對象進行操作,其中A線程調(diào)用寫入方法,而B線程調(diào)用讀取方法,由于指令重排等原因,可能導(dǎo)致程序執(zhí)行順序變?yōu)槿缦?#xff1a;
線程A 線程Bwriter: read:1:flag = true; 1:flag = true;2:a = 1; 2: a = 0 ; //誤讀3: i = 1 ;由于指令重排的原因,線程A的flag置為true被提前執(zhí)行了,而a賦值為1的程序還未執(zhí)行完,此時線程B,恰好讀取flag的值為true,直接獲取a的值(此時B線程并不知道a為0)并執(zhí)行i賦值操作,結(jié)果i的值為1,而不是預(yù)期的2,這就是多線程環(huán)境下,指令重排導(dǎo)致的程序亂序執(zhí)行的結(jié)果。
因此,請記住,指令重排只會保證單線程中串行語義的執(zhí)行的一致性,但并不會關(guān)心多線程間的語義一致性。
7-3 可見性:
理解了指令重排現(xiàn)象后,可見性容易了,可見性指的是當一個線程修改了某個共享變量的值,其他線程是否能夠馬上得知這個修改的值。對于串行程序來說,可見性是不存在的,因為我們在任何一個操作中修改了某個變量的值,后續(xù)的操作中都能讀取這個變量值,并且是修改過的新值。
但在多線程環(huán)境中可就不一定了,前面我們分析過,由于線程對共享變量的操作都是線程拷貝到各自的工作內(nèi)存進行操作后才寫回到主內(nèi)存中的,這就可能存在一個線程A修改了共享變量x的值,還未寫回主內(nèi)存時,另外一個線程B又對主內(nèi)存中同一個共享變量x進行操作,但此時A線程工作內(nèi)存中共享變量x對線程B來說并不可見,這種工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就造成了可見性問題,另外指令重排以及編譯器優(yōu)化也可能導(dǎo)致可見性問題,通過前面的分析,我們知道無論是編譯器優(yōu)化還是處理器優(yōu)化的重排現(xiàn)象,在多線程環(huán)境下,確實會導(dǎo)致程序輪序執(zhí)行的問題,從而也就導(dǎo)致可見性問題。
7-4 有序性:
有序性是指對于單線程的執(zhí)行代碼,我們總是認為代碼的執(zhí)行是按順序依次執(zhí)行的,這樣的理解并沒有毛病,畢竟對于單線程而言確實如此。
但對于多線程環(huán)境,則可能出現(xiàn)亂序現(xiàn)象,因為程序編譯成機器碼指令后可能會出現(xiàn)指令重排現(xiàn)象,重排后的指令與原指令的順序未必一致,要明白的是,在Java程序中,倘若在本線程內(nèi),所有操作都視為有序行為,如果是多線程環(huán)境下,一個線程中觀察另外一個線程,所有操作都是無序的,前半句指的是單線程內(nèi)保證串行語義執(zhí)行的一致性,后半句則指指令重排現(xiàn)象和工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象。
JMM提供的解決方案
7-5 JMM提供的解決方案:
在理解了原子性,可見性以及有序性問題后,看看JMM是如何保證的,在Java內(nèi)存模型中都提供一套解決方案供Java工程師在開發(fā)過程使用。
- 原子性問題
除了JVM自身提供的對基本數(shù)據(jù)類型讀寫操作的原子性外,對于方法級別或者代碼塊級別的原子性操作,可以使用synchronized關(guān)鍵字或者重入鎖(ReentrantLock)保證程序執(zhí)行的原子性。 - 可見性問題
而工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題,可以使用synchronized關(guān)鍵字或者volatile關(guān)鍵字解決,它們都可以使一個線程修改后的變量立即對其他線程可見。
對于指令重排導(dǎo)致的可見性問題和有序性問題,則可以利用volatile關(guān)鍵字解決,因為volatile的另外一個作用就是禁止重排序優(yōu)化。除了靠sychronized和volatile關(guān)鍵字來保證原子性、可見性以及有序性外,JMM內(nèi)部還定義一套happens-before 原則來保證多線程環(huán)境下兩個操作間的原子性、可見性以及有序性。 - 程序順序原則,即在一個線程內(nèi)必須保證語義串行性,也就是說按照代碼順序執(zhí)行。
- 鎖規(guī)則 解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個鎖的加鎖(lock)之前,也就是說,如果對于一個鎖解鎖后,再加鎖,那么加鎖的動作必須在解鎖動作之后(同一個鎖)。
- volatile規(guī)則 volatile變量的寫,先發(fā)生于讀,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內(nèi)存中讀該變量的值,而當該變量發(fā)生變化時,又會強迫將最新的值刷新到主內(nèi)存,任何時刻,不同的線程總是能夠看到該變量的最新值。
- 線程啟動規(guī)則 線程的start()方法先于它的每一個動作,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值,那么當線程B執(zhí)行start方法時,線程A對共享變量的修改對線程B可見。
- 傳遞性 A先于B ,B先于C 那么A必然先于C。
- 線程終止規(guī)則 線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當前執(zhí)行的線程終止。假設(shè)在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對共享變量的修改將對線程A可見。
- 線程中斷規(guī)則 對線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否中斷。
- 對象終結(jié)規(guī)則 對象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法。
上述8條原則無需手動添加任何同步手段(synchronized|volatile)即可達到效果,下面我們結(jié)合前面的案例演示這8條原則如何判斷線程是否安全,如下:
存在兩條線程A和B,線程A調(diào)用實例對象的writer()方法,而線程B調(diào)用實例對象的read()方法。線程A先啟動而線程B后啟動,那么線程B讀取到的i值是多少呢?現(xiàn)在依據(jù)8條原則,由于存在兩條線程同時調(diào)用,因此程序次序原則不合適。writer()方法和read()方法都沒有使用同步手段,鎖規(guī)則也不合適。沒有使用volatile關(guān)鍵字,volatile變量原則不適應(yīng)。線程啟動規(guī)則、線程終止規(guī)則、線程中斷規(guī)則、對象終結(jié)規(guī)則、傳遞性和本次測試案例也不合適。
線程A和線程B的啟動時間雖然有先后,但線程B執(zhí)行結(jié)果卻是不確定,也是說上述代碼沒有適合8條原則中的任意一條,也沒有使用任何同步手段,所以上述的操作是線程不安全的。
因此線程B讀取的值自然也是不確定的。修復(fù)這個問題的方式很簡單,要么給writer()方法和read()方法添加同步手段,如synchronized或者給變量flag添加volatile關(guān)鍵字,確保線程A修改的值對線程B總是可見。
7-6 理解JMM中的 happens-before 原則:
倘若在程序開發(fā)中,僅靠sychronized和volatile關(guān)鍵字來保證原子性、可見性以及有序性,那么編寫并發(fā)程序可能會顯得十分麻煩,幸運的是,在Java內(nèi)存模型中,還提供了happens-before 原則來輔助保證程序執(zhí)行的原子性、可見性以及有序性的問題。它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的依據(jù),happens-before 原則內(nèi)容如下:
提問:
1 JVM最開始各個區(qū)域的內(nèi)存量是固定的嗎?
是固定的。
public class Demo1 {public static void main(String[] args) {//返回JVM試圖使用的最大內(nèi)存Long max = Runtime.getRuntime().maxMemory(); // 字節(jié) 1024 * 1024//返回JVM的總內(nèi)存Long total = Runtime.getRuntime().totalMemory();System.out.println("max = " + max + "字節(jié)\t\t" + (max/(double)1024/1024) + "MB\t" + (max/(double)1024/1024/1024) + "G");System.out.println("total = " + total + "字節(jié)\t" + (total/(double)1024/1024) + "MB\t\t" + (total/(double)1024/1024/1024) + "G");} } 輸出: max = 1866465280字節(jié) 1780.0MB 1.73828125G total = 126877696字節(jié) 121.0MB 0.1181640625G- 結(jié)論:
默認情況下,分配的JVM總內(nèi)存,max = 電腦內(nèi)存的1/4
初始化的內(nèi)存,total = 1/64
開心一刻:
在日常英語中,stack和heap二者都指堆積(動詞)和一堆(名詞),但是
- heap 通常指雜亂的、呈小山狀的一堆東西,如:Now, the house is a heap of rubble(現(xiàn)在,房子成了一堆瓦礫)。
- stack通常是整齊的一疊,指扁平物體疊放起來,如:a neat stack of dishes(整齊的一疊盤子)。
參考文章:
- CSDN:
- JMM : https://blog.csdn.net/belongtocode/article/details/113918720
- 本地方法棧:https://blog.csdn.net/weixin_43893397/article/details/121899679
參考視頻:
- B站:
- JVM : https://www.bilibili.com/video/BV1iJ411d7jS?p=14&spm_id_from=333.880.my_history.page.click
- JVM : https://www.bilibili.com/video/BV1sp4y1Y7ap?p=4&spm_id_from=333.880.my_history.page.click
總結(jié)
以上是生活随笔為你收集整理的JVM架构、JVM垃圾回收机制、垃圾回收算法、垃圾回收器、JMM(内存模型)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 15000 字的 MySQL 速查手册
- 下一篇: 苹果cmsV10暗色系在线动漫影视网站模