文章目錄
- 1.JVM的位置
- 2.JVM的體系結構
- 3.類加載器
- 4.雙親委派機制(重要)
- 5.沙箱安全機制(了解)
- 6.native(核心)
- 7.PC寄存器(了解)
- 8.方法區
- 9.棧
- 10.三種JVM
- 11.堆(Heap)
- 12.新生區、老年區
- 13.永久區
- 14.堆內存調優
- 15.GC以及常用算法
- 引用計數法:
- 復制算法:
- 標記清除算法:
- 標記壓縮算法:
- 16.JMM
- 1、什么是JMM?
- 2、它干嘛的?
- 3、它該如何學習?
- 17.總結
常見的面試題:
- 請談一談你對JVM的理解?Java8虛擬機和之前的變化?
- 什么是OOM,什么是棧溢出?
- JVM的常用調優參數有哪些
- 內存快照如何抓取,怎么分析Dump文件?
- 談談JVM中你認識的類加載器
1.JVM的位置
類加載子系統:加載、鏈接、初始化
第三方插件:執行引擎處
Class File–javac
2.JVM的體系結構
Java棧、本地方法棧、程序計數器不會有垃圾回收,否則程序會死掉
百分之99的JVM調優都是在方法區和堆(99%是堆)中調優,Java棧、本地方法棧、程序計數器是不會有垃圾存在的。
3.類加載器
作用:加載class文件 new Student();
(有一個student的類,這個類是抽象的,當使用new關鍵詞new完后變成具體的實例,具體的實例在Java在棧中引用,具體的人實例放在堆中,要去堆中進行真正的數據引用)
類是模板,是抽象的,類實例化得到的對象是具體的。所有的對象反射回去得到的是同一個類模板。
- 虛擬機自帶的加載器
- 啟動類(根)加載器 BootstrapClassLoader(rt.jar):主要負責加載核心的類庫(java.lang.*等),構造ExtClassLoader和APPClassLoader。
- 擴展類加載器 ?ExtClassLoader(\jre\lib\ext):主要負責加載jre/lib/ext目錄下的一些擴展的jar。
- 應用程序加載器 ?AppClassLoader(當前應用程序加載器):主要負責加載應用程序的主函數類
- 百度:雙親委派機制
4.雙親委派機制(重要)
工作原理:
(1)如果一個類加載器收到了類加載請求,它并不會自己先加載,而是把這個請求委托給父類的加載器去執行
(2)如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的引導類加載器;
(3)如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成加載任務,子加載器才會嘗試自己去加載,這就是雙親委派機制
(4)父類加載器一層一層往下分配任務,如果子類加載器能加載,則加載此類,如果將加載任務分配至系統類加載器也無法加載此類,則拋出異常
當一個Hello.class這樣的文件要被加載時。不考慮我們自定義類加載器,
首先會在AppClassLoader中檢查是否加載過,如果有那就無需再加載了。
如果沒有,那么會拿到父加載器,然后調用父加載器的loadClass方法。
父類中同理也會先檢查自己是否已經加載過,如果沒有再往上。
注意這個類似遞歸的過程,直到到達Bootstrap classLoader之前,都是在檢查是否加載過,并不會選擇自己去加載。直到BootstrapClassLoader,已經沒有父加載器了,這時候開始考慮自己是否能加載了,如果自己無法加載,會下沉到子加載器去加載,一直到最底層,如果沒有任何加載器能加載,就會拋出ClassNotFoundException。那么有人就有下面這種疑問了?
為什么要設計這種機制?
這種設計有個好處是,如果有人想替換系統級別的類:String.java。篡改它的實現,在這種機制下這些系統的類已經被Bootstrap classLoader加載過了(為什么?因為當一個類需要加載的時候,最先去嘗試加載的就是BootstrapClassLoader),所以其他類加載器并沒有機會再去加載,從一定程度上防止了危險代碼的植入。
舉例一、
我自己建立一個 java.lang.String 類,寫上 static 代碼塊
package java.lang;public class String {static{System.out.println("我是自定義的String類的靜態代碼塊");}
}
在另外的程序中加載 String 類,看看加載的 String 類是 JDK 自帶的 String 類,還是我們自己編寫的 String 類
public class StringTest {public static void main(String[] args) {java.lang.String str = new java.lang.String();System.out.println("hello,atguigu.com");StringTest test = new StringTest();System.out.println(test.getClass().getClassLoader());}
}
為什么呢?
由于我們定義的String類本應用系統類加載器,但它并不會自己先加載,而是把這個請求委托給父類的加載器去執行,到了擴展類加載器發現String類不歸自己管,再委托給父類加載器(引導類加載器),這時發現是java.lang包,這事就歸引導類加載器管,所以加載的是 JDK 自帶的 String 類
舉例二、
在我們自己的 String 類中整個 main() 方法
package java.lang;public class String {static{System.out.println("我是自定義的String類的靜態代碼塊");}//錯誤: 在類 java.lang.String 中找不到 main 方法public static void main(String[] args) {System.out.println("hello,String");}
}
由于雙親委派機制找到的是 JDK 自帶的 String 類,但在引導類加載器的核心類庫API里的 String 類中并沒有 main() 方法
舉例三、
舉例 3:
在 java.lang 包下整個 ShkStart 類 (自定義類名)
package java.lang;public class ShkStart {public static void main(String[] args) {System.out.println("hello!");}
}
出于保護機制,java.lang 包下不允許我們自定義類
雙親委派機制優勢
通過上面的例子,我們可以知道,雙親機制可以
- 避免類的重復加載
- 保護程序安全,防止核心API被隨意篡改
- 自定義類:java.lang.String (沒用)
- 自定義類:java.lang.ShkStart(報錯:阻止創建 java.lang開頭的類)
- Class Not Found異常就是這么來的
- Null:Java調用不到。(底層是C、C++寫的)Java早期的名字:C+±- ?Java = C+±-:去掉繁瑣的東西,指針,內存管理~
- Java語言保留了C的接口,這些方法就是用native(本地)修飾的,java通過native方法調用操作系統的方法
5.沙箱安全機制(了解)
Java安全模型的核心就是Java沙箱(sanddiox),什么是沙箱?沙箱是一個限制程序運行的環境。沙箱機制就是將Java代碼限定在虛擬機 (JVM) 特定的運行范圍中,并且嚴格限制代碼對本地系統資源訪問,通過這樣的措施來保證對代碼的有效隔離,防止對本地系統造成破壞。沙箱主要限制系統資源訪問,那系統資源包括什么? CPU、內存、文件系統、網絡。不同級別的沙箱對這些資源訪問的限制也可以不一樣。
所有的Java程序運行都可以指定沙箱,可以定制安全策略。
在Java中將執行程序分成本地代碼和遠程代碼兩種,本地代碼默認視為可信任的,而遠程代碼則被看作是不受信的。對于授信的本地代碼,可以訪問一切本地資源。而對于非授信的遠程代碼在早期的Java實現中,安全依賴于沙箱(Sandbox)機制。如下圖所示JDK1.0安全模型
但如此嚴格的安全機制也給程序的功能擴展帶來障礙,比如當用戶希望遠程代碼訪問本地系統的文件時候,就無法實現。因此在后續的Java1.1版本中,針對安全機制做了改進,增加了安全策略,允許用戶指定代碼對本地資源的訪問權限。如下圖所示JDK1.1安全模型
在Java1.2版本中,再次改進了安全機制,增加了代碼簽名。不論本地代碼或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限不同的運行空間,來實現差異化的代碼執行權限控制。如下圖所示
當前最新的安全機制實現,則引入了**域(Domain)**的概念。虛擬機會把所有代碼加載到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行交互,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機中不同的受保護域(Protected Domain),對應不一樣的權限(Permission)。存在于不同域中的類文件就具有了當前域的全部權限,如下圖所示最新的安全模型(jdk 1.6)
組成沙箱的基本組件
字節碼校驗器(bytecode verifier):確保Java類文件遵循Java語言規范。這樣可以幫助Java程序實現內存保護。但并不是所有的類文件都會經過字節碼校驗,比如核心類。
類裝載器(class loader) :其中類裝載器在3個方面對Java沙箱起作用
它防止惡意代碼去干涉善意的代碼; //雙親委派機制
它守護了被信任的類庫邊界;
它將代碼歸入保護域,確定了代碼可以進行哪些操作。
虛擬機為不同的類加載器載入的類提供不同的命名空間,命名空間由一系列唯一的名稱組成, 每一個被裝載的類將有一個名字,這個命名空間是由Java虛擬機為每一個類裝載器維護的,它們互相之間甚至不可見。 ?
類裝載器采用的機制是雙親委派模式。
從最內層JVM自帶類加載器開始加載,外層惡意同名類得不到加載從而無法使用;
由于嚴格通過包來區分了訪問域,外層惡意的類通過內置代碼也無法獲得權限訪問到內層類,破壞代碼就自然無法生效。
●存取控制器(access controller) :存取控制器可以控制核心API對操作系統的存取權限,而這個控制的策略設定,可以由用戶指定。
●安全管理器(security manager) : 是核心API和操作系統之間的主要接口。實現權限控制,比存取控制器優先級高。
●安全軟件包(security package) : java.security下的類和擴展包下的類,允許用戶為自己的應用增加新的安全特性,包括:
安全提供者
消息摘要
數字簽名 keytools https
加密
鑒別
6.native(核心)
native:
凡是帶了native關鍵字的,說明java的作用范圍達不到了,會去調用底層c語言的庫
會進入本地方法棧
調用本地方法本地接口 JNI (Java Native Interface)
JNI作用:開拓Java的使用,融合不同的編程語言為Java所用,最初: C、C++
Java誕生的時候C、C++橫行,想要立足,必須要有調用C、C++的程序
它在內存區域中專門開辟了一塊標記區域: Native Method Stack,登記native方法
在最終執行的時候,加載本地方法庫中的方法通過JNI
例如:Java程序驅動打印機,管理系統,掌握即可,在企業級應用比較少
private native void start0();
調用其他接口:Socket… WebService … http~
? 目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過Java程序驅動打印機或者Java系統管理設備,在企業級應用中已經比較少見。因為現在的異構領域間通信很發達,比如可以使用Socket通信,也可以使用Web Service等等,不多做介紹!
Native Method Stack
它的具體做法是Native Method Stack 中登記native方法,在 ( Execution Engine ) 執行引擎執行的時候加載Native Libraies。【本地庫】
7.PC寄存器(了解)
程序計數器: Program Counter Register ?
?每個線程都有一個程序計數器,是線程私有的,就是一個指針, 指向方法區中的方法字節碼(用來存儲指向像一條指令的地址, 也即將要執行的指令代碼),在執行引擎讀取下一條指令, 是一個非常小的內存空間,幾乎可以忽略不計
8.方法區
方法區:Method Area
? 方法區是被所有線程共享,所有字段和方法字節碼,以及一些特殊方法,如構造函數,接口代碼也在此定義,簡單說,所有定義的方法的信息都保存在該區域,此區域屬于共享區間;
靜態變量、常量、類信息(構造方法、接口定義)、運行時的常量池存在方法區中,但是實例變量存在堆內存中,和方法區無關
static、final、Class、常量池
9.棧
1、棧:數據結構(棧跟隊列比較學習)
程序 = 數據結構+算法︰持續學習~
程序 = 框架+業務邏輯︰吃飯~(被淘汰)(springboot+springcloud)
棧:先進后出、后進先出,類似一個桶()
隊列:先進先出( FIFO : First Input First Output )
為什么main()先執行,最后結束~(main()方法先壓入棧,再壓入其他方法,main()最后彈出)
棧溢出:StackOverflowError
public void test(){a();
}
public void a(){test();
}
棧:也叫棧內存
主管程序的運行,生命周期和線程同步;
線程結束,棧內存也就釋放,對于棧來說,==不存在垃圾回收的問題;==一旦線程結束,棧就Over;
棧:棧內存中放8大基本類型+對象引用+實例的方法
棧運行原理:棧幀
棧幀:局部變量表+操作數棧
每執行一個方法,就會產生一個棧幀。程序正在運行的方法永遠都會在棧的頂部
棧+堆+方法區的交互關系:
查的內容:棧具體怎么存
手動畫出一個對象實例化的過程在內存中(百度、視頻)
調優是在堆中調優,堆是比較重要的
10.三種JVM
java -version
- Sun: HotSpot (java Hotspot?64-Bit server vw (build 25.181-b13,mixed mode))
- BEA :JRockit
- IBM: j9VM
- 我們學習都是:Hotspot
11.堆(Heap)
一個JVM只有一個堆內存,堆內存的大小是可以調節的。
類加載器讀取了類文件后,一般會把什么東西放到堆中?
類,方法,常量,變量~,保存我們所有引用類型的真實對象
堆內存中還要細分為三個區域:
- 新生區(伊甸園區) Young/New
- 養老區 Old
- 永久區 Perm
GC:Garbage recycling
- 輕GC:輕量級垃圾回收,主要是在新生區
- 重GC(Full GC):重量級垃圾回收,主要是養老區,重GC就說明內存都要爆了
GC垃圾回收,主要是在伊甸園區和養老區~
假設內存滿了,OOM(Out Of memory),堆內存不夠!
java.lang.OutOfMemoryError: Java heap space
在JDK8以后,永久存儲區改了個名字(元空間);
12.新生區、老年區
新生區:
類誕生和成長的地方,甚至死亡
- 伊甸園區:所有的對象都是在伊甸園區new出來的
- 幸存(0區,1區)
真理:經過研究,99%的對象都是臨時對象!
13.永久區
這個區域常駐內存的,用來存放jdk自身攜帶的class對象,interface元數據
存儲的是Java運行時的一些環境或類信息
這個區域不存在垃圾回收
關閉虛擬機就會釋放這個區域的內存
一個啟動類,加載了大量的第三方jar包,、tomcat部署了太多應用,大量動態生成的反射類。不斷地被加載直到內存滿,就會出現OOM;
- jdk1.6以前:永久代,常量池在方法區
- jdk1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中
- jdk1.8:無永久區,常量池在元空間
堆:新生區+老年區
元空間:非堆(邏輯上存在,物理上不存在)
OOM:
在一個項目中突然出現了OOM故障,那么如何排除,研究為什么錯
- 能夠看到代碼第幾行出錯:內存快照分析工具
- Debug:一行行分析代碼
MAT、Jprofiler的作用:
- 分析Dump內存文件,快速定位內存泄漏;
- 獲得堆中的數據
- 獲得大的對象
14.堆內存調優
Settings–Plugins–JProfiler
JProfiler客戶端官網下載
-Xm:設置初始化內存分配大小,默認1/64
-Xmx:設置最大分配內存,默認 1/4
-XX:+PrintGCDetails (打印GC垃圾回收信息)
-XX:+HeapDumpOutOfMenoryError(oom Dump)
?
biggest object–Thread Dump–main
15.GC以及常用算法
JVM在進行GC時,并不是對堆中這三個區域統一回收,大部分時候回收都是新生代
GC:垃圾分代收集算法
- 輕GC:
- 重GC:全局GC
GC題目:
- JVM的內存模型和分區,詳細到每個區放什么?
- 堆里面的分區有哪些?Eden、from、to、老年區、說說他們的特點
- GC算法有哪些?標記清除法、標記壓縮、復制算法,引用計數器(比較少)怎么用?
- 輕GC和重GC分別在什么時候發生?
引用計數法:
JVM一般不會采用這種方式,不高效,計數器繁瑣,一個大項目有很多對象
復制算法:
- 好處:沒有內存的碎片
- 壞處:浪費了內存空間,多了一半空間永遠是空的(浪費一個幸存區)
- 極端情況下:
- 假設對象100%存活,把所有拷貝到幸存區,所有地址重新做一遍
- 假設from區是滿的,全部復制到to區,成本很高。
- 復制算法最佳使用場景:對象存活度較低的時候(新生區)
標記清除算法:
缺點:兩次掃描,嚴重浪費時間,會產生內存碎片
優點:不需要額外的空間
標記壓縮算法:
優點:再次掃描,向一端移動存活的對象,防止內存碎片產生
缺點:又多了一次掃描成本
總結
- 內存效率:復制算法>標記清除算法>標記壓縮算法(時間復雜度)
- 內存整齊度:復制算法=標記壓縮算法>標記清除算法
- 內存利用率:標記壓縮算法=標記清除算法>復制算法
思考一個問題:難道沒有最優算法嗎?
答案:沒有,沒有最好的算法,只有最合適的算法 —>( GC:分代收集算法)
- 分代收集算法:
- 年輕代:存活率低(復制算法)!
- 老年代:區域大,存活率高(標記清除 (內存碎片不是太多) + 標記壓縮混合實現
一天時間學JVM,不現實,要深究,必須要下去花時間,和多看面試題,以及《深入理解JVM》
但是,我們可以掌握一個學習JVM的方法~
16.JMM
1、什么是JMM?
JMM:(Java Memory Model的縮寫):Java內存模型【百度百科】
2、它干嘛的?
官方,其他人的博客,對應的視頻!
作用:緩存一致性協議,用于定義數據讀寫的規則 (遵守,找到這個規則)。
JMM定義了線程工作內存和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),是從主內中拷貝的。
解決共享對象可見性這個問題: volilate;一旦刷新了就會很快的同步到主內存中。
3、它該如何學習?
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規則來進行分析。
搜索:JMM面試題
17.總結
學習新東西是常態:
- 如何針對面試學習。
- 如何針對技術學習。
針對面試學習:3/10–pass,總結面經,分析這10,再觸類旁通一下:百度面試題
通過大量的面試總結,得出一套解題思路;
學習方式:在線畫圖網站推薦搜索
https://www.processon.com/popular?criterion=jvm
百度:JVM參數(調優)+jvm內存的年輕代/老年代/持久代