本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~
Github地址:https://github.com/Tyson0314/Java-learning
JVM記憶體結構分為5大區域,程式計數器、虛擬機器器棧、本地方法棧、堆、方法區。
程式計數器
執行緒私有的,作為當前執行緒的行號指示器,用於記錄當前虛擬機器器正在執行的執行緒指令地址。程式計數器主要有兩個作用:
程式計數器是唯一一個不會出現 OutOfMemoryError
的記憶體區域,它的生命週期隨著執行緒的建立而建立,隨著執行緒的結束而死亡。
虛擬機器器棧
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通訊過程中,會存在堆內記憶體到堆外記憶體的資料拷貝操作,對於需要頻繁進行記憶體間資料拷貝且生命週期較短的暫存資料,都建議儲存到直接記憶體。
Java 程式通過棧上的 reference 資料來操作堆上的具體物件。物件的存取方式由虛擬機器器實現而定,目前主流的存取方式有使用控制程式碼和直接指標兩種:
本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~
堆的實體地址分配是不連續的,效能較慢;棧的實體地址分配是連續的,效能相對較快。
堆存放的是物件的範例和陣列;棧存放的是區域性變數,運算元棧,返回結果等。
堆是執行緒共用的;棧是執行緒私有的。
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
類,那麼類之間的比較結果及類的唯一性將無法保證。
實現通過類的全限定名獲取該類的二進位制位元組流的程式碼塊叫做類載入器。
主要有一下四種類載入器:
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
沒有任何的參照鏈相連時,說明這個物件是不可用的。
需要同時滿足以下 3 個條件類才可能會被解除安裝 :
java.lang.Class
物件沒有在任何地方被參照,無法在任何地方通過反射存取該類的方法。虛擬機器器可以對滿足上述 3 個條件的類進行回收,但不一定會進行回收。
強參照:在程式中普遍存在的參照賦值,類似Object obj = new Object()
這種參照關係。只要強參照關係還存在,垃圾收集器就永遠不會回收掉被參照的物件。
軟參照:如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體。
//軟參照
SoftReference<String> softRef = new SoftReference<String>(str);
弱參照:在進行垃圾回收時,不管當前記憶體空間足夠與否,都會回收只具有弱參照的物件。
//弱參照
WeakReference<String> weakRef = new WeakReference<String>(str);
虛參照:虛參照並不會決定物件的生命週期。如果一個物件僅持有虛參照,那麼它就和沒有任何參照一樣,在任何時候都可能被垃圾回收。虛參照主要是為了能在物件被收集器回收時收到一個系統通知。
GC(Garbage Collection
),垃圾回收,是Java與C++的主要區別之一。作為Java開發者,一般不需要專門編寫記憶體回收和垃圾清理程式碼。這是因為在Java虛擬機器器中,存在自動記憶體管理和垃圾清理機制。對JVM中的記憶體進行標記,並確定哪些記憶體需要回收,根據一定的回收策略,自動的回收記憶體,保證JVM中的記憶體空間,防止出現記憶體洩露和溢位問題。
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
。
對於 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
開始對堆中物件進行可達性分析,找出要回收的物件,耗時較長,不過可以和使用者程式並行執行。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
引數說明:
Survivor0
區當前使用比例Survivor1
區當前使用比例Eden
區使用比例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}
....
本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~
Java 記憶體中的物件由以下三部分組成:物件頭、範例資料和對齊填充位元組。
而物件頭由以下三部分組成:mark word、指向類資訊的指標和陣列長度(陣列才有)。
mark word
包含:物件的雜湊碼、分代年齡和鎖標誌位。
物件的範例資料就是 Java 物件的屬性和值。
對齊填充位元組:因為JVM要求物件佔的記憶體大小是 8bit 的倍數,因此後面有幾個位元組用於把物件的大小補齊至 8bit 的倍數。
記憶體對齊的主要作用是:
以下是範例程式碼:
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
類資訊載入到執行時資料區的方法區內,這個過程叫做類的載入。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
程式碼進行初始化。線上JVM必須設定
-XX:+HeapDumpOnOutOfMemoryError
和-XX:HeapDumpPath=/tmp/heapdump.hprof
,當OOM發生時自動 dump 堆記憶體資訊到指定目錄
排查 OOM 的方法如下:
記憶體溢位指的是程式申請記憶體時,沒有足夠的記憶體供申請者使用,比如給了你一塊儲存int型別資料的儲存空間,但是你卻儲存long型別的資料,那麼結果就是記憶體不夠用,此時就會報錯OOM,即記憶體溢位。
記憶體洩露是指程式中間動態分配了記憶體,但在程式結束時沒有釋放這部分記憶體,從而造成那部分記憶體不可用的情況。這種情況重啟計算機可以解決,但也有可能再次發生記憶體洩露。記憶體洩露和硬體沒有關係,它是由軟體設計缺陷引起的。
像IO操作或者網路連線等,在使用完成之後沒有呼叫close()方法將其連線關閉,那麼它們佔用的記憶體是不會自動被GC回收的,此時就會產生記憶體洩露。
比如運算元據庫時,通過SessionFactory獲取一個session:
Session session=sessionFactory.openSession();
完成後我們必須呼叫session.close()方法關閉,否則就會產生記憶體洩露,因為sessionFactory這個長生命週期物件一直持有session這個短生命週期物件的參照。
那兩者有什麼不同呢?
記憶體洩露可以通過完善程式碼來避免,記憶體溢位可以通過調整設定來減少發生頻率,但無法徹底避免。
如何避免記憶體洩露和溢位呢?
參考資料
最後給大家分享一個Github倉庫,上面有大彬整理的300多本經典的計算機書籍PDF,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~
Github地址:https://github.com/Tyson0314/java-books