一、內存管理
1、運行時的內存區域
線程私有:虛擬機棧、本地方法棧、程序計數器
線程共享:堆、方法區
2、各個內存區域可能拋出的異常
棧
1、當單線程時,棧的深度太大,會發生StackOverflowError,比如無窮的遞歸調用。
2、當多線程時,若不停地創建線程,則會導致OutOfMemoryError,因為除去堆和方法區之外,剩下的棧總空間是有限的,不停創建線程則會不停申請棧空間,最終會導致內存溢出。
堆
當不停地創建(new)對象時,會導致OutOfMemoryError
方法區
運行時產生大量的類,去填滿方法區,比如用CGLib去無窮生成類。
直接內存
使用Unsafe分配本機內存時,可能導致OutOfMemoryError。
3、各個內存區域容量設置的參數
-Xss2M:設置棧的容量為2M
-Xms10M:設置堆的初始容量為10M
-Xmx10M:設置對的最大容量為10M
-XX:PermSize=10M:設置方法區的初始容量為10M
-XX:MaxPermSize=10M:設置方法區的最大容量為10M
-XX:MaxDirectMemorySize=10M:設置直接內存的最大容量為10M
4、對象的創建
如何在堆中分配內存
根據內存是否規整,即GC收集器是否帶有壓縮整理功能,分為指針碰撞和空閒列表兩種。
如何處理內存分配衝突
1、CAS+失敗重試;2、TLAB,即本地線程分配緩衝。
對象在內存中的佈局
對象頭(哈希碼、GC分代年齡、所狀態標誌等)、實例數據、對象填充
如何訪問對象
1、通過句柄(棧上的指針指向句柄,句柄中分別有指向對象的指針,和指向類信息的指針)
2、直接指針(棧上的指針直接指向堆中的對象,對象中頭部有一個類型指針,指向類型信息)
5、對象存活判定
即如何判斷一個對象所佔用的內存是否該回收?
有兩種方法:1、引用計數法;2、可達性分析法。
引用計數法
該方法容易出現循環引用的問題,JVM並未採用。
可達性分析法
判斷是否能從GCRoots中找到一條到達該對象的路徑。
GCRoots包括:棧中變量引用的對象、方法區中靜態屬性(static)引用的對象、方法區中常量(final)引用的對象。
6、垃圾回收
引用的種類
1、強引用:通常new出來的對象的引用都是強引用。
2、軟引用(SoftReference):如果某次回收完之後,還是可能發生內存溢出,則進行第二次回收,在第二次回收時會回收軟引用,若這次回收後仍是內存不夠,這時候才發生內存溢出。
3、弱引用(WeakReference):其指向的對象只能生存到下一次垃圾收集之前,無論當前內存是否足夠,都會回收弱引用指向的對象。
4、虛引用(PhantomReference):設置虛引用的目的可能是為了,在該對象被回收前能夠得到一個系統通知。
finalize方法
若某類覆蓋了該方法,則其對象再被回收前會調用此方法(僅限於第一次回收該對象時)。
方法區的回收
廢棄的常量:當前系統中沒有一個對象引用此常量。
無用的類:堆中不存在該類的任何實例,該類的類加載器已被回收,該類的Class對象沒有在任何位置被引用。
垃圾收集算法
1、JVM整體上是採用“分代收集算法”。
根據對象的存活週期的不同將Java堆分為新生代和老年代。
新生代又分為Eden區,Survivor From區,Survivor To區,其大小比例默認為8:1:1。
Java的方法區被定義為永久代
2、對於新生代,一般採用“複製算法”。因為新生代的存活時間相對較短,複製的時候不會複製太多對象,所以整體效率不至於太低。
當從Eden和Survivor From區向Survivor To區複製時,若Suvivor To區的空間不夠,則需要依賴老年代進行“分配擔保”。
3、對於老年代,一般採用“標記-清除”(容易產生內存碎片)或“標記-整理”算法。因為老年代的對象存活率較高,若仍是採用複製操作,則需要複製的對象太多,效率會很低。
4、Stop The World
在判斷對象是否應該被回收時,是通過GCRoots來判斷的。
當枚舉GCRoots時,不可以出現在分析過程中,對象的引用關係還在不斷髮生變化,所以這時候必須停頓所有線程,這種現象稱為“Stop The World”。
5、安全點和安全區域
安全區域是指:在這段代碼片段內,引用關係不會發生變化。
6、內存分配和回收策略
對象優先在Eden區分配,若內存不夠則進行一次Minor GC,將Eden區的活躍對象複製到Survivor To區。
若Survivor To區大小足夠,則將其中的存活對象的GC年齡加1,並判斷是否應該晉升到老年代區。
若Survivor To區大小不夠,則進行分配擔保,將對象複製到老年代。
若此時老年代內存大小不夠,則進行一次Full GC。
垃圾收集器
Serial:新生代收集器,單線程收集器,採用複製算法。
ParNew:新生代收集器,多線程收集器,採用複製算法。
Parallel Scavenge:新生代收集器,多線程收集器,側重於提高程序運行的吞吐量。
Serial Old:老年代收集器,單線程收集器,採用標記-整理算法。
Parallel Old:老年代收集器,多線程收集器,採用標記-整理算法。
由於多線程的老年代收集器可以充分利用服務器多CPU的處理能力,所以常用Parallel Scavenge/Parallel Old組合,亦提高了吞吐量。
CMS(Concurrent Mark Sweep)
老年代收集器
初始標記-->併發標記-->重新標記-->併發清除
G1收集器
初始標記-->併發標記-->最終標記-->篩選回收
二、執行子系統
(一)、類文件結構
1、平臺無關性和語言無關性
平臺無關性:通過Java虛擬機,Java代碼可以運行在不同的操作系統上。
語言無關性:不同的語言通過編譯成字節碼,均可以運行在JVM上。
2、Class文件結構
(1)、 兩種數據類型:無符號數,表。
(2)、魔數:“CAFEBABE”
(3)、次版本號,主版本號
(4)、常量池:常量個數,常量項(類型tag+內容)。包括字面量和符號引用。
(5)、訪問標誌:是否 public,final,super,interface,abstract,synthetic,annotation,enum
(6)、類索引,父類索引,接口索引
(7)、字段表
(8)、方法表
(9)、屬性表
3、字節碼指令
(1)、字節碼中的數據類型:byte、short、int、long、float、double、char、reference
(2)、加載和存儲指令:將數據加載到操作數棧,將數據存儲到局部變量表
(3)、運算指令:加減乘除、取餘、取反、位移、位運算(與、或、異或)、局部變量自增、比較
(4)、類型轉換
(5)、對象創建和訪問
(6)、操作數棧相關指令
(7)、控制轉移指令:條件分支等
(8)、方法調用和返回:
invokevirtual(實例方法)
invokeinterface(接口方法)
invokespecial(構造方法,私有方法,父類方法)
invokestatic(靜態方法)
(9)、同步指令:monitorenter、monitorexit。通過管程實現,用於支持synchronized關鍵字。
(二)、類加載機制
1、類在什麼時候會加載
(1)、new(使用new實例化對象的時候),getstatic(讀取一個類的靜態字段,不包括final的),putstatic(設置一個類的靜態字段,不包括final的),invokestatic(調用一個類的靜態方法時)。
(2)、通過java.lang.reflect包的方法,對類進行反射調用的時候,若類未初始化,則會對其進行初始化。
(3)、當初始化一個類時,若其父類還未初始化,則先初始化其父類。
(4)、虛擬機啟動時,會先初始化包含main方法的那個類,即主類。
(5)、被動引用不會導致類加載,比如:通過子類引用父類的靜態字段,不會導致子類初始化;定義某類的數組,則該類不會初始化;引用某類的靜態常量,則該類不會初始化。
2、類加載的過程
(1)、加載
通過類的全限定類名獲取二進制流;將字節流轉換為方法區內的運行時數據結構;在內存中生成一個代表該類的Class對象,作為方法區裡這個類的各種訪問數據的入口。
(2)、驗證
校驗Class文件中的信息是否複合JVM的要求。
文件格式驗證(基於二進制字節流,校驗主次版本號是否支持等);
元數據驗證(是否繼承的final類,是否實現了接口的所有方法等);
字節碼驗證(通過數據流和控制流分析,驗證程序語義,比如只能父類引用指向子類對象,子類引用不能指向父類對象);
符號引用驗證(當JVM將符號引用轉換為直接引用時,會檢查是否能根據名稱找到相應的類,方法,字段等);
驗證階段其實不是必須的,如果該字節碼被反覆驗證過,其實可以關閉驗證。
(3)、準備
為靜態變量設置初始值(int為0,reference為null等);
為常量設置初始值。
(4)、解析
將常量池裡的符號引用解析為直接引用(即指向內存中某個區域的指針)。
解析的符號引用有:類或接口、字段、方法、接口方法等。
(5)、初始化
若父類沒有加載,則先加載父類;
然後為靜態變量設置初始值,執行靜態代碼塊等;
3、類加載器
(1)、每個類,都要由“加載它的類加載器”和“這個類本身”一塊確定該類在虛擬機裡的唯一性。
(2)、類加載器種類
啟動類加載器:加載<JAVA_HOME>/lib目錄中的類;
擴展類加載器:加載<JAVA_HOME>/lib/ext目錄中的類;
應用程序類加載器:加載CLASSPATH中的類,如果應用程序沒有自定義過自己的類加載器,這個便是程序中默認的類加載器。
(3)、雙親委派模型
當一個類加載器加載類的時候,首先不會親自加載這個類,而是會把這個請求委派給父加載器去加載,如此遞歸下去,所有的請求最終會傳到頂層的引啟動類加載器。只有當父加載器無法加載該類時,才會讓子類去嘗試加載。
這樣做可以保證,同一個類在虛擬機中不會被不同的類加載器加載很多次。
(三)、字節碼執行引擎
1、運行時棧幀結構
程序執行時,內存中的棧,裡面是一個一個的棧幀。每一個方法的調用及其執行,都對應著一個棧幀。
2、方法調用
(1)、方法調用指令
invokestatic:調用靜態方法
invokespecial:調用實例構造器、私有方法、父類方法
invokesvirtual:調用所有的虛方法(非static非final方法)
invokeinterface:調用接口方法
(2)、分派
靜態分派:對應於方法參數上的重載。編譯器在重載時,是通過參數的靜態類型,而不是實際類型作為判斷依據的。
動態分派:對應於多態。當invokespecial指令執行時,第一步就是在運行期確定接受者的實際類型。
三、代碼優化
編譯期優化
(1)、編譯過程
詞法分析,語法分析-->
填充符號表-->
處理註解-->
語義分析(標註檢查、數據及控制流分析、解語法糖)-->
字節碼生成。
(2)、Java的語法糖
泛型與類型擦除
自動裝箱、拆箱
foreach循環
變長參數
注意:1、JVM在字節碼裡,用Signature屬性存儲了方法在字節碼層面的方法簽名,通過這項元數據,可以通過反射獲取類的泛型信息。2、包裝類的“==”運算,在不遇到算數運算時不會自動拆箱,這時候比較的是引用是否相等。
運行期優化
(1)JIT編譯器(Just In Time)
Java程序最初是通過解釋執行的,當虛擬機發現“某個方法或某段代碼塊
”運行特別頻繁時,會把這些代碼認定為“熱點代碼(Hot Spot Code)”。為了提高熱點代碼的執行效率,在運行時,會把這些代碼編譯為與本地平臺相關的機器碼,並進行各種優化。完成這個任務的編譯器叫做即時編譯器。
(2)、解釋器與編譯器並存
1、編譯對象
被多次調用的方法:將整個方法作為編譯對象。
被多次執行的循環體:依然將整個方法作為編譯對象,進行棧上替換(On Stack Replacement),即替換棧幀。
2、熱點探測的方式
基於採樣:週期性的檢查棧頂,若某方法經常出現在棧頂,則其是熱點方法。
基於計數器:統計方法執行次數,到了一定的閾值,則其是熱點方法。
3、兩種計數器
JVM的熱點探測是基於計數器的,有兩種計數器:方法調用計數器和回邊計數器。
方法調用計數器:即統計方法執行次數。
回邊計數器:循環體中代碼執行的次數。“回邊”,即在字節碼中遇到控制流向後跳轉的指令。
4、編譯優化技術
公共子表達式消除、數組邊界檢查消除、方法內聯、逃逸分析等。
四、併發
1、JVM內存模型和線程
(1)、JVM內存模型
1、JVM內存模型主要是定義程序中變量的訪問規則,即在虛擬機中將變量存儲到內存,和從內存中取出變量這樣的細節。這些變量指的是實例字段、靜態字段等,不包括局部變量(因為局部變量是線程私有的,不被共享,不存在競爭問題)。
2、工作內存中保存了該線程使用到的變量的在主存中的拷貝。線程對變量的所有操作(讀取,賦值)都必須在工作內存中進行,不能直接讀寫主存中的變量。
3、Java內存模型中的工作內存只是個抽象概念,並不真實存在,它涵蓋了緩存、寫緩衝區、寄存器以及其它的硬件和編譯器優化。
4、內存間的交互操作
lock、unlock
read、load、use、assign、store、write
5、volatile變量
6、原子性、可見性、有序性
7、happens-before原則
(2)、線程
1、線程的實現方式
三種方式:1:1、1:N、N:M。
Java是通過將線程映射到操作系統的線程上去實現的。
2、線程的調度方式
兩種方式:協同式線程調度、搶佔式線程調度。
Java中使用的是搶佔式線程調度。
3、線程的狀態轉換
1、線程安全和鎖優化
線程安全
(1)、共享數據的種類:
1、不可變:不可變對象是值“對象中帶有狀態的變量都聲明為final”。
2、絕對線程安全
3、相對線程安全:Vector、Hashtable等
4、線程兼容:ArrayList、HashMap
5、線程對立
(2)、線程安全的實現方法
1、互斥同步(Mutual Exclution & Synchronization)
同步是指在多個線程併發訪問共享數據時,保證共享數據在同一時刻只被一個(一些)線程使用。
互斥是實現同步的一種手段,臨界區、互斥量、信號量都是互斥的實現方式。
互斥是因,同步是果;互斥是方法,同步是目的。
互斥同步又稱為“阻塞同步”,屬於一種悲觀的併發策略。
2、非阻塞同步
非阻塞同步是一種基於“衝突檢測”的樂觀併發策略;
即先進行操作,如果“沒有其它線程爭用共享數據”,那操作就成功了。如果共享數據有爭用,產生了衝突,那就再採取其它的補償措施(比如不斷重試,直到成功為止)。
因為這種策略不需要將線程掛起,所以成為非阻塞同步。
“操作和衝突檢測”這兩個步驟,需要具備原子性,這可以通過硬件的CAS來實現。
CAS指令需要三個操作數:內存位置,舊的預期值,新值。當且僅當“內存位置的值”符合“舊的預期值”時,處理器用“新值”更新“內存位置的值”,否則就不更新。最終無論是否更新,均會返回“舊值”。
3、不須同步
可重入代碼:不依賴堆上的數據和共用的系統資源,用到的狀態量都由參數中傳入,不調用非可重入的方法。如果一個方法,它的返回結果可預測,相同的輸入,均能返回相同的輸出,它便是可重入的。
線程本地存儲:即ThreadLocal,以當前線程哈希碼為鍵,某變量位置的一個鍵值對。
鎖優化
1、自旋鎖
因為當線程阻塞,或從阻塞中恢復時,掛起線程和恢復線程的操作都需要轉入到內核態去完成,這給系統的性能帶來了很大壓力。
如果某個鎖被佔用的時間很短,這時候可以讓“後面那個請求鎖的線程”稍微等一下,不放棄CPU的執行時間,執行一個忙循環,直到獲得鎖。
2、鎖消除
將一些代碼上進行了同步,但實際不會存在共享數據競爭的鎖進行消除。
3、鎖粗化
如果對一個對象反覆加鎖和解鎖,甚至這個對象在循環體內,則可以把鎖加到循環體外,這樣可以消除反覆加解鎖所帶來的損耗。
4、輕量級鎖
5、偏向鎖
學習Java的同學注意了!!!
學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流群,群號碼:495273252【長按複製】我們一起學Java!