java程序執行過程
運行時數據區域劃分
線程私有區域
1. 程序計數器:
1.1. 這塊內存區域很小,是當前線程所執行的字節碼行號的指示器。字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。
1.2. 線程執行java方法,程序計數器才有值,保存當前需要執行的指令地址,如果執行的是native方法,則這個計數器是未定義即是空的。
1.3. 在jvm中,多線程是通過線程輪流切換來獲得cpu執行時間。在任何一具體時刻,一個cpu的內核只會執行一條線程中的指令。所以為了使得每個線程在線程切換后能夠恢復到切換前的狀態,每個線程都要需要自己獨立且互不干擾的程序計數器。
2. java棧/虛擬機棧
2.1. 生命周期和線程生命周期相同,每個方法執行都會創建一個棧幀。
2.2. 存儲局部變量表,操作數棧,指向當前方法所屬的類的運行時常量池的引用,方法返回地址和一些額外的附加信息。每一個方法從調用到執行完畢的過程,對應一個棧幀在虛擬機中入棧到出棧的過程。(局部變量表就是存儲局部變量,包過在方法中聲明的非靜態變量以及函數形參。對于基礎類型直接存儲值,對于引用類型則存的是指向對象的引用)
-
本地方法棧
作用和原理與java棧相似,不同是為執行本地方法服務的。
線程間共享的區域
1. 堆
大多數應用程序中,堆都是虛擬機所管理內存中最大的一塊,唯一目的存放對象的實例和數組(數組的引用存放在java棧中)
2. 方法區
在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯后的代碼等
3. 運行時常量池
是方法區中有一個非常重要的部分,Class文件中除了有類的版本信息、字段、方法、接口等描述信息外,還有一項信息就是常量池。它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后,對應的運行時常量池就被創建出來。當然并非Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中
對象創建過程
虛擬機上運到new指令時創建對象的步驟:
- 類加載檢查
去檢查這個指令的參數能否在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經被加載、解析和初始化,如果沒有,那么必須先執行類的初始化過程 - 虛擬機為新生對象分配內存。對象所需內存大小在類加載完成后便可以完全確定。
- 內存分配結束,虛擬機將分配到的內存空間都初始化為零值(不包括對象頭)
- 對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息存放在對象的對象頭中
- 執行<init>方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來
怎么保證new對象的安全
虛擬機采用了CAS配上失敗重試的方式保證更新更新操作的原子性和TLAB(Thread Local Allocation Buffer,即線程本地分配緩存區)
TLAB這是一個線程專用的內存分配區域。
由于對象一般會分配在堆上,而堆是全局共享的。因此在同一時間,可能會有多個線程在堆上申請空間。因此,每次對象分配都必須要進行同步(虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性),而在競爭激烈的場合分配的效率又會進一步下降。JVM使用TLAB來避免多線程沖突,在給對象分配內存時,每個線程使用自己的TLAB,這樣可以避免線程同步,提高了對象分配的效率。
對象內存分配的兩種方法
為對象分配空間的任務等同于把一塊確定大小的內存從Java堆中劃分出來。
指針碰撞(Serial、ParNew等帶Compact過程的收集器)
假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。
空閑列表(CMS這種基于Mark-Sweep算法的收集器)
如果Java堆中的內存并不是規整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。
GC回收機制
1. 那些內存需要回收
堆是Java虛擬機進行垃圾回收的主要場所,其次要場所是方法區
2. 什么時候回收
2.1 對象在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
2.2. 當老年代中沒有足夠的內存空間來存放對象時,虛擬機會發起一次Major GC/Full GC,只要老年代的連續空間大于新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full CG。
2.3. 手動調用System.gc()方法,通常這樣會觸發一次的Full GC以及至少一次的Minor GC.
3. 對什么回收,如何找到這些需要回收的垃圾
3.1 堆回收
引用計數法 :Java中卻沒有使用這種算法,因為這種算法很難解決對象之間相互引用的情況。
可達性分析法:通過一系列稱為“GC Roots”的對象作為起始點,從這些節點向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的。
Java語言中可以作為GC Roots的對象,只有引用類型的變量才被認為是根,值類型的變量永遠不被認為是根包括:
a. 虛擬機棧中引用的對象
b. 方法區中靜態屬性引用的對象
c. 方法區中常量引用的對象
d.本地方法棧中JNI(即Native方法)引用的對象
3.2 方法區回收
虛擬機規范中不要求方法區一定要實現垃圾回收,而且方法區中進行垃圾回收的效率也確實比較低,但是HotSpot對方法區也是進行回收的,主要回收的是廢棄常量和無用的類兩部分。判斷一個常量是否“廢棄常量”比較簡單,只要當前系統中沒有任何一處引用該常量就好了,但是要判定一個類是否“無用的類”條件就要苛刻很多,類需要同時滿足以下三個條件:
1、該類所有實例都已經被回收,也就是說Java堆中不存在該類的任何實例
2、加載該類的ClassLoader已經被回收
3、該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載功能,以保證方法區不會溢出。
4. 垃圾回收算法
4.1 標記-清除(Mark-Sweep)算法
這是最基礎的算法,標記-清除算法就如同它的名字樣,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,標記完成后統一回收所有被標記的對象。這種算法的不足主要體現在效率和空間,從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除后會產生大量不連續的內存碎片, 內存碎片太多可能會導致以后程序運行過程中在需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發一次垃圾收集動作
4.2 復制(Copying)算法
復制算法是為了解決效率問題而出現的,它將可用的內存分為兩塊,每次只用其中一塊,當這一塊內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已經使用過的內存空間一次性清理掉。這樣每次只需要對整個半區進行內存回收,內存分配時也不需要考慮內存碎片等復雜情況,只需要移動指針,按照順序分配即可.
新生代的內存被劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。每次回收時,將Eden和Survivor中還存活著的對象一次性復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden區和Survivor區的比例為8:1,意思是每次新生代中可用內存空間為整個新生代容量的90%。當然,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保(Handle Promotion)
4.3 標記-整理(Mark-Compact)算法
過程與標記-清除算法一樣,不過不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然后直接清理掉邊界以外的內存
GC實現機制-Java虛擬機具體實現流程
a虛擬機在進行垃圾回收時,將Eden和Survivor中還存活著的對象進行一次性地復制到另一塊Survivor空間上,直到其兩個區域中對象被回收完成,當Survivor空間不夠用時,需要依賴其他老年代的內存進行分配擔保。當另外一塊Survivor中沒有足夠的空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老生代,在老生代中不僅存放著這一種類型的對象,還存放著大對象(需要很多連續的內存的對象),當Java程序運行時,如果遇到大對象將會被直接存放到老生代中,長期存活的對象也會直接進入老年代。如果老生代的空間也被占滿,當來自新生代的對象再次請求進入老生代時就會報OutOfMemory異常。
6 GC判斷對象死亡過程
Java虛擬機在進行死亡對象判定時,會經歷兩個過程。如果對象在進行可達性分析后沒有與GC Roots相關聯的引用鏈,則該對象會被JVM進行第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法,如果當前對象沒有覆蓋該方法,或者finalize方法已經被JVM調用過都會被虛擬機判定為“沒有必要執行”。如果該對象被判定為沒有必要執行,那么該對象將會被放置在一個叫做F-Queue的隊列當中,并在稍后由一個虛擬機自動建立的、低優先級的Finalizer線程去執行它,在執行過程中JVM可能不會等待該線程執行完畢,因為如果一個對象在finalize方法中執行緩慢,或者發生死循環,將很有可能導致F-Queue隊列中其他對象永久處于等待狀態,甚至導致整個內存回收系統崩潰。如果在finalize方法中該對象重新與引用鏈上的任何一個對象建立了關聯,即該對象連上了任何一個對象的引用鏈,例如this關鍵字,那么該對象就會逃脫垃圾回收系統;如果該對象在finalize方法中沒有與任何一個對象進行關聯操作,那么該對象會被虛擬機進行第二次標記,該對象就會被垃圾回收系統回收。值得注意的是finaliza方法JVM系統只會自動調用一次,如果對象面臨下一次回收,它的finalize方法不會被再次執行。
7.內存分配 主要規律
7.1. 本地線程分配緩沖
每個線程在Java堆中預先分配一小塊內存,TLAB比較小,直接在TLAB上分配內存的方式稱為快速分配方式,而TLAB大小不夠,導致內存被分配在Eden區的內存分配方式稱為慢速分配方式。
7.2. 對象優先分配在Eden區上
7.3. 大對象直接進入老年代
7.4. 長期存活的對象將進入老年代。Eden區中的對象在一次Minor GC后沒有被回收,則對象年齡+1,當對象年齡達到“-XX:MaxTenuringThreshold”設置的值的時候,對象就會被晉升到老年代中。
7.5. Survivor空間中相同年齡的所有對象大小總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到“-XX:MaxTenuringThreshold”設置要求的年齡
8 Java內存模型
主內存和工作內存
Java內存模型的主要目的是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。注意一下,此處的變量并不包括局部變量與方法參數,因為它們是線程私有的,不會被共享,自然也不會存在競爭,此處的變量應該是實例字段、靜態字段和構成數組對象的元素。
Java內存模型規定了所有的變量都存儲在主內存(Main Memory)中,每條線程還有自己的工作內存(Working Memory),線程的工作內存中保存了被該線程使用到的變量和主內存副本拷貝(是一些在線程中用到的對象中的字段罷了),線程對變量所有的操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成
內存間相互交互
關于主內存與工作內存之間具體的交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存之類的實現細節,Java內存模型中定義了以下8種操作來完成,虛擬機實現時必須保證下面體積的每一種操作都是原子的、不可再分的:
1、lock(鎖定):作用于主內存中的變量,它把一個變量標識為一條線程獨占的狀態
2、unlock(解鎖):作用于主內存中的變量,它把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定
3、read(讀取):作用于主內存中的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用
4、load(載入):作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
5、use(使用):作用于工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,沒當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作
6、assign(賦值):作用于工作內存中的變量,它把一個從執行引擎接收到的值賦值給工作內存中的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作
7、store(存儲):作用于工作內存中的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用
8、write(寫入):作用于主內存中的變量,它把store操作從工作內存中得到的變量值放入主內存的變量中
Java內存模型還規定了在執行上述8種基本操作時必須滿足以下規則:
1、不允許read和load、store和write操作之一單獨出現
2、不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了滯后必須把該變化同步回主內存
3、不允許一個線程無原因地把數據從線程的工作內存同步回主內存中
4、一個新的變量只能從主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量
5、一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖
6、如果對同一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值
7、如果一個變量事先沒有被lock操作鎖定,那就不允許對它進行unlock操作,也不允許去unlock一個被其他線程鎖定的變量
8、對一個變量執行unlock操作之前,必須先把此變量同步回主內存中
volatile型變量的特殊規則
1、在工作內存中,每次使用某個變量的時候都必須線從主內存刷新最新的值,用于保證能看見其他線程對該變量所做的修改之后的值
2、在工作內存中,每次修改完某個變量后都必須立刻同步回主內存中,用于保證其他線程能夠看見自己對該變量所做的修改
3、volatile修飾的變量不會被指令重排序優化,保證代碼的執行順序與程序順序相同