JAVA內存區域
線程私有的包括:
程序計數器
- 若正在執行的是java方法,則計數器記錄的是正在執行的字節碼指令的地址
- 若正在執行的是native方法,則計數器爲空
- 該區域是唯一一個不會導致OutOfMemoryError的區域
虛擬機棧
- 描述的是Java方法執行的內存模型:每個方法都會創建一個棧幀用於存儲局部變量表,操作數棧,動態鏈接,方法出口等信息
- 局部變量表存放了編譯期可知的基本數據類型,對象引用,和returnAddress類型(指向一條字節碼指令地址),局部變量表的內存空間在編譯器確定,在運行期不變
- 可導致兩種異常:線程請求的棧深度大於虛擬機允許的深度-StackOverFlowError;虛擬機無法申請到足夠的內存-OutOfMemoryError
本地方法棧
- 和虛擬機棧類似,但它是爲Native方法服務的
線程共享的包括:
堆
- java堆是被所有線程共享的內存區域,在虛擬機啓動時創建,用來分配對象實例和數組
- 堆是垃圾回收器主要管理的區域,可分爲新生代和老年代,新生代分爲有Eden 空間、From Survivor空間、To Survivor空間
- 大小可通過 -Xmx 和 -Xms 控制
方法區
- 用來存放虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等信息
- GC會回收該區域的常量池和進行類型的卸載
運行時常量池
- Class文件的常量池用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放在運行時常量池中
- 還把翻譯出來的直接引用也放在運行時常量池中,運行時產生的常量也放在裏面
直接內存
- 並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,是NIO使用Native函數庫直接分配堆外內存
HotSpot虛擬機在Java堆中對象分配、 佈局和訪問的全過程
對象創建
- 虛擬機遇到一條new指令時, 首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程
- 內存分配
指針碰撞
如果JVM的垃圾收集器採用複製算法或標記-整理算法,那麼堆中空閒內存是完整的區域,並且空閒內存和已使用內存之間由一個指針標記。那麼當爲一個對象分配內存時,只需移動指針即可。因此,這種在完整空閒區域上通過移動指針來分配內存的方式就叫做「指針碰撞」。
空閒列表
如果JVM的垃圾收集器採用標記-清除算法,那麼堆中空閒區域和已使用區域交錯,因此需要用一張「空閒列表」來記錄堆中哪些區域是空閒區域,從而在創建對象的時候根據這張「空閒列表」找到空閒區域,並分配內存。
安全性
同步
虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性
本地線程分配緩衝(TLAB)
把內存分配的動作按照線程劃分爲在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存(TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配。只有TLAB用完並分配新的TLAB時,才需要同步鎖定。
- 內存空間初始化
- 對對象進行必要的設置(例如這個對象是哪個類的實例、如何才能找到 類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息)
- 執行init,完成
對象的內存佈局
- 對象頭
第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向 線程ID、偏向時間戳等
另外一部分是類型指針,即對象指向它的類元數據的指針, 虛擬機通過這個指針來確定這個對象是哪個類的實例實例數據
- 實例數據
是對象真正存儲的有效信息,也是在程序代碼中所定義的各種 類型的字段內容。 無論是從父類繼承下來的,還是在子類中 定義的,都需要記錄起來對齊填充
- 對齊補充
對象的訪問定位
使用句柄
直接指針(Sun HotSpot)
垃圾收集和內存分配
引用計數法
- 思想:給對象設置引用計數器,每引用該對象一次,計數器就+1,引用失效時,計數器就-1,當任意時候引用計數器的值都爲0時,則該對象可被回收
- Java不適用原因:無法解決對象互相循環引用的問題
可達性分析法
以GC Roots爲起點,從這些起點開始向下搜索,經過的路徑稱爲引用鏈。若一個對象到GC Roots之間沒有任何引用鏈,則該對象是不可達的。
可作爲GC Roots的對象有
- 虛擬機棧(棧幀中的局部變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(Native方法)引用的對象
在可達性分析過程中,對象引用類型會對對象的生命週期產生影響
JAVA中有這幾種類型的引用:
- 強引用:只要該引用還有效,GC就不會回收
- 軟引用:內存空間足夠時不進行回收,在內存溢出發生前進行回收、用SoftReference類實現
- 弱引用:弱引用關聯的對象只能存活到下一次Gc收集、用WeakReference類實現
- 虛引用:無法通過虛引用獲得對象實例,也不會對對象的生存時間產生影響、唯一目的:當該對象被Gc收集時,收到一個系統通知。用PhantomReference類實現
一個對象真正不可用,要經歷兩次標記過程:
- 首先進行可達性分析,篩選出與GC Roots沒用引用鏈的對象,進行第一次標記和篩選,篩選條件是是否有必要執行finalize()方法。若對象有沒有重寫finalize()方法,或者finalize()是否已被jvm調用過,則沒必要執行,GC會回收該對象,若有必要執行,則該對象會被放入F-Queue中,由jvm開啓一個低優先級的線程去執行它(但不一定等待finalize執行完畢)
- 第一次標記後,GC將對F-Queue中的對象進行第二次標記,Finalize()是對象最後一次自救的機會,若對象在finalize()中重新加入到引用鏈中,則它會被移出要回收的對象的集合,其他對象則會被第二次標記,進行回收
JAVA中的垃圾回收算法有:
標記-清除(Mark-Sweep)
兩個階段:標記, 清除
缺點:兩個階段的效率都不高;容易產生大量的內存碎片
複製(Copying)
把內存分成大小相同的兩塊,當一塊的內存用完了,就把可用對象複製到另一塊上,將使用過的一塊一次性清理掉
缺點:浪費了一半內存
標記-整理(Mark-Compact)
標記後,讓所有存活的對象移到一端,然後直接清理掉端邊界以外的內存
分代收集
把堆分爲新生代和老年代
新生代使用複製算法
將新生代內存分爲一塊大的Eden區和兩塊小的Survivor;每次使用Eden和一個Survivor,回收時將Eden和Survivor存活的對象複製到另一個Survivor(HotSpot的比例Eden:Survivor = 8:1)
老年代使用標記-清理或者標記-整理
HotSpot的算法實現
枚舉根節點
OopMap數據結構記錄哪些位置是引用
安全點
哪些位置可以生產OopMap
以是否具有讓程序長時間執行的特徵爲標準進行選定
搶先式中斷(不採用)和主動式中斷
安全區域
一段代碼片段中,引用不會發生變化
垃圾收集器:
Serial(串行收集器)
特性:單線程,stop the world,採用複製算法,簡單高效
應用場景:在Client模式下默認的新生代收集器
ParNew
特點:是Serial的多線程版本,採用複製算法
應用場景:在Server模式下常用的新生代收集器,可與CMS配合工作
Parallel Scavenge
特點:並行的多線程收集器,採用複製算法,吞吐量優先,有自適應調節策略
應用場景:需要吞吐量大的時候
SerialOld
特點:Serial的老年代版本,單線程,使用標記-整理算法
Parallel Old
Parallel Scavenge的老年代版本,多線程,標記-整理算法
CMS
處理過程:
- 初始標記:stop the world 標記GC Roots能直接關聯到的對象
- 併發標記:進行GC Roots Tracing
- 重新標記:stop the world;修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄
- 併發清除:清除對象
特點:以最短回收停頓時間爲目標,使用標記-清除算法
優點:併發收集,低停頓
缺點:
- 對CPU資源敏感
- 無法處理浮動垃圾(併發清除時,用戶線程仍在運行,此時產生的垃圾爲浮動垃圾)
- 產生大量的空間碎片
G1
面向服務端應用,將整個堆劃分爲大小相同的region。
處理過程:
- 初始標記:stop the world 標記GC Roots能直接關聯到的對象
- 併發標記:可達性分析
- 最終標記:修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的一部分標記記錄
- 篩選回收:篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃
特點
- 並行與併發
- 分代收集
- 空間整合:從整體看是基於「標記-整理」的,從局部(兩個region之間)看是基於「複製」的。
- 可預測的停頓:使用者可明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
內存分配規則
觸發GC又涉及到了內存分配規則:(對象主要分配在Eden,若啓動了本地線程分配緩衝,將優先在TLAB上分配)
- 對象優先在Eden分配
當Eden區沒有足夠的空間時就會發起一次Minor GC
- 大對象直接進入老年代
典型的大對象是很長的字符串和數組
- 長期存活的對象進入老年代
每個對象有年齡計數器,每經過一次GC,計數器值加一,當到達一定程度時(默認15),就會進入老年代,年齡的閾值可通過參數 -XX:MaxTenuringThreshold設置
對象年齡的判定
Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於等於該年齡的對象就可直接進入老年代,無須等到MaxTenuringThreshold要求的年齡
- 空間分配擔保
發生Minor GC前,jvm會檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,若大於,則Minor GC是安全的,若不大於,jvm會查看HandlePromotionFailure是否允許擔保失敗,若不允許,則改爲一次Full GC,若允許擔保失敗,則檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若大於,則嘗試進行Minor GC;若小於,則要改爲Full GC
最後提一下也會回收方法區:
永久代中主要回收兩部分內容:廢棄常量和無用的類
廢棄常量回收和對象的回收類似
無用的類需滿足3個條件
- 該類的所有實例對象已被回收
- 加載該類的ClassLoader已被回收
- 該類的Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
JVM常用命令
JDK命令行工具
Jps
查看虛擬機進程
Jstat
監視虛擬機各種運行狀態信息
Jinfo
查看和調整虛擬機各項參數
Jmap
生產堆轉儲快照(一般稱爲heapdump或dump文件)
查詢finalize執行隊列、Java堆和永久代的詳細信息,如空間使用率、當前用的是哪種收集器
Jhat
分析堆轉儲快照
Jstack
生成虛擬機當前時刻的線程快照(一般稱爲threaddump或者javacore文件)
舉例:
高內存分析:
top 得到pid
jmap -histo:live pid
jmap -dump:live,format=b,file=xxx.xxx [pid]
高CPU分析:
top 得到pid
ps -mp pid -o THREAD,tid,ttime 得到線程id
printf "%x\n" 線程id 得到轉換後線程id
jstack pid| grep 轉換後線程id -A n
可視化工具
Jconsole
VisualVM
目前爲止JDK發佈的功能最強大的運行監視和故障處理工具
對應用程序的實際性能影響很小,可以直接應用在生產環境中
插件形式擴展
類文件結構
無關性
- 平臺無關性
- 語言無關性
Class類文件結構
Class文件是一組以8位字節爲基礎單位的二進制流,採用一種類似於C語言結構體的僞結構來存儲數據,這種僞結構有兩種數據類型:
- 無符號數
基本的數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節、8個字節
- 表
由多個無符號數或者其他表作爲數據項構成的複合數據類型
具體構成:
魔數:前4個字節,0xCAFEBABE
次版本號:5~6字節
主版本號:7~8字節
常量池:
- 入口有一個u2類型的數據,代表常量池的容量
- 主要存放字面量和符號引用
- 字面量:文本字符串、申明爲final的常量值等
- 符號引用:類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符
- 每一個常量都是一個表,開始的第一位是一個u1類型的標誌位,代表屬於哪種常量類型,共有14中結構各不相同的表結構數據
- Javap –verbose *.class
訪問標誌:常量池後的兩個字節,用於識別一些類或者接口層次的訪問信息,包括:這個 Class是類還是接口;是否定義爲public類型;是否定義爲abstract類型;如果是類的話, 是否被聲明爲final等
類索引:u2類型的數據
父類索引:u2類型的數據
接口索引集合:第一項爲u2類型的數據,代表索引表的容量
字段表集合:
方法表集合:
屬性表集合:
字節碼指令
虛擬機操作碼爲一個字節,即最多操作碼總數不可能超過256條
類加載機制
類加載的時機
什麼時候對類進行初始化?
1、遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new 關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
2、使用java.lang. reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
3、當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父 類的初始化。
4、當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
5、當使用JDK 1. 7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_ getStatic、REF_ putStatic、REF_ invokeStatic的方法句柄,並且這個方法 句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
特殊:
1、通過子類引用父類的靜態字段,不會導致子類初始化
2、通過數組定義來引用類,不會觸發此類的初始化
3、 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
接口初始化和類的區別點:
當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。
類加載的過程
加載
1、通過一個類的全限定名來獲取定義此類的二進制字節流。
2、將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時運行時數據結構。
3、在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。
驗證
文件格式驗證
元數據驗證
字節碼驗證
符號引用驗證
準備
是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。
解析
是虛擬機將常量池內的符號引用替換爲直接引用的過程
初始化
- 執行類構造器<clinit>()方法的過程
- <clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但是不能訪問
- 由於父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操作
- <clinit>()方法對於類或接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不爲這個類生成<clinit>()方法。
- 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。
- 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,從而保證只執行一次
類加載器
- 兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義,這裏所指的「相等」,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關係判定等情況。
- 雙親委派模型
啓動類加載器:負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。
擴展類加載器:負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫
應用程序類加載器:負責加載用戶類路徑(ClassPath)上所指定的類庫
工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。
程序編譯與代碼優化
編譯期的優化
完成了從程序到抽象語法樹或中間字節碼的生成
編譯器種類:
前端編譯器 如:javac
JIT編譯器 如:HotSpot VM的C1、C2
AOT編譯器
javac
把*.java文件轉變爲*.class文件的過程,對代碼運行效率幾乎沒有優化,相當多新生的Java語法特性都是靠編譯器的語法糖來實現的,由java語言編寫的程序
實現過程:
解析與填充符號表->註解處理器->語義分析與字節碼生成
語法糖
- 泛型與類型擦除
- 自動裝箱、拆箱和遍歷循環
- 條件編譯
其它:變長參數、內部類、枚舉類、斷言語句、對枚舉和字符串(在JDK1.7中支持)的switch支持、try語句中定義和關閉資源(在JDK1.7中支持)等
註解處理器:
繼承AbstractProcessor
類似處理:Hibernate Validator、Lombok
運行期的優化
Hotspot JVM:
解釋器和編譯器並存
有兩個即時編譯器:Cilent Compiler、Server Compiler
java -version mixed mode 混合模式;java -Xint -version 解釋模式;java -Xcomp version 編譯模式(已作廢)
分層編譯策略
第0層,程序解釋執行,解釋器不開啓性能監控功能(Profiling),可觸發第1層編譯。
第1層,也稱爲C1編譯,將字節碼編譯爲本地代碼,進行簡單、可靠的優化,如有必要將加入性能監控的邏輯。
第2層(或2層以上),也稱爲C2編譯,也是將字節碼編譯爲本地代碼,但是會啓用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。
如何觸發即時編譯:
熱點代碼
被多次調用的方法、被多次執行的循環體
熱點探測
基於採樣的熱點探測:簡單、高效,容易因爲受到線程阻塞或別的外界因素的影響而擾亂熱點探測。
基於計數器的熱點探測:精確、嚴謹,但相對複雜,需要建立並維護計數器(採用)
Hotspot採用基於計數器的熱點探測
方法調用計數器
這個計數器就用於統計方法被調用的次數,它的默認閾值在Client模式下是1500次,在Server模式下是10000次,這個閾值可以通過虛擬機參數-XX:CompileThreshold來人爲設定;有熱度衰減,可以使用虛擬機參數-XX:-UseCounterDecay來關閉熱度衰減,可以使用-XX:CounterHalfLifeTime參數設置半衰週期的時間,單位是秒。
回邊計數器
統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令稱爲「回邊」(BackEdge)。
編譯過程(執行各種優化)
方法內聯
冗餘訪問消除(公共子表達式消除)
複寫傳播
無用代碼消除
數組邊界檢查消除
逃逸分析:方法逃逸、線程逃逸
棧上分配、同步消除、標量替換
併發高效
Java內存模型與線程
內存模型
volatile變量
保證變量對所有線程的可見性
禁止指令重排序優化
線程內表現爲串行的語義
內存屏障:指令重排序時不能把後面的指令重排序到內存屏障之前
不是線程安全的
long和double型變量的非原子協定,但商業虛擬機都保證了原子性
原子性、可見性、有序性
先行發生原則
Java與線程
線程的實現:使用內核線程實現(採用),使用用戶線程實現,使用用戶線程加輕量級進程混合實現
線程調度:協同式(簡單但不穩定)、搶佔式(採用)
線程狀態轉換:
線程安全與鎖優化
線程安全
不可變、絕對線程安全、相對線程安全、線程兼容、線程對立
線程安全的實現方法
互斥同步
synchronized:可重入
j.u.c下:
Reentrantlock:可重入、等待可中斷、可實現公平鎖、可綁定多條件
Condition:
CountDownLatch:
非阻塞同步
CAS:指令需要有3個操作數,分別是內存位置(在Java中可以簡單理解爲變量的內存地址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則它就不執行更新,但是無論是否更新了V的值,都會返回V的舊值,上述的處理過程是一個原子操作。
鎖優化
自旋鎖與自適應自旋
默認開啓自旋
-XX:PreBlockSpin自旋次數
鎖消除
鎖粗化
輕量級鎖:是在無競爭的情況下使用CAS操作去消除同步使用的互斥量
偏向鎖:是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了