請你談談你對JVM的理解?
JVM類載入器是怎麼樣的?有幾種?
什麼是OOM,什麼是StackOverFlowError? 怎麼分析?
JVM常用調優參數有哪寫?
GC有幾種演算法?分別是怎麼執行的?
你知道JProfiler嗎,怎麼分析Dump檔案?
第一次看到這些真真實實的面試題的時候,我~
image-20200802133524318
這都什麼玩意???????
經過一段時間的研究!!接下來,我將以大白話從頭到尾給大家講講Java虛擬機器!!
不對的地方還請大家指正~
目錄
1、什麼是JVM?在哪?
2、JVM、JRE、JDK 的關係
3、JVM體系結構
4、三種JVM(瞭解)
5、類載入器
1、回顧new物件的過程
2、類載入器的類別
6、雙親委派機制 機製
1、什麼是雙親委派機制 機製
2、作用
7、沙箱安全機制 機製
什麼是沙箱?
java中的安全模型演進
組成沙箱的基本元件
位元組碼校驗器
(bytecode verifier)類裝載器
(class loader)1、什麼是JVM?在哪?
JVM是Java Virtual Machine(Java虛擬機器)的縮寫,JVM是一種用於計算裝置的規範,它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。
百度的解釋雲裡霧裏,對於我們Java程式設計師,說白了就是:
JVM本質上是一個程式,它能識別.class 位元組碼檔案(裏面存放的是我們對.java編譯後產生的二進制程式碼),並且能夠解析它的指令,最終呼叫操作系統上的函數,完成我們想要的操作!
關於Java語言的跨平臺性,就是因爲JVM,我們可以將其想象爲一個抽象層,只要這個抽象層JVM正確執行了.class檔案,就能執行在各種操作系統之上了!這就是一次編譯,多次執行
對於JVM的位置:
JVM是執行在操作系統之上的,它與硬體沒有直接的互動
image-20200802133302669
2、JVM、JRE、JDK 的關係
JDK(Java Development Kit):Java開發工具包
JRE(Java Runtime Environment):Java執行環境
JDK = JRE + javac/java/jar 等指令工具
JRE = JVM + Java基本類庫
image-20200802140843921
3、JVM體系結構
Java虛擬機器主要分爲五大模組:
類裝載器子系統
執行時數據區
執行引擎
本地方法介面
垃圾收集模組
image-20200802154319450
方法區是一種特殊的堆
棧裏面不會有垃圾,用完就彈出了,否則阻塞了main方法
垃圾幾乎都在堆裡,所以JVM效能調優%99都針對於堆
4、三種JVM(瞭解)
Sun公司 HotSpot(我們都用的這個)
BEA公司 JRockit
IBM公司 J9 VM
5、類載入器
作用:載入.Class位元組碼檔案
1、回顧new物件的過程
public class Student {
//私有屬性
private String name;
//構造方法
public Student(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
類是模板、模板是抽象的;物件是具體的,是對抽象的範例化
//執行時,JVM將Test的資訊放入方法區
public class Test{
//main方法本身放入方法區
public static void main(String[] args){
//s1、s2、s3爲不同對象
Student s1 = new Student(「zsr」); //參照放在棧裡,具體的範例放在堆裡
Student s2 = new Student(「gcc」);
Student s3 = new Student(「BareTH」);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
//class1、class2、class3爲同一個物件
Class<? extends Student> class1 = s1.getClass();
Class<? extends Student> class2 = s2.getClass();
Class<? extends Student> class3 = s3.getClass();
System.out.println(class1.hashCode());
System.out.println(class2.hashCode());
System.out.println(class3.hashCode());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
根據結果,我們發現:
s1、s2、s3的hashcode是不同的,因爲是三個不同的物件,物件是具體的
class1、class2、class3的hashcode是相同的,因爲這是類別範本,模板是抽象的
我們畫圖分析以下new一個物件的流程:
首先Class Loader讀取位元組碼.class檔案,載入初始化生成Student模板類
通過Student模板類new出三個物件
image-20200801191801450
那麼Class Loader具體是怎麼執行我們的.class位元組碼檔案呢,這就引出了我們類載入器~
2、類載入器的類別
我們編寫這樣一個程式
image-20200801201223277
根據返回結果,我們來講解以下三種載入器:
級別從高到底
啓動類(根)載入器:BootstrapClassLoader
c++編寫,載入java核心庫 java.*,構造拓展類載入器和應用程式載入器。
根載入器載入拓展類載入器,並且將拓展類載入器的父載入器設定爲根載入器,
然後再載入應用程式載入器,應將應用程式載入器的父載入器設定爲拓展類載入器
由於引導類載入器涉及到虛擬機器本地實現細節,我們無法直接獲取到啓動類載入器的參照;這就是上面那個程式我們第三個結果爲null的原因。
載入檔案存在位置
image-20200801201827272
拓展類載入器:PlatformClassLoader
java編寫,載入擴充套件庫,開發者可以直接使用標準擴充套件類載入器。
java9之前爲ExtClassloader,Java9以後改名爲PlatformClassLoader
載入檔案存在位置
image-20200801201942721
應用程式載入器:AppClassLoader
java編寫,載入程式所在的目錄
是Java預設的類載入器
使用者自定義類載入器:CustomClassLoader
java編寫,使用者自定義的類載入器,可載入指定路徑的class檔案
6、雙親委派機制 機製
1、什麼是雙親委派機制 機製
類載入器收到類載入的請求
將這個請求向上委託給父類別載入器去完成,一直向上委託,直到根載入器BootstrapClassLoader
根載入器檢查是否能夠載入當前類,能載入就結束,使用當前的載入器;否則就拋出異常,通知子載入器進行載入;自載入器重複該步驟。
2、作用
舉個例子:我們重寫以下java.lang包下的String類
image-20200802150012759
發現報錯了,這就是雙親委派機制 機製起的作用,當類載入器委託到根載入器的時候,String類已經被根載入器載入過一遍了,所以不會再載入,從一定程度上防止了危險程式碼的植入!!
作用總結:
防止重複載入同一個.class。通過不斷委託父載入器直到根載入器,如果父載入器載入過了,就不用再載入一遍。保證數據安全。
保證系統核心.class,如上述的String類不能被篡改。通過委託方式,不會去篡改核心.class,即使篡改也不會去載入,即使載入也不會是同一個.class物件了。不同的載入器載入同一個.class也不是同一個class物件。這樣保證了class執行安全。
7、沙箱安全機制 機製
這裏參照了這篇博文參照鏈接,瞭解即可
什麼是沙箱?
Java安全模型的核心就是Java沙箱(sandbox)
沙箱是一個限製程式執行的環境。沙箱機制 機製就是將 Java 程式碼限定在虛擬機器(JVM)特定的執行範圍中,並且嚴格限制程式碼對本地系統資源存取,通過這樣的措施來保證對程式碼的有效隔離,防止對本地系統造成破壞。
沙箱主要限制系統資源存取,系統資源包括CPU、記憶體、檔案系統、網路。不同級別的沙箱對這些資源存取的限制也可以不一樣。
所有的Java程式執行都可以指定沙箱,可以定製安全策略。
java中的安全模型演進
在Java中將執行程式分成原生代碼和遠端程式碼兩種
原生代碼可信任,可以存取一切本地資源。
遠端程式碼不可信信在早期的Java實現中,安全依賴於沙箱 (Sandbox) 機制 機製。
如下圖所示
img
如此嚴格的安全機制 機製也給程式的功能擴充套件帶來障礙,比如當使用者希望遠端程式碼存取本地系統的檔案時候,就無法實現。
因此在後續的 Java1.1 版本中,針對安全機制 機製做了改進,增加了安全策略,允許使用者指定程式碼對本地資源的存取許可權。
如下圖所示
在Java1.2版本中,再次改進了安全機制 機製,增加了程式碼簽名。
不論原生代碼或是遠端程式碼,都會按照使用者的安全策略設定,由類載入器載入到虛擬機器中許可權不同的執行空間,來實現差異化的程式碼執行許可權控制。
如下圖所示
JDK1.2安全模型
當前最新的安全機制 機製實現,則引入了域 (Domain) 的概念。
虛擬機器會把所有程式碼載入到不同的系統域和應用域
系統域部分專門負責與關鍵資源進行互動
應用域部分則通過系統域的部分代理來對各種需要的資源進行存取。
虛擬機器中不同的受保護域 (Protected Domain),對應不一樣的許可權 (Permission)。存在於不同域中的類檔案就具有了當前域的全部許可權,如下圖所示
最新的安全模型
組成沙箱的基本元件
位元組碼校驗器(bytecode verifier)
確保Java類檔案遵循Java語言規範。這樣可以幫助Java程式實現記憶體保護。但並不是所有的類檔案都會經過位元組碼校驗,比如核心類(如上述java.lang.String)。
類裝載器(class loader)
其中類裝載器在3個方面對Java沙箱起作用
它防止惡意代碼去幹涉善意的程式碼;
它守護了被信任的類庫邊界;
它將程式碼歸入保護域,確定了程式碼可以進行哪些操作。
虛擬機器爲不同的類載入器載入的類提供不同的名稱空間,名稱空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個名稱空間是由Java虛擬機器爲每一個類裝載器維護的,它們互相之間甚至不可見。
類裝載器採用的機制 機製是雙親委派模式。
從最內層JVM自帶類載入器開始載入,外層惡意同名類得不到載入從而無法使用;
由於嚴格通過包來區分了存取域,外層惡意的類通過內建程式碼也無法獲得許可權存取到內層類,破壞程式碼就自然無法生效。
存取控制器(access controller):存取控制器可以控制核心API對操作系統的存取許可權,而這個控制的策略設定,可以由使用者指定。
安全管理器(security manager):是核心API和操作系統之間的主要介面。實現許可權控制,比存取控制器優先順序高。
安全軟體包(security package):java.security下的類和擴充套件包下的類,允許使用者爲自己的應用增加新的安全特性,包括:
安全提供者
訊息摘要
數位簽名
加密
鑑別
8、Native本地方法介面
JNI:Java Native Interface
本地介面的作用是融合不同的程式語言爲Java所用,它的初衷是融合C/C++程式
image-20200801214515097
native:凡是帶native關鍵字的,說明java的作用範圍達不到了,會去呼叫底層c語言的庫!進入本地方法棧,呼叫本地方法介面JNI,拓展Java的使用,融合不同的語言爲Java所用
Java誕生的時候C、C++橫行,爲了立足,必須要能呼叫C、C++的程式
於是在記憶體區域中專門開闢了一塊標記區域:Native Method Stack,登記Native方法
最終在執行引擎執行的的時候通過JNI(本地方法介面)載入本地方法庫的方法
目前該方法使用的越來越少了,除非是與硬體有關的應用,比如通過Java程式驅動印表機或者Java系統管理生產裝置,在企業級應用中已經比較少見。因爲現在的異構領域間通訊很發達,比如可以使用 Socket通訊,也可以使用 Web service等等,瞭解即可!
9、PC暫存器
程式計數器: Program Counter Register
每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指針,指向方法區中的方法位元組碼(用來儲存指向像一條指令的地址,也即將要執行的指令程式碼),在執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不計
10、方法區
方法區:Method Area
方法區是被所有執行緒共用,所有欄位和方法位元組碼,以及一些特殊方法,如建構函式,介面程式碼也在此定義,簡單說,所有定義的方法的資訊都儲存在該區域,此區域屬於共用區間;
方法區與Java堆一樣,是各個執行緒共用的記憶體區域,用於儲存已被虛擬機器載入的類資訊、常數、靜態變數、即時編譯器編譯後的程式碼等數據。雖然Java 虛擬機器規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java 堆區分開來。
這就是堆、棧、方法區的互動關係
11、棧
又稱棧記憶體,主管程式的執行,生命週期和執行緒同步,執行緒結束,棧記憶體就釋放了,不存在垃圾回收
棧:先進後出
佇列:先進先出(FIFO)
1、棧中存放啥?
8大基本型別
物件參照
範例的方法
2、棧執行原理
棧表示Java方法執行的記憶體模型
每呼叫一個方法就會爲每個方法生成一個棧幀(Stack Frame),每個方法被呼叫和完成的過程,都對應一個棧幀從虛擬機器棧上入棧和出棧的過程。
程式正在執行的方法一定在棧的頂部
image-20200802105127681
3、堆疊溢位StackOverflowError
舉個例子:
public class Test {
public static void main(String[] args) {
new Test().a();
}
public void a() {
b();
}
public void b() {
a();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
最開始,main()方法壓入棧中,然後執行a(),a()壓入棧中;再呼叫b(),b()壓入棧中;以此往復,a與b方法不斷被壓入棧中,最終導致棧溢位
image-20200802175751568
image-20200802100238868
12、堆
Heap,一個JVM只有一個堆記憶體(棧是執行緒級的),堆記憶體的大小是可以調節的
image-20200803130536438
1、堆中有啥?
範例化的物件
2、堆記憶體詳解
image-20200802183315909
1、Young 年輕代
物件誕生、成長甚至死亡的區
Eden Space(伊甸園區):所有的物件都是在此new出來的
Survivor Space(倖存區)
倖存0區(From Space)(動態的,From和To會互相交換)
倖存1區(To Space)
Eden區佔大容量,Survivor兩個區佔小容量,預設比例是8:1:1。
2、Tenured 老年代
3、Perm 元空間
儲存的是Java執行時的一些環境或類資訊,這個區域不存在垃圾回收!關閉虛擬機器就會釋放這個區域記憶體!
這個區域常駐記憶體,用來存放JDK自身攜帶的Class物件、Interface元數據。
名稱演變
jdk1.6之前:永久代
jdk1.7:永久代慢慢退化,去永久代
jdk1.8之後:永久代改名爲元空間
注意:元空間在邏輯上存在,在物理上不存在
新生代 + 老年代的記憶體空間 = JVM分配的總記憶體
如圖所示:
image-20200802211056044
3、什麼是OOM?
記憶體溢位java.lang.OutOfMemoryError
產生原因:
分配的太少
用的太多
用完沒釋放
4、GC垃圾回收
GC垃圾回收,主要在年輕代和老年代
首先,物件出生再伊甸園區
假設伊甸園區只能存一定數量的物件,則每當存滿時就會觸發一次輕GC(Minor GC)
輕GC清理後,有的物件可能還存在參照,就活下來了,活下來的物件就進入倖存區;有的物件沒用了,就被GC清理掉了;每次輕GC都會使得伊甸園區爲空
如果倖存區和伊甸園都滿了,則會進入老年代,如果老年代滿了,就會觸發一次重GC(FullGC),年輕代+老年代的物件都會清理一次,活下的物件就進入老年代
如果新生代和老年代都滿了,則OOM
Minor GC:伊甸園區滿時觸發;從年輕代回收記憶體
Full GC:老年代滿時觸發;清理整個堆空間,包含年輕代和老年代
Major GC:清理老年代
什麼情況永久區會崩?
一個啓動類載入了大量的第三方Jar包,Tomcat部署了過多應用,或者大量動態生成的反射類
這些東西不斷的被載入,直到記憶體滿,就會出現OOM
14、堆記憶體調優
1、檢視並設定JVM堆記憶體
檢視我們jvm的堆記憶體
public class Test {
public static void main(String[] args) {
//返回jvm試圖使用的最大記憶體
long max = Runtime.getRuntime().maxMemory();
//返回jvm的初始化記憶體
long total = Runtime.getRuntime().totalMemory();
//預設情況下:分配的總記憶體爲電腦記憶體的1/4,初始化記憶體爲電腦記憶體的1/64
System.out.println(「max=」 + max / (double) 1024 / 1024 / 1024 + 「G」);
System.out.println(「total=」 + total / (double) 1024 / 1024 / 1024 + 「G」);
}
}
1
2
3
4
5
6
7
8
9
10
11
image-20200802203740212
image-20200802203754264
預設情況下:
JVM最大分配記憶體爲電腦記憶體的1/4
JVM初始化記憶體爲電腦記憶體的1/64
我們可以手動調堆記憶體大小
image-20200802203828544
在VM options中可以指定jvm試圖使用的最大記憶體和jvm初始化記憶體大小
-Xms1024m -Xmx1024m -Xlog:gc*
1
-Xmx用來設定jvm試圖使用的最大記憶體,預設爲1/4
-Xms用來設定jvm初始化記憶體,預設爲1/64
-Xlog:gc*用來列印GC垃圾回收資訊
image-20200802205748906
image-20200802205834231
2、怎麼排除OOM錯誤?
嘗試擴大堆記憶體看結果
利用上述方法指定jvm試圖使用的最大記憶體和jvm初始化記憶體大小
利用記憶體快照工具JProfiler
記憶體快照工具:
MAT(Eclipse)
JProfiler
作用:
分析Dump記憶體檔案,快速定位記憶體漏失
獲得堆中的檔案
獲得大的物件
…
3. 什麼是Dump檔案?如何分析?
Dump檔案是進程的記憶體映象,可以把程式的執行狀態通過偵錯程式儲存到dump檔案中
舉個例子
import java.util.ArrayList;
public class Test {
byte[] array = new byte[1024 * 1024];//1M
public static void main(String[] args) {
ArrayList<Test> list = new ArrayList<>();
int count = 0;
try {
while (true) {
list.add(new Test());
count++;
}
} catch (Exception e) {
System.out.println("count=" + count);
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
執行該程式,報錯OOM
image-20200803101511962
接下來我們設定以下堆記憶體,並附加生成對應的dump檔案的指令
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
1
-XX:+HeapDumpOnOutOfMemoryError表示當JVM發生OOM時,自動生成DUMP檔案。
再次點選執行,下載了對應的Dump檔案
image-20200803101950485
我們右鍵該類,點選Show in Explorer
image-20200803102029506
一直點選上級目錄,直到找到.hprof檔案,與src同級目錄下
image-20200803102326809
我們雙擊開啓,可以看到每塊所佔的大小,便於分析問題
image-20200803103451739
點選Thread Dump,裏面是所有的執行緒,點選對應的執行緒可以看到相應的錯誤,反饋到具體的行,便於排錯
image-20200803103549701
每次開啓Dump檔案檢視完後,建議刪除,可以在idea中看到,開啓檔案後生成了很多內容,佔記憶體,建議刪除
image-20200803105321131
附:安裝Jprofiler教學
idea中安裝外掛
image-20200802220019318
下載用戶端 https://www.ej-technologies.com/download/jprofiler/files
image-20200802220133541
安裝用戶端
選擇自定義安裝,注意:路徑不能有中文和空格
image-20200802221801637
image-20200802222314832
這裏name和Company任意,License Key大家可以尋找對應版本的註冊機獲得
image-20200802224806160
image-20200802224847517
後續預設,安裝成功即可!!!
安裝完成後,重新啓動IDEA,可以看到我們的記憶體快照工具
image-20200803100145161
開啓IDEA的設定,找到Tools裏面的JProfiler,沒有設定位置則設定位置
image-20200803100302668 此時則全部安裝完成!
15、GC垃圾回收
1、回顧
Garbage Collection:垃圾回收
image-20200803111122670
在12.4中,我們已經對GC的流程進行了大概的講解,這裏做一些總結:
JVM在進行GC時,並不是對年輕代、老年代統一回收;大部分時候,回收都是在年輕代
GC分爲兩種:
輕GC(清理年輕代)
重GC(清理年輕代+老年代)
2、GC演算法
1、參照計數演算法(很少使用)
每個物件在建立的時候,就給這個物件系結一個計數器。
每當有一個參照指向該物件時,計數器加一;每當有一個指向它的參照被刪除時,計數器減一。
這樣,當沒有參照指向該物件時,該物件死亡,計數器爲0,這時就應該對這個物件進行垃圾回收操作。
image-20200803131912296
2、複製演算法
複製演算法主要發生在年輕代( 倖存0區 和 倖存1區)
當Eden區滿的時候,會觸發輕GC,每觸發一次,活的物件就被轉移到倖存區,死的就被GC清理掉了,所以每觸發輕GC時,Eden區就會清空;
物件被轉移到了倖存區,倖存區又分爲From Space和To Space,這兩塊區域是動態交換的,誰是空的誰就是To Space,然後From Space就會把全部物件轉移到To Space去;
那如果兩塊區域都不爲空呢?這就用到了複製演算法,其中一個區域會將存活的物件轉移到令一個區域去,然後將自己區域的記憶體空間清空,這樣該區域爲空,又成爲了To Space;
所以每次觸發輕GC後,Eden區清空,同時To區也清空了,所有的物件都在From區
這也就是倖存0區和倖存1區總有一塊爲空的原因
image-20200803120637032
好處:沒有記憶體的碎片(記憶體集中在一塊)
壞處:
浪費了記憶體空間(浪費了倖存區一半空間)
物件存活率較高的場景下(比如老年代那樣的環境),需要複製的東西太多,效率會下降。
最佳使用環境:物件存活度較低的時候,也就是年輕代
3、標記–清除演算法
爲每個物件儲存一個標記位,記錄物件的生存狀態
標記階段:這個階段內,爲每個物件更新標記位,檢查物件是否死亡;
清除階段:該階段對死亡的物件進行清除,執行 GC 操作。
image-20200803131533109
缺點:兩次掃描嚴重浪費時間,會產生記憶體碎片
優點:不需要額外的空間
4、標記–整理演算法
標記-整理法 是 標記-清除法 的一個改進版。
又叫做 標記-清楚-壓縮法
標記階段,該演算法也將所有物件標記爲存活和死亡兩種狀態;https://www.szcbjs.com/
不同的是,在第二個階段,該演算法並沒有直接對死亡的物件進行清理,而是將所有存活的物件整理一下,放到另一處空間,然後把剩下的所有物件全部清除。
image-20200803131545066
可以進一步優化,在記憶體碎片不太多的情況下,就繼續標記清除,到達一定量的時候再壓縮
總結
記憶體(時間複雜度)效率:複製演算法 > 標記清除演算法 > 標記壓縮演算法
記憶體整齊度:複製演算法 = 標記壓縮法 > 標記清除法
記憶體利用率:標記壓縮法 = 標記清除法 > 複製演算法
思考:有沒有最優的演算法?
沒有最優的演算法,只有最合適的演算法
GC 也稱爲 分代收集演算法
對於年輕代:
物件存活率低
用複製演算法
對於老年代:
區域大,物件存活率高
用標記清除+標記壓縮混合實現