JVM入門
參考視頻:B站狂神,寫這個只是方便個人復習,怎么寫是我自己的事,我能看懂就行,沒要求非要讓你看!白嫖還挑刺,是很沒有風度的事情。希望做個有風度的“五好青年”!
面試常見:
- 請你談談你對JVM的理解?
- java8虛擬機和之前的變化更新?
- 什么是OOM,什么是棧溢出StackOverFlowError? 怎么分析?
- JVM的常用調優參數有哪些?
- 內存快照如何抓取?怎么分析Dump文件?
- 談談JVM中,類加載器你的認識?
1.JVM的位置
三種JVM:
- Sun公司:HotSpot 用的最多
- BEA:JRockit
- IBM:J9VM
我們學習都是:HotSpot
2.JVM的體系結構
- jvm調優:99%都是在方法區和堆,大部分時間調堆。 JNI(java native interface)本地方法接口。
3.類加載器
- 作用:加載Class文件——如果new Student();(具體實例在堆里,引用變量名放棧里) 。
- 先來看看一個類加載到 JVM 的一個基本結構:
- 類是模板,對象是具體的,通過new來實例化對象。car1,car2,car3,名字在棧里面,真正的實例,具體的數據在堆里面,棧只是引用地址。
- 虛擬機自帶的加載器
- 啟動類(根)加載器
- 擴展類加載器
- 應用程序加載器
package github.JVM.Demo01;
/**
* @author subeiLY
* @create 2021-06-08 07:42
*/
public class Test01 {
public static void main(String[] args) {
Test01 test01 = new Test01();
Test01 test02 = new Test01();
Test01 test03 = new Test01();
System.out.println(test01.hashCode());
System.out.println(test02.hashCode());
System.out.println(test03.hashCode());
/*
1836019240
325040804
1173230247
*/
Class<? extends Test01> aClass1 = test01.getClass();
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
/*
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@330bedb4
null
*/
Class<? extends Test01> aClass2 = test02.getClass();
Class<? extends Test01> aClass3 = test03.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
/*
2133927002
2133927002
2133927002
*/
}
}
類加載器的分類
- Bootstrap ClassLoader 啟動類加載器
- Extention ClassLoader 標准擴展類加載器
- Application ClassLoader 應用類加載器
- User ClassLoader 用戶自定義類加載器
4.雙親委派機制
package java.lang;
/**
* @author subeiLY
* @create 2021-06-08 08:06
*/
public class String {
/*
雙親委派機制:安全
1.APP-->EXC-->BOOT(最終執行)
BOOT
EXC
APP
*/
public String toString() {
return "Hello";
}
public static void main(String[] args) {
String s = new String();
System.out.println(s.getClass());
s.toString();
}
/*
1.類加載器收到類加載的請求
2.將這個請求向上委托給父類加載器去完成,一直向上委托,知道啟動類加載
3.啟動加載器檢查是否能夠加載當前這個類,能加載就結束,使用當前的加載器,否則,拋出異常,適知子加載器進行加載
4.重復步驟3
*/
}
- idea報了一個錯誤:
這是因為,在運行一個類之前,首先會在應用程序加載器(APP)中找,如果APP中有這個類,繼續向上在擴展類加載器EXC中找,然后再向上,在啟動類( 根 )加載器BOOT中找。如果在BOOT中有這個類的話,最終執行的就是根加載器中的。如果BOOT中沒有的話,就會倒找往回找。
過程總結
-
1.類加載器收到類加載的請求
-
2.將這個請求向上委托給父類加載器去完成,一直向上委托,直到啟動類加載器
-
3.啟動類加載器檢查是否能夠加載當前這個類,能加載就結束,使用當前的加載器,否則,拋出異常,一層一層向下,通知子加載器進行加載
-
4.重復步驟3
-
關於雙親委派機制的博客:
-
概念:當某個類加載器需要加載某個.class文件時,它首先把這個任務委托給他的上級類加載器,遞歸這個操作,如果上級的類加載器沒有加載,自己才會去加載這個類。
-
例子:當一個Hello.class這樣的文件要被加載時。不考慮我們自定義類加載器,首先會在AppClassLoader中檢查是否加載過,如果有那就無需再加載了。如果沒有,那么會拿到父加載器,然后調用父加載器的loadClass方法。父類中同理也會先檢查自己是否已經加載過,如果沒有再往上。注意這個類似遞歸的過程,直到到達Bootstrap classLoader之前,都是在檢查是否加載過,並不會選擇自己去加載。直到BootstrapClassLoader,已經沒有父加載器了,這時候開始考慮自己是否能加載了,如果自己無法加載,會下沉到子加載器去加載,一直到最底層,如果沒有任何加載器能加載,就會拋出ClassNotFoundException。
作用:
- 防止重復加載同一個.class。通過委托去向上面問一問,加載過了,就不用再加載一遍。保證數據安全。
- 保證核心.class不能被篡改。通過委托方式,不會去篡改核心.class,即使篡改也不會去加載,即使加載也不會是同一個.class對象了。不同的加載器加載同一個.class也不是同一個Class對象。這樣保證了Class執行安全。
比如:如果有人想替換系統級別的類:String.java。篡改它的實現,在這種機制下這些系統的類已經被Bootstrap classLoader加載過了(為什么?因為當一個類需要加載的時候,最先去嘗試加載的就是BootstrapClassLoader),所以其他類加載器並沒有機會再去加載,從一定程度上防止了危險代碼的植入。
5.沙箱安全機制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一個限制程序運行的環境。沙箱機制就是將Java代碼限定在虛擬機(JVM)特定的運行范圍中,並且嚴格限制代碼對本地系統資源訪問,通過這樣的措施來保證對代碼的有效隔離,防止對本地系統造成破壞。沙箱主要限制系統資源訪問,那系統資源包括什么?CPU、內存、文件系統、網絡。不同級別的沙箱對這些資源訪問的限制也可以不一樣。
所有的Java程序運行都可以指定沙箱,可以定制安全策略。
在]ava中將執行程序分成本地代碼和遠程代碼兩種,本地代碼默認視為可信任的,而遠程代碼則被看作是不受信的。對於授信的本地代碼,可以訪問一切本地資源。而對於非授信的遠程代碼在早期的ava實現中,安全依賴於沙箱(Sandbox)機制。如下圖所示JDK1.0安全模型。
但如此嚴格的安全機制也給程序的功能擴展帶來障礙,比如當用戶希望遠程代碼訪問本地系統的文件時候,就無法實現。因此在后續的Java1.1 版本中,針對安全機制做了改進,增加了安全策略,允許用戶指定代碼對本地資源的訪問權限。如下圖所示JDK1.1安全模型。
在Java1.2版本中,再次改進了安全機制,增加了代碼簽名。不論本地代碼或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限不同的運行空間,來實現差異化的代碼執行權限控制。如下圖所示JDK1.2安全模型。
當前最新的安全機制實現,則引入了域(Domain)的概念。虛擬機會把所有代碼加載到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行交互,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機中不同的受保護域(Protected Domain),對應不一樣的權限(Permission)。存在於不同域中的類文件就具有了當前域的全部權限,如下圖所示最新的安全模型(jdk 1.6)。
組成沙箱的基本組件:
-
字節碼校驗器
(bytecode verifier)︰確保Java類文件遵循lava語言規范。這樣可以幫助lava程序實現內存保護。但並不是所有的類文件都會經過字節碼校驗,比如核心類。 -
類裝載器(class loader) :其中類裝載器在3個方面對Java沙箱起作用:
。它防止惡意代碼去干涉善意的代碼;
。它守護了被信任的類庫邊界;
。它將代碼歸入保護域,確定了代碼可以進行哪些操作。
虛擬機為不同的類加載器載入的類提供不同的命名空間,命名空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個命名空間是由Java虛擬機為每一個類裝載器維護的,它們互相之間甚至不可見。
類裝載器采用的機制是雙親委派模式。
1.從最內層VM自帶類加載器開始加載,外層惡意同名類得不到加載從而無法使用;
2.由於嚴格通過包來區分了訪問域,外層惡意的類通過內置代碼也無法獲得權限訪問到內層類,破壞代碼就自然無法生效。
- 存取控制器(access controller)︰存取控制器可以控制核心API對操作系統的存取權限,而這個控制的策略設定,可以由用戶指定。
- 安全管理器(security manager)︰是核心API和操作系統之間的主要接口。實現權限控制,比存取控制器優先級高。
- 安全軟件包(security package) : java.security下的類和擴展包下的類,允許用戶為自己的應用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 數字簽名
- 加密
- 鑒別
6.Native
- 編寫一個多線程類啟動。
public static void main(String[] args) {
new Thread(()->{ },"your thread name").start();
}
- 點進去看start方法的源碼:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); // 調用了一個start0方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// 這個Thread是一個類,這個方法定義在這里是不是很詭異!看這個關鍵字native;
private native void start0();
-
凡是帶了native關鍵字的,說明 java的作用范圍達不到,去調用底層C語言的庫!
-
JNI:Java Native Interface(Java本地方法接口)
-
凡是帶了native關鍵字的方法就會進入本地方法棧;
-
Native Method Stack 本地方法棧
-
本地接口的作用是融合不同的編程語言為Java所用,它的初衷是融合C/C++程序,Java在誕生的時候是C/C++橫行的時候,想要立足,必須有調用C、C++的程序,於是就在內存中專門開辟了一塊區域處理標記為native的代碼,它的具體做法是 在 Native Method Stack 中登記native方法,在 ( ExecutionEngine ) 執行引擎執行的時候加載Native Libraies。
-
目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用中已經比較少見。因為現在的異構領域間通信很發達,比如可以使用Socket通信,也可以使用Web Service等等,不多做介紹!
7.PC寄存器
程序計數器:Program Counter Register
- 每個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向像一條指令的地址,也即將要執行的指令代碼),在執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不計。
8.方法區
Method Area 方法區
-
方法區是被所有線程共享,所有字段和方法字節碼,以及一些特殊方法,如構造函數,接口代碼也在此定義,簡單說,所有定義的方法的信息都保存在該區域,此區域屬於共享區間;
-
靜態變量、常量、類信息(構造方法、接口定義)、運行時的常量池存在方法區中,但是實例變量存在堆內存中,和方法區無關。
-
static ,final ,Class ,常量池~
9.棧
-
在計算機流傳有一句廢話: 程序 = 算法 + 數據結構
-
但是對於大部分同學都是: 程序 = 框架 + 業務邏輯
-
棧:后進先出 / 先進后出
-
隊列:先進先出(FIFO : First Input First Output)
棧管理程序運行
-
存儲一些基本類型的值、對象的引用、方法等。
-
棧的優勢是,存取速度比堆要快,僅次於寄存器,棧數據可以共享。
思考:為什么main方法最后執行!為什么一個test() 方法執行完了,才會繼續走main方法!
喝多了吐就是棧,吃多了拉就是隊列。
說明:
-
1、棧也叫棧內存,主管Java程序的運行,是在線程創建時創建,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放。
-
2、對於棧來說不存在垃圾回收問題,只要線程一旦結束,該棧就Over,生命周期和線程一致,是線程私有的。
-
3、方法自己調自己就會導致棧溢出(遞歸死循環測試)。
棧里面會放什么東西那?
- 8大基本類型 + 對象的引用 + 實例的方法
棧運行原理
-
Java棧的組成元素——棧幀。
-
棧幀是一種用於幫助虛擬機執行方法調用與方法執行的數據結構。他是獨立於線程的,一個線程有自己的一個棧幀。封裝了方法的局部變量表、動態鏈接信息、方法的返回地址以及操作數棧等信息。
-
第一個方法從調用開始到執行完成,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
當一個方法A被調用時就產生了一個棧幀F1,並被壓入到棧中,A方法又調用了B方法,於是產生了棧幀F2也被壓入棧中,B方法又調用了C方法,於是產生棧幀F3也被壓入棧中 執行完畢后,先彈出F3, 然后彈出F2,在彈出F1........
- 遵循 “先進后出” / "后進先出" 的原則。
- 棧滿了,拋出異常:stackOverflowError
- 對象實例化的過程。
10.三種JVM
- Sun公司HotSpot java Hotspot™64-Bit server vw (build 25.181-b13,mixed mode)
- BEA JRockit
- IBM 39 VM
- 我們學習都是:Hotspot
11.堆
Java7之前
-
Heap 堆,一個JVM實例只存在一個堆內存,堆內存的大小是可以調節的。
-
類加載器讀取了類文件后,需要把類,方法,常變量放到堆內存中,保存所有引用類型的真實信息,以方便執行器執行。
-
堆內存分為三部分:
-
新生區 Young Generation Space Young/New
-
養老區 Tenure generation space Old/Tenure
-
永久區 Permanent Space Perm
-
-
堆內存邏輯上分為三部分:新生,養老,永久(元空間 : JDK8 以后名稱)。
誰空誰是to
-
GC垃圾回收主要是在新生區和養老區,又分為輕GC 和 重GC,如果內存不夠,或者存在死循環,就會導致
-
在JDK8以后,永久存儲區改了個名字(元空間)。
12.新生區、養老區
-
新生區是類誕生,成長,消亡的區域,一個類在這里產生,應用,最后被垃圾回收器收集,結束生命。
-
新生區又分為兩部分:伊甸區(Eden Space)和幸存者區(Survivor Space),所有的類都是在伊甸區被new出來的,幸存區有兩個:0區 和 1區,當伊甸園的空間用完時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC)。將伊甸園中的剩余對象移動到幸存0區,若幸存0區也滿了,再對該區進行垃圾回收,然后移動到1區,那如果1區也滿了呢?(這里幸存0區和1區是一個互相交替的過程)再移動到養老區,若養老區也滿了,那么這個時候將產生MajorGC(Full GC),進行養老區的內存清理,若養老區執行了Full GC后發現依然無法進行對象的保存,就會產生OOM異常 “OutOfMemoryError ”。如果出現 java.lang.OutOfMemoryError:java heap space異常,說明Java虛擬機的堆內存不夠,原因如下:
-
1、Java虛擬機的堆內存設置不夠,可以通過參數 -Xms(初始值大小),-Xmx(最大大小)來調整。
-
2、代碼中創建了大量大對象,並且長時間不能被垃圾收集器收集(存在被引用)或者死循環。
-
13.永久區(Perm)
- 永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的Class,Interface的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉JVM才會釋放此區域所占用的內存。
- 如果出現 java.lang.OutOfMemoryError:PermGen space,說明是 Java虛擬機對永久代Perm內存設置不夠。一般出現這種情況,都是程序啟動需要加載大量的第三方jar包,
- 例如:在一個Tomcat下部署了太多的應用。或者大量動態反射生成的類不斷被加載,最終導致Perm區被占滿。
注意:
- JDK1.6之前: 有永久代,常量池1.6在方法區;
- JDK1.7: 有永久代,但是已經逐步 “去永久代”,常量池1.7在堆;
- JDK1.8及之后:無永久代,常量池1.8在元空間。
熟悉三區結構后方可學習JVM垃圾回收機制
-
實際而言,方法區(Method Area)和堆一樣,是各個線程共享的內存區域,它用於存儲虛擬機加載的:類信息+普通常量+靜態常量+編譯器編譯后的代碼,雖然JVM規范將方法區描述為堆的一個邏輯部分,但它卻還有一個別名,叫做Non-Heap(非堆),目的就是要和堆分開。
-
對於HotSpot虛擬機,很多開發者習慣將方法區稱之為 “永久代(Parmanent Gen)”,但嚴格本質上說兩者不同,或者說使用永久代實現方法區而已,永久代是方法區(相當於是一個接口interface)的一個實現,Jdk1.7的版本中,已經將原本放在永久代的字符串常量池移走。
-
常量池(Constant Pool)是方法區的一部分,Class文件除了有類的版本,字段,方法,接口描述信息外,還有一項信息就是常量池,這部分內容將在類加載后進入方法區的運行時常量池中存放!
14.堆內存調優
- -Xms:設置初始分配大小,默認為物理內存的 “1/64”。
- -Xmx:最大分配內存,默認為物理內存的 “1/4”。
- -XX:+PrintGCDetails:輸出詳細的GC處理日志。
測試1
代碼測試
public class Demo01 {
public static void main(String[] args) {
// 返回虛擬機試圖使用的最大內存
long max = Runtime.getRuntime().maxMemory(); // 字節:1024*1024
// 返回jvm的總內存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max=" + max + "字節\t" + (max/(double)1024/1024) + "MB");
System.out.println("total=" + total + "字節\t" + (total/(double)1024/1024) + "MB");
// 默認情況下:分配的總內存是電腦內存的1/4,初始化的內存是電腦的1/64
}
}
- IDEA中進行VM調優參數設置,然后啟動。
- 發現,默認的情況下分配的內存是總內存的 1/4,而初始化的內存為 1/64 !
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
- VM參數調優:把初始內存,和總內存都調為 1024M,運行,查看結果!
- 來大概計算分析一下!
- 再次證明:元空間並不在虛擬機中,而是使用本地內存。
測試2
代碼:
package github.JVM.Demo02;
import java.util.Random;
/**
* @author subeiLY
* @create 2021-06-08 10:22
*/
public class Demo02 {
public static void main(String[] args) {
String str = "suneiLY";
while (true) {
str += str + new Random().nextInt(88888888)
+ new Random().nextInt(999999999);
}
}
}
- vm參數:
-Xms8m -Xmx8m -XX:+PrintGCDetails
- 測試,查看結果!
-
這是一個young 區域撐爆的JAVA 內存日志,其中 PSYoungGen 表示 youngGen分區的變化1536k 表示 GC 之前的大小。
-
488k 表示GC 之后的大小。
-
整個Young區域的大小從 1536K 到 672K , young代的總大小為 7680K。
-
user – 總計本次 GC 總線程所占用的總 CPU 時間。
-
sys – OS 調用 or 等待系統時間。
-
real – 應用暫停時間。
-
如果GC 線程是 Serial Garbage Collector 串行搜集器的方式的話(只有一條GC線程,), real time 等於user 和 system 時間之和。
-
通過日志發現Young的區域到最后 GC 之前后都是0,old 區域 無法釋放,最后報堆溢出錯誤。
其他文章鏈接
15.GC
1.Dump內存快照
在運行java程序的時候,有時候想測試運行時占用內存情況,這時候就需要使用測試工具查看了。在eclipse里面有 Eclipse Memory Analyzer tool(MAT)插件可以測試,而在idea中也有這么一個插件,就是JProfiler,一款性能瓶頸分析工具!
作用:
-
分析Dump文件,快速定位內存泄漏;
-
獲得堆中對象的統計數據
-
獲得對象相互引用的關系
-
采用樹形展現對象間相互引用的情況
安裝JProfiler
- IDEA插件安裝
- 安裝JProfiler監控軟件
- 下載完雙擊運行,選擇自定義目錄安裝,點擊Next。
- 注意:安裝路徑,建議選擇一個文件名中沒有中文,沒有空格的路徑 ,否則識別不了。然后一直點Next。
- 注冊
// 注冊碼僅供大家參考
L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215
L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674
- 配置IDEA運行環境
- Settings–Tools–JProflier–JProflier executable選擇JProfile安裝可執行文件。(如果系統只裝了一個版本, 啟動IDEA時會默認選擇)保存。
- 代碼測試:
package github.JVM.Demo02;
import java.util.ArrayList;
/**
* @author subeiLY
* @create 2021-06-08 11:13
*/
public class Demo03 {
byte[] byteArray = new byte[1*1024*1024]; // 1M = 1024K
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true) {
list.add(new Demo03()); // 問題所在
count = count + 1;
}
} catch (Error e) {
System.out.println("count:" + count);
e.printStackTrace();
}
}
}
- vm參數 :
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
- 尋找文件:
使用 Jprofiler 工具分析查看
雙擊這個文件默認使用 Jprofiler 進行 Open大的對象!
- 從軟件開發的角度上,dump文件就是當程序產生異常時,用來記錄當時的程序狀態信息(例如堆棧的狀態),用於程序開發定位問題。
2.GC四大算法
1.引用計數法
-
每個對象有一個引用計數器,當對象被引用一次則計數器加1,當對象引用失效一次,則計數器減1,對於計數器為0的對象意味着是垃圾對象,可以被GC回收。
-
目前虛擬機基本都是采用可達性算法,從GC Roots 作為起點開始搜索,那么整個連通圖中的對象邊都是活對象,對於GC Roots 無法到達的對象變成了垃圾回收對象,隨時可被GC回收。
2.復制算法
- 年輕代中使用的是Minor GC,采用的就是復制算法(Copying)。
什么是復制算法?
-
Minor GC 會把Eden中的所有活的對象都移到Survivor區域中,如果Survivor區中放不下,那么剩下的活的對象就被移動到Old generation中,也就是說,一旦收集后,Eden就是變成空的了
-
當對象在Eden(包括一個Survivor區域,這里假設是From區域)出生后,在經過一次Minor GC后,如果對象還存活,並且能夠被另外一塊Survivor區域所容納 (上面已經假設為from區域,這里應為to區域,即to區域有足夠的內存空間來存儲Eden 和 From 區域中存活的對象),則使用復制算法將這些仍然還活着的對象復制到另外一塊Survivor區域(即 to 區域)中,然后清理所使用過的Eden 以及Survivor 區域(即form區域),並且將這些對象的年齡設置為1,以后對象在Survivor區,每熬過一次MinorGC,就將這個對象的年齡 + 1,當這個對象的年齡達到某一個值的時候(默認是15歲,通過- XX:MaxTenuringThreshold 設定參數)這些對象就會成為老年代。
-
-XX:MaxTenuringThreshold
任期門檻=>設置對象在新生代中存活的次數
面試題:如何判斷哪個是to區呢?一句話:誰空誰是to
原理解釋:
-
年輕代中的GC,主要是復制算法(Copying)
-
HotSpot JVM 把年輕代分為了三部分:一個 Eden 區 和 2 個Survivor區(from區 和 to區)。默認比例為 8:1:1,一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC后,如果仍然存活,將會被移到Survivor區,對象在Survivor中每熬過一次Minor GC , 年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中,因為年輕代中的對象基本上 都是朝生夕死,所以在年輕代的垃圾回收算法使用的是復制算法!復制算法的思想就是將內存分為兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象復制到另外一塊上面。復制算法不會產 生內存碎片!
- 在GC開始的時候,對象只會在Eden區和名為 “From” 的Survivor區,Survivor區“TO” 是空的,緊接着進行GC,Eden區中所有存活的對象都會被復制到 “To”,而在 “From” 區中,仍存活的對象會更具他們的年齡值來決定去向。
- 年齡達到一定值的對象會被移動到老年代中,沒有達到閾值的對象會被復制到 “To 區域”,經過這次GC后,Eden區和From區已經被清空,這個時候, “From” 和 “To” 會交換他們的角色, 也就是新的 “To” 就是GC前的“From” , 新的 “From” 就是上次GC前的 “To”。
- 不管怎樣,都會保證名為To 的Survicor區域是空的。 Minor GC會一直重復這樣的過程。直到 To 區 被填滿 ,“To” 區被填滿之后,會將所有的對象移動到老年代中。
-
因為Eden區對象一般存活率較低,一般的,使用兩塊10%的內存作為空閑和活動區域,而另外80%的內存,則是用來給新建對象分配內存的。一旦發生GC,將10%的from活動區間與另外80%中存活的Eden 對象轉移到10%的to空閑區域,接下來,將之前的90%的內存,全部釋放,以此類推;
-
好處:沒有內存碎片;壞處:浪費內存空間。
劣勢:
- 復制算法它的缺點也是相當明顯的。
- 1、他浪費了一半的內存,這太要命了。
- 2、如果對象的存活率很高,我們可以極端一點,假設是100%存活,那么我們需要將所有對象都復制一遍,並將所有引用地址重置一遍。復制這一工作所花費的時間,在對象存活率達到一定程度時,將會變的不可忽視,所以從以上描述不難看出。復制算法要想使用,最起碼對象的存活率要非常低才行,而且 最重要的是,我們必須要克服50%的內存浪費。
標記清除(Mark-Sweep)
-
回收時,對需要存活的對象進行標記;
-
回收不是綠色的對象。
-
當堆中的有效內存空間被耗盡的時候,就會停止整個程序(也被稱為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除。
-
標記:從引用根節點開始標記所有被引用的對象,標記的過程其實就是遍歷所有的GC Roots ,然后將所有GC Roots 可達的對象,標記為存活的對象。
-
清除: 遍歷整個堆,把未標記的對象清除。
-
缺點:這個算法需要暫停整個應用,會產生內存碎片。兩次掃描,嚴重浪費時間。
用通俗的話解釋一下 標記/清除算法,就是當程序運行期間,若可以使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨后將依舊存活的對象標記一遍,最終再將堆中所有沒被標記的對象全部清 除掉,接下來便讓程序恢復運行。
劣勢:
-
首先、它的缺點就是效率比較低(遞歸與全堆對象遍歷),而且在進行GC的時候,需要停止應用 程序,這會導致用戶體驗非常差勁
-
其次、主要的缺點則是這種方式清理出來的空閑內存是不連續的,這點不難理解,我們的死亡對象 都是隨機的出現在內存的各個角落,現在把他們清除之后,內存的布局自然亂七八糟,而為了應付 這一點,JVM就不得不維持一個內存空間的空閑列表,這又是一種開銷。而且在分配數組對象的時 候,尋找連續的內存空間會不太好找。
3.標記壓縮
- 標記整理說明:老年代一般是由標記清除或者是標記清除與標記整理的混合實現。
什么是標記壓縮?
原理:
-
在整理壓縮階段,不再對標記的對象作回收,而是通過所有存活對象都像一端移動,然后直接清除邊界以外的內存。可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被 清理掉,如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷。
-
標記、整理算法 不僅可以彌補 標記、清除算法當中,內存區域分散的缺點,也消除了復制算法當中,內存減半的高額代價;
4.標記清除壓縮
- 先標記清除幾次,再壓縮。
3.總結
-
內存效率:復制算法 > 標記清除算法 > 標記壓縮算法 (時間復雜度);
-
內存整齊度:復制算法 = 標記壓縮算法 > 標記清除算法;
-
內存利用率:標記壓縮算法 = 標記清除算法 > 復制算法;
可以看出,效率上來說,復制算法是當之無愧的老大,但是卻浪費了太多內存,而為了盡量兼顧上面所 提到的三個指標,標記壓縮算法相對來說更平滑一些 , 但是效率上依然不盡如人意,它比復制算法多了一個標記的階段,又比標記清除多了一個整理內存的過程。
難道就沒有一種最優算法嗎?
答案: 無,沒有最好的算法,只有最合適的算法 。 -----------> 分代收集算法
年輕代:(Young Gen)
- 年輕代特點是區域相對老年代較小,對象存活低。
- 這種情況復制算法的回收整理,速度是最快的。復制算法的效率只和當前存活對象大小有關,因而很適 用於年輕代的回收。而復制算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。
老年代:(Tenure Gen)
- 老年代的特點是區域較大,對象存活率高!
- 這種情況,存在大量存活率高的對象,復制算法明顯變得不合適。一般是由標記清除或者是標記清除與標記整理的混合實現。Mark階段的開銷與存活對象的數量成正比,這點來說,對於老年代,標記清除或 者標記整理有一些不符,但可以通過多核多線程利用,對並發,並行的形式提標記效率。Sweep階段的 開銷與所管理里區域的大小相關,但Sweep “就地處決” 的 特點,回收的過程沒有對象的移動。使其相對其他有對象移動步驟的回收算法,仍然是是效率最好的,但是需要解決內存碎片的問題。
16.JMM
-
什么是JMM?
- JMM:(java Memory Model的縮寫)
-
他干嘛的?官方,其他人的博客,對應的視頻!
-
作用:緩存一致性協議,用於定義數據讀寫的規則(遵守,找到這個規則)。
-
JMM定義了線程工作內存和主內存之間的抽象關系∶線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory)。
-
- 解決共享對象可見性這個問題:volilate
-
它該如何學習?
- JMM:抽象的概念,理論。
- JMM對這八種指令的使用,制定了如下規則:
- 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write。
- 不允許線程丟棄他最近的assign操作,即工作變量的數據改變了之后,必須告知主存。
- 不允許一個線程將沒有assign的數據從工作內存同步回主內存。
- 一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是懟變量實施use、store操作之前,必須經過assign和load操作。
- 一個變量同一時間只有一個線程能對其進行lock。多次lock后,必須執行相同次數的unlock才能解鎖。
- 如果對一個變量進行lock操作,會清空所有工作內存中此變量的值,在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值。
- 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量。
- 對一個變量進行unlock操作之前,必須把此變量同步回主內存。
JMM對這八種操作規則和對volatile的一些特殊規則就能確定哪里操作是線程安全,哪些操作是線程不安全的了。但是這些規則實在復雜,很難在實踐中直接分析。所以一般我們也不會通過上述規則進行分析。更多的時候,使用java的happen-before規則來進行分析。