什么是JVM?
JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的,由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成,JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機上運行的目標代碼(位元組碼),就可在多種平臺上不加修改的運行,這也是Java能夠“一次編譯,到處運行的”原因,
講一下JVM記憶體結構?
JVM記憶體結構分為5大區域,程式計數器、虛擬機堆疊、本地方法堆疊、堆、方法區,最全面的Java面試網站
程式計數器
執行緒私有的,作為當前執行緒的行號指示器,用于記錄當前虛擬機正在執行的執行緒指令地址,程式計數器主要有兩個作用:
- 當前執行緒所執行的位元組碼的行號指示器,通過它實作代碼的流程控制,如:順序執行、選擇、回圈、例外處理,
- 在多執行緒的情況下,程式計數器用于記錄當前執行緒執行的位置,當執行緒被切換回來的時候能夠知道它上次執行的位置,
程式計數器是唯一一個不會出現 OutOfMemoryError
的記憶體區域,它的生命周期隨著執行緒的創建而創建,隨著執行緒的結束而死亡,
本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~
Github地址
如果訪問不了Github,可以訪問gitee地址,
gitee地址
虛擬機堆疊
Java 虛擬機堆疊是由一個個堆疊幀組成,而每個堆疊幀中都擁有:區域變數表、運算元堆疊、動態鏈接、方法出口資訊,每一次函式呼叫都會有一個對應的堆疊幀被壓入虛擬機堆疊,每一個函式呼叫結束后,都會有一個堆疊幀被彈出,
區域變數表是用于存放方法引數和方法內的區域變數,
每個堆疊幀都包含一個指向運行時常量池中該堆疊所屬方法的符號參考,在方法呼叫程序中,會進行動態鏈接,將這個符號參考轉化為直接參考,
- 部分符號參考在類加載階段的時候就轉化為直接參考,這種轉化就是靜態鏈接
- 部分符號參考在運行期間轉化為直接參考,這種轉化就是動態鏈接
Java 虛擬機堆疊也是執行緒私有的,每個執行緒都有各自的 Java 虛擬機堆疊,而且隨著執行緒的創建而創建,隨著執行緒的死亡而死亡,Java 虛擬機堆疊會出現兩種錯誤:StackOverFlowError
和 OutOfMemoryError
,
可以通過-Xss
引數來指定每個執行緒的虛擬機堆疊記憶體大小:
java -Xss2M
本地方法堆疊
虛擬機堆疊為虛擬機執行 Java
方法服務,而本地方法堆疊則為虛擬機使用到的 Native
方法服務,Native
方法一般是用其它語言(C、C++等)撰寫的,
本地方法被執行的時候,在本地方法堆疊也會創建一個堆疊幀,用于存放該本地方法的區域變數表、運算元堆疊、動態鏈接、出口資訊,
堆
堆用于存放物件實體,是垃圾收集器管理的主要區域,因此也被稱作GC
堆,堆可以細分為:新生代(Eden
空間、From Survivor
、To Survivor
空間)和老年代,
通過 -Xms
設定程式啟動時占用記憶體大小,通過-Xmx
設定程式運行期間最大可占用的記憶體大小,如果程式運行需要占用更多的記憶體,超出了這個設定值,就會拋出OutOfMemory
例外,
java -Xms1M -Xmx2M
1.方法區
方法區與 Java 堆一樣,是各個執行緒共享的記憶體區域,它用于存盤已被虛擬機加載的類資訊、常量、靜態變數、即時編譯器編譯后的代碼等資料,
對方法區進行垃圾回收的主要目標是對常量池的回收和對類的卸載,
2.永久代
方法區是 JVM 的規范,而永久代PermGen
是方法區的一種實作方式,并且只有 HotSpot
有永久代,對于其他型別的虛擬機,如JRockit
沒有永久代,由于方法區主要存盤類的相關資訊,所以對于動態生成類的場景比較容易出現永久代的記憶體溢位,
3.元空間
JDK 1.8 的時候,HotSpot
的永久代被徹底移除了,使用元空間替代,元空間的本質和永久代類似,都是對JVM規范中方法區的實作,兩者最大的區別在于:元空間并不在虛擬機中,而是使用直接記憶體,
為什么要將永久代替換為元空間呢?
永久代記憶體受限于 JVM 可用記憶體,而元空間使用的是直接記憶體,受本機可用記憶體的限制,雖然元空間仍舊可能溢位,但是相比永久代記憶體溢位的概率更小,
運行時常量池
運行時常量池是方法區的一部分,在類加載之后,會將編譯器生成的各種字面量和符號引號放到運行時常量池,在運行期間動態生成的常量,如 String 類的 intern()方法,也會被放入運行時常量池,
直接記憶體
直接記憶體并不是虛擬機運行時資料區的一部分,也不是虛擬機規范中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導致 OutOfMemoryError
錯誤出現,
NIO的Buffer提供了DirectBuffer
,可以直接訪問系統物理記憶體,避免堆內記憶體到堆外記憶體的資料拷貝操作,提高效率,DirectBuffer
直接分配在物理記憶體中,并不占用堆空間,其可申請的最大記憶體受作業系統限制,不受最大堆記憶體的限制,
直接記憶體的讀寫操作比堆記憶體快,可以提升程式I/O操作的性能,通常在I/O通信程序中,會存在堆內記憶體到堆外記憶體的資料拷貝操作,對于需要頻繁進行記憶體間資料拷貝且生命周期較短的暫存資料,都建議存盤到直接記憶體,
好東西應該要分享出來!我把自己學習計算機多年以來的書籍分享出來了,匯總到一個計算機經典編程書籍倉庫了,一共300多本,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演算法、機器學習、編程人生等,可以star一下,下次找書直接在上面搜索,倉庫持續更新中~
Github地址
Java物件的定位方式
Java 程式通過堆疊上的 reference 資料來操作堆上的具體物件,物件的訪問方式由虛擬機實作而定,目前主流的訪問方式有使用句柄和直接指標兩種:
- 如果使用句柄的話,那么 Java 堆中將會劃分出一塊記憶體來作為句柄池,reference 中存盤的就是物件的句柄地址,而句柄中包含了物件實體資料與型別資料各自的具體地址資訊,使用句柄來訪問的最大好處是 reference 中存盤的是穩定的句柄地址,在物件被移動時只會改變句柄中的實體資料指標,而 reference 本身不需要修改,
- 直接指標,reference 中存盤的直接就是物件的地址,物件包含到物件型別資料的指標,通過這個指標可以訪問物件型別資料,使用直接指標訪問方式最大的好處就是訪問物件速度快,它節省了一次指標定位的時間開銷,虛擬機hotspot主要是使用直接指標來訪問物件,
說一下堆疊的區別?
-
堆的物理地址分配是不連續的,性能較慢;堆疊的物理地址分配是連續的,性能相對較快,
-
堆存放的是物件的實體和陣列;堆疊存放的是區域變數,運算元堆疊,回傳結果等,
-
堆是執行緒共享的;堆疊是執行緒私有的,
什么情況下會發生堆疊溢位?
- 當執行緒請求的堆疊深度超過了虛擬機允許的最大深度時,會拋出
StackOverFlowError
例外,這種情況通常是因為方法遞回沒終止條件, - 新建執行緒的時候沒有足夠的記憶體去創建對應的虛擬機堆疊,虛擬機會拋出
OutOfMemoryError
例外,比如執行緒啟動過多就會出現這種情況,
類檔案結構
Class 檔案結構如下:
ClassFile {
u4 magic; //類檔案的標志
u2 minor_version;//小版本號
u2 major_version;//大版本號
u2 constant_pool_count;//常量池的數量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//類的訪問標記
u2 this_class;//當前類的索引
u2 super_class;//父類
u2 interfaces_count;//介面
u2 interfaces[interfaces_count];//一個類可以實作多個介面
u2 fields_count;//欄位屬性
field_info fields[fields_count];//一個類會可以有個欄位
u2 methods_count;//方法數量
method_info methods[methods_count];//一個類可以有個多個方法
u2 attributes_count;//此類的屬性表中的屬性數
attribute_info attributes[attributes_count];//屬性表集合
}
主要引數如下:
魔數:class
檔案標志,
檔案版本:高版本的 Java 虛擬機可以執行低版本編譯器生成的類檔案,但是低版本的 Java 虛擬機不能執行高版本編譯器生成的類檔案,
常量池:存放字面量和符號參考,字面量類似于 Java 的常量,如字串,宣告為final
的常量值等,符號參考包含三類:類和介面的全限定名,方法的名稱和描述符,欄位的名稱和描述符,
訪問標志:識別類或者介面的訪問資訊,比如這個Class
是類還是介面,是否為 public
或者 abstract
型別等等,
當前類的索引:類索參考于確定這個類的全限定名,
什么是類加載?類加載的程序?
類的加載指的是將類的class
檔案中的二進制資料讀入到記憶體中,將其放在運行時資料區的方法區內,然后在堆區創建一個此類的物件,通過這個物件可以訪問到方法區對應的類資訊,
加載
- 通過類的全限定名獲取定義此類的二進制位元組流
- 將位元組流所代表的靜態存盤結構轉換為方法區的運行時資料結構
- 在記憶體中生成一個代表該類的
Class
物件,作為方法區類資訊的訪問入口
驗證
確保Class檔案的位元組流中包含的資訊符合虛擬機規范,保證在運行后不會危害虛擬機自身的安全,主要包括四種驗證:檔案格式驗證,元資料驗證,位元組碼驗證,符號參考驗證,
準備
為類變數分配記憶體并設定類變數初始值的階段,
決議
虛擬機將常量池內的符號參考替換為直接參考的程序,符號參考用于描述目標,直接參考直接指向目標的地址,
初始化
開始執行類中定義的Java
代碼,初始化階段是呼叫類構造器的程序,
什么是雙親委派模型?
一個類加載器收到一個類的加載請求時,它首先不會自己嘗試去加載它,而是把這個請求委派給父類加載器去完成,這樣層層委派,因此所有的加載請求最終都會傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載,
雙親委派模型的具體實作代碼在 java.lang.ClassLoader
中,此類的 loadClass()
方法運行程序如下:先檢查類是否已經加載過,如果沒有則讓父類加載器去加載,當父類加載器加載失敗時拋出 ClassNotFoundException
,此時嘗試自己去加載,原始碼如下:
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
為什么需要雙親委派模型?
雙親委派模型的好處:可以防止記憶體中出現多份同樣的位元組碼,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶撰寫了一個java.lang.Object
的同名類并放在ClassPath
中,多個類加載器都去加載這個類到記憶體中,系統中將會出現多個不同的Object
類,那么類之間的比較結果及類的唯一性將無法保證,
什么是類加載器,類加載器有哪些?
-
實作通過類的全限定名獲取該類的二進制位元組流的代碼塊叫做類加載器,
主要有一下四種類加載器:
- 啟動類加載器:用來加載 Java 核心類別庫,無法被 Java 程式直接參考,
- 擴展類加載器:它用來加載 Java 的擴展庫,Java 虛擬機的實作會提供一個擴展庫目錄,該類加載器在此目錄里面查找并加載 Java 類,
- 系統類加載器:它根據應用的類路徑來加載 Java 類,可通過
ClassLoader.getSystemClassLoader()
獲取它, - 自定義類加載器:通過繼承
java.lang.ClassLoader
類的方式實作,
類的實體化順序?
- 父類中的
static
代碼塊,當前類的static
代碼塊 - 父類的普通代碼塊
- 父類的建構式
- 當前類普通代碼塊
- 當前類的建構式
如何判斷一個物件是否存活?
對堆垃圾回收前的第一步就是要判斷那些物件已經死亡(即不再被任何途徑參考的物件),判斷物件是否存活有兩種方法:參考計數法和可達性分析,
參考計數法
給物件中添加一個參考計數器,每當有一個地方參考它,計數器就加 1;當參考失效,計數器就減 1;任何時候計數器為 0 的物件就是不可能再被使用的,
這種方法很難解決物件之間相互回圈參考的問題,比如下面的代碼,obj1
和 obj2
互相參考,這種情況下,參考計數器的值都是1,不會被垃圾回收,
public class ReferenceCount {
Object instance = null;
public static void main(String[] args) {
ReferenceCount obj1 = new ReferenceCount();
ReferenceCount obj2 = new ReferenceCount();
obj1.instance = obj2;
obj2.instance = obj1;
obj1 = null;
obj2 = null;
}
}
可達性分析
通過GC Root
物件為起點,從這些節點向下搜索,搜索所走過的路徑叫參考鏈,當一個物件到GC Root
沒有任何的參考鏈相連時,說明這個物件是不可用的,
可作為GC Roots的物件有哪些?
- 虛擬機堆疊中參考的物件
- 本地方法堆疊中Native方法參考的物件
- 方法區中類靜態屬性參考的物件
- 方法區中常量參考的物件
什么情況下類會被卸載?
需要同時滿足以下 3 個條件類才可能會被卸載 :
- 該類所有的實體都已經被回收,
- 加載該類的類加載器已經被回收,
- 該類對應的
java.lang.Class
物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,
虛擬機可以對滿足上述 3 個條件的類進行回收,但不一定會進行回收,
強參考、軟參考、弱參考、虛參考是什么,有什么區別?
強參考:在程式中普遍存在的參考賦值,類似Object obj = new Object()
這種參考關系,只要強參考關系還存在,垃圾收集器就永遠不會回收掉被參考的物件,
軟參考:如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體,
//軟參考
SoftReference<String> softRef = new SoftReference<String>(str);
弱參考:在進行垃圾回收時,不管當前記憶體空間足夠與否,都會回收只具有弱參考的物件,
//弱參考
WeakReference<String> weakRef = new WeakReference<String>(str);
虛參考:虛參考并不會決定物件的生命周期,如果一個物件僅持有虛參考,那么它就和沒有任何參考一樣,在任何時候都可能被垃圾回收,虛參考主要是為了能在物件被收集器回收時收到一個系統通知,
GC是什么?為什么要GC?
GC(Garbage Collection
),垃圾回收,是Java與C++的主要區別之一,作為Java開發者,一般不需要專門撰寫記憶體回收和垃圾清理代碼,這是因為在Java虛擬機中,存在自動記憶體管理和垃圾清理機制,對JVM中的記憶體進行標記,并確定哪些記憶體需要回收,根據一定的回收策略,自動的回收記憶體,保證JVM中的記憶體空間,防止出現記憶體泄露和溢位問題,
Minor GC 和 Full GC的區別?
-
Minor GC:回收新生代,因為新生代物件存活時間很短,因此
Minor GC
會頻繁執行,執行的速度一般也會比較快, -
Full GC:回收老年代和新生代,老年代的物件存活時間長,因此
Full GC
很少執行,執行速度會比Minor GC
慢很多,
記憶體的分配策略?
物件優先在 Eden 分配
大多數情況下,物件在新生代 Eden
上分配,當 Eden
空間不夠時,觸發 Minor GC
,
大物件直接進入老年代
大物件是指需要連續記憶體空間的物件,最典型的大物件有長字串和大陣列,可以設定JVM引數 -XX:PretenureSizeThreshold
,大于此值的物件直接在老年代分配,
長期存活的物件進入老年代
通過引數 -XX:MaxTenuringThreshold
可以設定物件進入老年代的年齡閾值,物件在Survivor
區每經過一次 Minor GC
,年齡就增加 1 歲,當它的年齡增加到一定程度,就會被晉升到老年代中,
動態物件年齡判定
并非物件的年齡必須達到 MaxTenuringThreshold
才能晉升老年代,如果在 Survivor
中相同年齡所有物件大小的總和大于 Survivor
空間的一半,則年齡大于或等于該年齡的物件可以直接進入老年代,無需達到 MaxTenuringThreshold
年齡閾值,
空間分配擔保
在發生 Minor GC
之前,虛擬機先檢查老年代最大可用的連續空間是否大于新生代所有物件總空間,如果條件成立的話,那么 Minor GC
是安全的,如果不成立的話虛擬機會查看 HandlePromotionFailure
的值是否允許擔保失敗,如果允許,那么就會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代物件的平均大小,如果大于,將嘗試著進行一次 Minor GC
;如果小于,或者 HandlePromotionFailure
的值為不允許擔保失敗,那么就要進行一次 Full GC
,
Full GC 的觸發條件?
對于 Minor GC,其觸發條件比較簡單,當 Eden 空間滿時,就將觸發一次 Minor GC,而 Full GC 觸發條件相對復雜,有以下情況會發生 full GC:
呼叫 System.gc()
只是建議虛擬機執行 Full GC,但是虛擬機不一定真正去執行,不建議使用這種方式,而是讓虛擬機管理記憶體,
老年代空間不足
老年代空間不足的常見場景為前文所講的大物件直接進入老年代、長期存活的物件進入老年代等,為了避免以上原因引起的 Full GC,應當盡量不要創建過大的物件以及陣列、注意編碼規范避免記憶體泄露,除此之外,可以通過 -Xmn
引數調大新生代的大小,讓物件盡量在新生代被回收掉,不進入老年代,還可以通過 -XX:MaxTenuringThreshold
調大物件進入老年代的年齡,讓物件在新生代多存活一段時間,
空間分配擔保失敗
使用復制演算法的 Minor GC 需要老年代的記憶體空間作擔保,如果擔保失敗會執行一次 Full GC,
JDK 1.7 及以前的永久代空間不足
在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區是用永久代實作的,永久代中存放的為一些 Class 的資訊、常量、靜態變數等資料,當系統中要加載的類、反射的類和呼叫的方法較多時,永久代可能會被占滿,在未配置為采用 CMS GC 的情況下也會執行 Full GC,如果經過 Full GC 仍然回收不了,那么虛擬機會拋出 java.lang.OutOfMemoryError
,
垃圾回收演算法有哪些?
垃圾回收演算法有四種,分別是標記清除法、標記整理法、復制演算法、分代收集演算法,
標記清除演算法
首先利用可達性去遍歷記憶體,把存活物件和垃圾物件進行標記,標記結束后統一將所有標記的物件回收掉,這種垃圾回收演算法效率較低,并且會產生大量不連續的空間碎片,
復制清除演算法
半區復制,用于新生代垃圾回收,將記憶體分為大小相同的兩塊,每次使用其中的一塊,當這一塊的記憶體使用完后,就將還存活的物件復制到另一塊去,然后再把使用的空間一次清理掉,
特點:實作簡單,運行高效,但可用記憶體縮小為了原來的一半,浪費空間,
標記整理演算法
根據老年代的特點提出的一種標記演算法,標記程序仍然與標記-清除
演算法一樣,但后續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然后直接清理掉邊界以外的記憶體,
分類收集演算法
根據各個年代的特點采用最適當的收集演算法,
一般將堆分為新生代和老年代,
- 新生代使用復制演算法
- 老年代使用標記清除演算法或者標記整理演算法
在新生代中,每次垃圾收集時都有大批物件死去,只有少量存活,使用復制演算法比較合適,只需要付出少量存活物件的復制成本就可以完成收集,老年代物件存活率高,適合使用標記-清理或者標記-整理演算法進行垃圾回收,
有哪些垃圾回收器?
垃圾回收器主要分為以下幾種:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1
,
這7種垃圾收集器的特點:
收集器 | 串行、并行or并發 | 新生代/老年代 | 演算法 | 目標 | 適用場景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 復制演算法 | 回應速度優先 | 單CPU環境下的Client模式 |
ParNew | 并行 | 新生代 | 復制演算法 | 回應速度優先 | 多CPU環境時在Server模式下與CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 復制演算法 | 吞吐量優先 | 在后臺運算而不需要太多互動的任務 |
Serial Old | 串行 | 老年代 | 標記-整理 | 回應速度優先 | 單CPU環境下的Client模式、CMS的后備預案 |
Parallel Old | 并行 | 老年代 | 標記-整理 | 吞吐量優先 | 在后臺運算而不需要太多互動的任務 |
CMS | 并發 | 老年代 | 標記-清除 | 回應速度優先 | 集中在互聯網站或B/S系統服務端上的Java應用 |
G1 | 并發 | both | 標記-整理+復制演算法 | 回應速度優先 | 面向服務端應用,將來替換CMS |
Serial 收集器
單執行緒收集器,使用一個垃圾收集執行緒去進行垃圾回收,在進行垃圾回收的時候必須暫停其他所有的作業執行緒( Stop The World
),直到它收集結束,
特點:簡單高效;記憶體消耗小;沒有執行緒互動的開銷,單執行緒收集效率高;需暫停所有的作業執行緒,用戶體驗不好,
ParNew 收集器
Serial
收集器的多執行緒版本,除了使用多執行緒進行垃圾收集外,其他行為、引數與 Serial
收集器基本一致,
Parallel Scavenge 收集器
新生代收集器,基于復制清除演算法實作的收集器,特點是吞吐量優先,能夠并行收集的多執行緒收集器,允許多個垃圾回收執行緒同時運行,降低垃圾收集時間,提高吞吐量,所謂吞吐量就是 CPU 中用于運行用戶代碼的時間與 CPU 總消耗時間的比值(吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)
),Parallel Scavenge
收集器關注點是吞吐量,高效率的利用 CPU 資源,CMS
垃圾收集器關注點更多的是用戶執行緒的停頓時間,
Parallel Scavenge
收集器提供了兩個引數用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis
引數以及直接設定吞吐量大小的-XX:GCTimeRatio
引數,
-
-XX:MaxGCPauseMillis
引數的值是一個大于0的毫秒數,收集器將盡量保證記憶體回識訓費的時間不超過用戶設定值, -
-XX:GCTimeRatio
引數的值大于0小于100,即垃圾收集時間占總時間的比率,相當于吞吐量的倒數,
Serial Old 收集器
Serial
收集器的老年代版本,單執行緒收集器,使用標記整理演算法,
Parallel Old 收集器
Parallel Scavenge
收集器的老年代版本,多執行緒垃圾收集,使用標記整理演算法,
CMS 收集器
Concurrent Mark Sweep
,并發標記清除,追求獲取最短停頓時間,實作了讓垃圾收集執行緒與用戶執行緒基本上同時作業,
CMS
垃圾回識訓于標記清除演算法實作,整個程序分為四個步驟:
- 初始標記: 暫停所有用戶執行緒(
Stop The World
),記錄直接與GC Roots
直接相連的物件 , - 并發標記:從
GC Roots
開始對堆中物件進行可達性分析,找出存活物件,耗時較長,但是不需要停頓用戶執行緒, - 重新標記: 在并發標記期間物件的參考關系可能會變化,需要重新進行標記,此階段也會暫停所有用戶執行緒,
- 并發清除:清除標記物件,這個階段也是可以與用戶執行緒同時并發的,
在整個程序中,耗時最長的是并發標記和并發清除階段,這兩個階段垃圾收集執行緒都可以與用戶執行緒一起作業,所以從總體上來說,CMS
收集器的記憶體回收程序是與用戶執行緒一起并發執行的,
優點:并發收集,停頓時間短,
缺點:
- 標記清除演算法導致收集結束有大量空間碎片,
- 產生浮動垃圾,在并發清理階段用戶執行緒還在運行,會不斷有新的垃圾產生,這一部分垃圾出現在標記程序之后,
CMS
無法在當次收集中回收它們,只好等到下一次垃圾回收再處理;
G1收集器
G1垃圾收集器的目標是在不同應用場景中追求高吞吐量和低停頓之間的最佳平衡,
G1將整個堆分成相同大小的磁區(Region
),有四種不同型別的磁區:Eden、Survivor、Old和Humongous
,磁區的大小取值范圍為 1M 到 32M,都是2的冪次方,磁區大小可以通過-XX:G1HeapRegionSize
引數指定,Humongous
區域用于存盤大物件,G1規定只要大小超過了一個磁區容量一半的物件就認為是大物件,
G1 收集器對各個磁區回收所獲得的空間大小和回收所需時間的經驗值進行排序,得到一個優先級串列,每次根據用戶設定的最大回收停頓時間,優先回收價值最大的磁區,
特點:可以由用戶指定期望的垃圾收集停頓時間,
G1 收集器的回收程序分為以下幾個步驟:
- 初始標記,暫停所有其他執行緒,記錄直接與
GC Roots
直接相連的物件,耗時較短 , - 并發標記,從
GC Roots
開始對堆中物件進行可達性分析,找出要回收的物件,耗時較長,不過可以和用戶程式并發執行, - 最終標記,需對其他執行緒做短暫的暫停,用于處理并發標記階段物件參考出現變動的區域,
- 篩選回收,對各個磁區的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,然后把決定回收的磁區的存活物件復制到空的磁區中,再清理掉整個舊的磁區的全部空間,這里的操作涉及存活物件的移動,會暫停用戶執行緒,由多條收集器執行緒并行完成,
常用的 JVM 調優的命令都有哪些?
jps:列出本機所有 Java 行程的行程號,
常用引數如下:
-m
輸出main
方法的引數-l
輸出完全的包名和應用主類名-v
輸出JVM
引數
jps -lvm
//output
//4124 com.zzx.Application -javaagent:E:\IDEA2019\lib\idea_rt.jar=10291:E:\IDEA2019\bin -Dfile.encoding=UTF-8
jstack:查看某個 Java 行程內的執行緒堆疊資訊,使用引數-l
可以列印額外的鎖資訊,發生死鎖時可以使用jstack -l pid
觀察鎖持有情況,
jstack -l 4124 | more
輸出結果如下:
"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
WAITING (parking)
指執行緒處于掛起中,在等待某個條件發生,來把自己喚醒,
jstat:用于查看虛擬機各種運行狀態資訊(類裝載、記憶體、垃圾收集等運行資料),使用引數-gcuitl
可以查看垃圾回收的統計資訊,
jstat -gcutil 4124
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 67.21 19.20 96.36 94.96 10 0.084 3 0.191 0.275
引數說明:
- S0:
Survivor0
區當前使用比例 - S1:
Survivor1
區當前使用比例 - E:
Eden
區使用比例 - O:老年代使用比例
- M:元資料區使用比例
- CCS:壓縮使用比例
- YGC:年輕代垃圾回收次數
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
jmap:查看堆記憶體快照,通過jmap
命令可以獲得運行中的堆記憶體的快照,從而可以對堆記憶體進行離線分析,
查詢行程4124的堆記憶體快照,輸出結果如下:
>jmap -heap 4124
Attaching to process ID 4124, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11
using thread-local object allocation.
Parallel GC with 6 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4238344192 (4042.0MB)
NewSize = 88604672 (84.5MB)
MaxNewSize = 1412431872 (1347.0MB)
OldSize = 177733632 (169.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 327155712 (312.0MB)
used = 223702392 (213.33922576904297MB)
free = 103453320 (98.66077423095703MB)
68.37795697725736% used
From Space:
capacity = 21495808 (20.5MB)
used = 0 (0.0MB)
free = 21495808 (20.5MB)
0.0% used
To Space:
capacity = 23068672 (22.0MB)
used = 0 (0.0MB)
free = 23068672 (22.0MB)
0.0% used
PS Old Generation
capacity = 217579520 (207.5MB)
used = 41781472 (39.845916748046875MB)
free = 175798048 (167.65408325195312MB)
19.20285144484187% used
27776 interned Strings occupying 3262336 bytes.
jinfo:jinfo -flags 1
,查看當前的應用JVM引數配置,
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.111-b14
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=31457280 -XX:MaxHeapSize=480247808 -XX:MaxNewSize=160038912 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldSize=20971520 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
Command line:
查看所有引數:java -XX:+PrintFlagsFinal -version
,用于查看最終值,初始值可能被修改掉(查看初始值可以使用java -XX:+PrintFlagsInitial),
[Global flags]
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product}
uintx AdaptiveSizePolicyOutputInterval = 0 {product}
uintx AdaptiveSizePolicyWeight = 10 {product}
uintx AdaptiveSizeThroughPutPolicy = 0 {product}
uintx AdaptiveTimeWeight = 25 {product}
bool AdjustConcurrency = false {product}
bool AggressiveOpts = false {product}
....
物件頭了解嗎?
Java 記憶體中的物件由以下三部分組成:物件頭、實體資料和對齊填充位元組,
而物件頭由以下三部分組成:mark word、指向類資訊的指標和陣列長度(陣列才有),
mark word
包含:物件的哈希碼、分代年齡和鎖標志位,
物件的實體資料就是 Java 物件的屬性和值,
對齊填充位元組:因為JVM要求物件占的記憶體大小是 8bit 的倍數,因此后面有幾個位元組用于把物件的大小補齊至 8bit 的倍數,
記憶體對齊的主要作用是:
- 平臺原因:不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則拋出硬體例外,
- 性能原因:經過記憶體對齊后,CPU的記憶體訪問速度大大提升,
Object o = new Object()占用多少個位元組?
答案是16個位元組,
首先先分析物件的記憶體布局,
在 JVM 中,Java物件保存在堆中時,由以下三部分組成:
物件頭(Object Header):包括關于堆物件的布局、型別、GC狀態、同步狀態和標識哈希碼的基本資訊,由兩個詞mark word
和classpointer
組成,如果是陣列物件的話,還會有一個length field
,
- mark word:通常是一組位域,用于存盤物件自身的運行時資料,如hashCode、GC分代年齡、鎖同步資訊等等,占用64個位元(64位系統),8個位元組,
- classpointer:類指標,是物件指向它的類元資料的指標,虛擬機通過這個指標來確定這個物件是哪個類的實體,占用64個位元(64位系統),8個位元組,開啟壓縮類指標后,占用32個位元,4個位元組,
實體資料(Instance Data):存盤了代碼中定義的各種欄位的內容,包括從父類繼承下來的欄位和子類中定義的欄位,如果物件無屬性欄位,則這里就不會有資料,根據欄位型別的不同占不同的位元組,例如boolean型別占1個位元組,int型別占4個位元組等等,為了提高存盤空間的利用率,這部分資料的存盤順序會受到虛擬機分配策略引數和欄位在Java原始碼中定義順序的影響,
對齊填充(Padding):物件可以有對齊資料也可以沒有,默認情況下,Java虛擬機堆中物件的起始地址需要對齊至8的整數倍,如果一個物件的物件頭和實體資料占用的總大小不到8位元組的整數倍,則以此來填充物件大小至8位元組的整數倍,
為什么要對齊填充?欄位記憶體對齊的其中一個原因,是讓欄位只出現在同一CPU的快取行中,如果欄位不是對齊的,那么就有可能出現跨快取行的欄位,也就是說,該欄位的讀取可能需要替換兩個快取行,而該欄位的存盤也會同時污染兩個快取行,這兩種情況對程式的執行效率而言都是不利的,其實對其填充的最終目的是為了計算機高效尋址,
經過上面的分析之后,就可以知道Object o = new Object()具體占用多少記憶體了(以64位系統為例),
- 在開啟指標壓縮的情況下,markword占用8位元組,classpointer占用4位元組,Instance data無資料,總共是12位元組,由于物件需要為8的整數倍,Padding會補充4個位元組,總共占用16位元組,
- 在沒有開啟指標壓縮的情況下,markword占用8位元組,classpointer占用8位元組,Instance data無資料,也是占用16位元組,
main方法執行程序
以下是示例代碼:
public class Application {
public static void main(String[] args) {
Person p = new Person("大彬");
p.getName();
}
}
class Person {
public String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
執行main
方法的程序如下:
- 編譯
Application.java
后得到Application.class
后,執行這個class
檔案,系統會啟動一個JVM
行程,從類路徑中找到一個名為Application.class
的二進制檔案,將Application
類資訊加載到運行時資料區的方法區內,這個程序叫做類的加載, - JVM 找到
Application
的主程式入口,執行main
方法, main
方法的第一條陳述句為Person p = new Person("大彬")
,就是讓 JVM 創建一個Person
物件,但是這個時候方法區中是沒有Person
類的資訊的,所以 JVM 馬上加載Person
類,把Person
類的資訊放到方法區中,- 加載完
Person
類后,JVM 在堆中分配記憶體給Person
物件,然后呼叫建構式初始化Person
物件,這個Person
物件持有指向方法區中的 Person 類的型別資訊的參考, - 執行
p.getName()
時,JVM 根據 p 的參考找到 p 所指向的物件,然后根據此物件持有的參考定位到方法區中Person
類的型別資訊的方法表,獲得getName()
的位元組碼地址, - 執行
getName()
方法,
物件創建程序
- 類加載檢查:當虛擬機遇到一條
new
指令時,首先檢查是否能在常量池中定位到這個類的符號參考,并且檢查這個符號參考代表的類是否已被加載過、決議和初始化過,如果沒有,那先執行類加載, - 分配記憶體:在類加載檢查通過后,接下來虛擬機將為物件實體分配記憶體,
- 初始化,分配到的記憶體空間都初始化為零值,通過這個操作保證了物件的欄位可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值,
- 設定物件頭,
Hotspot
虛擬機的物件頭包括:存盤物件自身的運行時資料(哈希碼、分代年齡、鎖標志等等)、型別指標和資料長度(陣列物件才有),型別指標就是物件指向它的類資訊的指標,虛擬機通過這個指標來確定這個物件是哪個類的實體, - 按照
Java
代碼進行初始化,
如何排查 OOM 的問題?
線上JVM必須配置
-XX:+HeapDumpOnOutOfMemoryError
和-XX:HeapDumpPath=/tmp/heapdump.hprof
,當OOM發生時自動 dump 堆記憶體資訊到指定目錄
排查 OOM 的方法如下:
- 查看服務器運行日志日志,捕捉到記憶體溢位例外
- jstat 查看監控JVM的記憶體和GC情況,評估問題大概出在什么區域
- 使用MAT工具載入dump檔案,分析大物件的占用情況
什么是記憶體溢位和記憶體泄露?
記憶體溢位指的是程式申請記憶體時,沒有足夠的記憶體供申請者使用,比如給了你一塊存盤int型別資料的存盤空間,但是你卻存盤long型別的資料,那么結果就是記憶體不夠用,此時就會報錯OOM,即記憶體溢位,
記憶體泄露是指程式中間動態分配了記憶體,但在程式結束時沒有釋放這部分記憶體,從而造成那部分記憶體不可用的情況,這種情況重啟計算機可以解決,但也有可能再次發生記憶體泄露,記憶體泄露和硬體沒有關系,它是由軟體設計缺陷引起的,
像IO操作或者網路連接等,在使用完成之后沒有呼叫close()方法將其連接關閉,那么它們占用的記憶體是不會自動被GC回收的,此時就會產生記憶體泄露,
比如操作資料庫時,通過SessionFactory獲取一個session:
Session session=sessionFactory.openSession();
完成后我們必須呼叫session.close()方法關閉,否則就會產生記憶體泄露,因為sessionFactory這個長生命周期物件一直持有session這個短生命周期物件的參考,
那兩者有什么不同呢?
記憶體泄露可以通過完善代碼來避免,記憶體溢位可以通過調整配置來減少發生頻率,但無法徹底避免,
如何避免記憶體泄露和溢位呢?
- 盡早釋放無用物件的參考,比如使用臨時變數的時候,讓參考變數在退出活動域后自動設定為null,暗示垃圾收集器來收集該物件,防止發生記憶體泄露,
- 盡量少用靜態變數,因為靜態變數是全域的,GC不會回收,
- 避免集中創建物件尤其是大物件,如果可以的話盡量使用流操作,
- 盡量運用池化技術(資料庫連接池等)以提高系統性能,
- 避免在回圈中創建過多物件,
參考資料
- 周志明. 深入理解 Java 虛擬機 [M]. 機械工業出版社
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/550536.html
標籤:其他
上一篇:使用Java接入小程式訂閱訊息!