01. JVM是什么
概述:
大白話:
全稱Java Virtual Machine(Java虛擬機), 它是一個虛構出來的計算機, 通過實際的計算機來模擬各種計算機的功能.
專業版:
JVM是一個行程, 用來模擬計算單元, 將.class位元組碼檔案轉成計算機能夠識別的指令.
//這里可以聯想以前大家學的"VM ware", 它也是一個虛擬機.
//它們的區別就在于: VM Ware是你能看見的, JVM是你看不見的.
回顧:
我們以前寫的Java程式是: 撰寫 --> 編譯 --> 運行三個階段的.
.class檔案是Java語言獨有的, 只有JVM能識別, 其他任何軟體都識別不了.
所以Java語言的"跨平臺性(一次編譯到處運行)"就是由JVM來保證的.
畫圖演示:
JVM把.class位元組碼檔案 轉成 計算機能夠識別的指令的程序.
代碼演示:
D:\compile\Worker.java檔案, 通過"jps"命令查看啟動的行程.
02. JVM虛擬機運行的流程
JVM是一個行程,接下來我們來研究它的: 作業機制, 這個問題是很深奧的, 不亞于研究一個完整VM Ware虛擬機,但是諸如"硬碟, CD/DVD這些部分和我們都沒關系", 所以研究JVM的作業機制就是在研究它的: 運算機制.
首先, 請你思考一個問題: 如果我給你一個A.class位元組碼檔案, 想把它運行起來, 你會做哪些事情?
畫圖演示:
1. 讀取位元組碼檔案所在的路徑.
//類加載機制
2. 獲取位元組碼檔案中具體的內容.
//方法區: 用來存放類的描述資訊.
3. 獲取該類的實體(物件)
//堆(Heap): 用來存盤物件的(所有new出來的內容)
4. 通過物件名.的方式呼叫方法.
//堆疊(Stack): 用來存放區域變數及所有代碼執行的.
今天我們的學習順序, 就是按照這個流程來走的.
03. JVM虛擬機類加載機制(一):運行順序
首先, 我們先來研究JVM的類加載機制, 類加載機制就是把類給讀取出來, 我們來看一下它是如何運行的.
畫圖演示:
JVM底層加載類依靠三大組件:
BootStrapClassLoader //啟動類加載器
//負責加載: jre\lib\rt.jar //rt: runtime, 運行的意思
//windows最早不支持java, 沒有JRE, 后來Sun公司打官司贏了, windows開始默認支持JRE.
ExtClassLoader: //擴展類加載器
//負責加載: jre\lib\ext\* 檔案夾下所有的jar包
//這兩個加載器執行完畢后, JVM虛擬機基本上就初始化完畢了.
APPClassLoader: //應用程式類加載器
//負責加載: 用戶自定義的類的.
//就是加載: 用戶配置的classpath環境變數值的.
//UserClassLoader //自定義類加載器
//自定義類加載器就是自定義一個類繼承ClassLoader, 然后重寫findClass(), loadClass()兩個方法即可.
加載順序是: BootStrap --> ExtClassLoader --> AppClassLoader --> UserClassLoader
代碼演示:
1) 隨便撰寫一個A類, 然后演示: jar包的加載程序(rt.jar, ext\*等相關的jar包)
2) 列印類加載器物件:
//1. 獲取當前執行緒的類加載器
ClassLoader load = Thread.currentThread().getContextClassLoader();
//2. 列印當前執行緒的類加載器.
System.out.println(load); //AppClassLoader
//3. 列印當前執行緒的類加載器的父類(加載器).
System.out.println(load.getParent()); //ExtClassLoader
//4. 列印當前執行緒的類加載器的父類的父類(加載器).
System.out.println(load.getParent().getParent()); //null: 其實應該是BootStrapClassLoader, 但是它是C語言寫的, 所以列印不出來.
04) JVM虛擬機類加載機制(二):檢查順序
剛才我們學完了JVM類加載機制的"加載循序", 現在, 我們來研究下它的"檢查順序", 請你思考,
假設: D:\compile, ext\*.jar, rt.jar三類中都有 A.class, 那么A.class是否會被加載3次, 如果不會, 它的加載順序是什么樣的?
不會, BootStrap會加載A.class.
運行順序是:
bootstrap --> ext --> app
1) bootstrap先加載 A.class
2) ext檢查A.class是否加載:
是: 不加載A.class
否: 加載A.class
3) app檢查A.class是否加載:
是: 不加載A.class
否: 加載A.class
例如:
UserClassLoader
APPClassLoader
ExtClassLoader
BootStrapClassLoader
總結:
自上而下檢查, 自下而上運行.
05) JVM的記憶體模型(方法區, 堆區, 堆疊區, 程式計數器)
到目前為止我們已經知道類加載器是用來加載位元組碼檔案的, 那加載完位元組碼檔案之后, 是不是要運行起來啊?
那它是怎么運行的呢? 在我的課件中有一個"JVM運行時記憶體資料區", 接下來我們詳細的來學習一下.
1) A.class位元組碼檔案被加載到記憶體.
//存盤在方法區中, 并且方法區中也包含常量池.
2) 創建本類的實體物件, 存盤在堆中(heap)
3) 通過物件名.的形式呼叫方法, 方法執行程序是在: 虛擬機堆疊中完成的.
//一個執行緒對應一個虛擬機堆疊, 每一個方法對應一個: 虛擬機堆疊中的堆疊幀
4) 程式計數器區域記錄的是當前程式的執行位置, 例如:
執行緒1: print(), 第3行
5) 將具體要執行的代碼交給: 執行引擎來執行.
6) 執行引擎呼叫: 本地庫介面, 本地方法庫來執行具體的內容.
//這部分了解即可, 用native修飾的方法都是本地方法.
7) 本地方法堆疊: 顧名思義, 就是本地方法執行的區域.(C語言, 外部庫運行的空間)
//了解即可.
8) 直接記憶體: 大白話翻譯, 當JVM記憶體不夠用的時候, 會找作業系統"借點"記憶體.
//了解即可.
06) JVM的一個小例子
1) 撰寫源代碼.
//創建一個A類, 里邊有個print()方法.
public class A {
public void print() {
System.out.println("h");
System.out.println("e");
System.out.println("l");
System.out.println("l");
System.out.println("o");
}
}
2) 在A類中, 撰寫main()函式, 創建兩個執行緒, 分別呼叫A#print()方法.
/*
java A //運行Java程式
加載類:
1) bootstrap 加載rt.jar
2) ext 加載 jre\lib\ext\*.jar
3) app 加載 A.class
具體運行:
1) 主函式運行. 堆疊中有個主執行緒, 呼叫MainThread.main();
2) 執行第23行, A a = new A(); 將a物件存盤到堆區.
3) 執行第24行, 呼叫a.print()方法, 生成一個堆疊幀, 壓入主執行緒堆疊.
-----> 執行, 運行print()方法的5行代碼.
4) 堆疊中有個新的執行緒, t1,
t1 --> run堆疊幀 --> print堆疊幀
5) 堆疊中有個新的執行緒, t2,
t2 --> run堆疊幀 --> print堆疊幀
*/
public class A {
public void print() {
System.out.println("h");
System.out.println("e");
System.out.println("l");
System.out.println("l");
System.out.println("o");
}
public static void main(String[] args) {
A a = new A();
a.print();
//創建兩個執行緒物件, 呼叫A#print();
//執行緒是CPU運行的基本單位, 創建銷毀由作業系統執行.
new Thread(new Runnable() {
@Override
public void run() {
a.print();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
a.print();
}
}).start();
}
}
3) 畫圖演示此代碼的執行流程.
4) 時間夠的情況下, 演示下: 守護執行緒和非守護執行緒.
07) 執行緒安全和記憶體溢位的問題
到目前為止, 大家已經知道了JVM的記憶體模型, 也知道了各個模塊的作用,
接下來, 請你思考一個問題: 上述的模塊中, 哪些模塊會出現執行緒安全的問題,
哪些模塊有記憶體溢位的問題?
舉例:
public class A{
int i;
public void add() {
i++;
}
}
//當兩個執行緒同時呼叫add()方法修改變數i的值時, 就會引發執行緒安全問題.
畫圖演示上述代碼.
結論:
1) 存在執行緒安全問題的模塊.
堆: 會. //多執行緒, 并發, 操作同一資料.
堆疊: 不會. //執行緒堆疊之間是相互獨立的.
方法區: 不會. //存盤常量, 類的描述資訊(.class位元組碼檔案).
程式計數器:不會.//記錄程式的執行流程.
2) 存在記憶體溢位問題的模塊.
堆: 會. //不斷創建物件, 記憶體被撐爆.
堆疊: 會. //不斷呼叫方法, 記憶體被撐爆.
方法區: 會. //常量過多, jar包過大, 記憶體被撐爆.
程式計數器: 會. //理論上來講會, 因為執行緒過多, 導致計數器過多, 記憶體被撐爆.
其實我們研究JVM性能優化, 研究的就是這兩個問題, 這兩個問題也是常見面試題.
//面試題:說一下你對 執行緒安全和記憶體溢位這兩個問題的看法.
總結:
研究這兩個問題, 其實主要研究的還是"堆(Heap)記憶體".
08) JDK1.7的堆記憶體的垃圾回收演算法
JDK1.7 將堆記憶體劃分為3部分: 年輕代, 年老代, 持久代(就是方法區).
年輕代又分為三個區域: //使用的是 復制演算法(需要有足夠多的空閑空間).
Eden: 伊甸園
//存盤的新生物件, 當伊甸園滿的時候, 會將存活物件復制到S1區.
//并移除那些垃圾物件(空指標物件).
Survivor: 幸存者區1
//當該區域滿的時候, 會將存活物件復制到S2區
//并移除那些垃圾物件.
Survivor: 幸存者區2
//當該區域滿的時候, 會將存活物件復制到S1區.
//并移除那些垃圾物件.
大白話翻譯:
s1區 和 s2區是來回互相復制的.
年老代: //使用的是標記清除演算法, 標記整理演算法.
//當物件在S1區和S2區之間來回復制15次, 才會被加載到: 年老代.
//當年輕代和年老代全部裝滿的時候, 就會報: 堆記憶體溢位.
持久代: //就是方法區
存盤常量, 類的描述資訊(也叫: 元資料).
09) JDK1.7默認垃圾回收器 //所謂的回收器, 就是已經存在的產品, 可以直接使用.
Serial收集器:
單執行緒收集器, 它使用一個CPU或者一個執行緒來回收物件,
它在垃圾收集的時候, 必須暫停其他作業執行緒, 直到垃圾回收完畢.
//類似于: 國家領匯入出行(封路), 排隊點餐(遇到插隊現象)
//假設它在回收垃圾的時候用了3秒, 其他執行緒就要等3秒, 這樣做效率很低.
ParNew收集器:
多執行緒收集器, 相當于: Serial的多執行緒版本.
Parallel Scavenge收集器:
是一個新生代的收集器,并且使用復制演算法,而且是一個并行的多執行緒收集器.
其他收集器是盡量縮短垃圾收集時用戶執行緒的停頓時間,而Parallel Scavenge收集器的目標是達到一個可控制的吞吐量:
吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間+垃圾收集時間)
(虛擬機總共運行100分鐘,垃圾收集時間為1分鐘,那么吞吐量就是99%)
//因為虛擬機會根據系統運行情況進行自適應調節, 所以不需要我們設定.
CMS收集器: //主要針對于年老代.
整個程序分為:
初始標記; //用戶執行緒等待
并發標記; //用戶執行緒可以執行
重新標記; //用戶執行緒等待
并發清除; //用戶執行緒可以執行
可以理解為是:
精細化運營, 前邊的垃圾收集器都是一刀切(在回收垃圾的時候, 其他執行緒等待), 而CMS是盡可能的降低等待時間, 并行執行程式, 提高運行效率.
以上為JDK1.7及其以前的垃圾回收器, JDK1.8的時候多了一個: G1.
G1在JDK1.9的時候, 成為了默認的垃圾回收器.
10) VM宏觀結構梳理
1) Java程式的三個階段:
撰寫: A.java
編譯: javac A.java
運行: java A.class
2) 類加載器
bootstrap
ext
app
3) JVM的記憶體結構
堆:
年輕代
年老代
持久代(也就是方法區)
元資料(類的描述資訊, 也就是.class位元組碼檔案), 常量池
堆疊:
有n個執行緒堆疊, 每個執行緒堆疊又會有n個堆疊幀(一個堆疊幀就是一個方法)
程式計數器:
用來記錄程式的執行流程的.
本地方法堆疊:
C語言, 外部程式運行空間.
11) G1垃圾回收器
在上個圖解上做優化, 用G1新圖解, 覆寫之前堆中的內容.
1) 將記憶體劃分為同樣大小的region(區域).
2) 每個region既可以是年輕代, 也可以是老年代, 還可以是幸存者區.
3) 程式運行前期, 創建大量物件的時候, 可以將每個region看做是: Eden(伊甸園).
4) 程式運行中期, 可以將eden的region變成old的region.
5) 程式運行后期, 可以縮短Eden, Survivor的區域, 變成Old區域.
//這樣做的好處是: 盡可能大的利用堆記憶體空間.
6) H: 存盤大物件的.
7) G1是JDK1.8出來的, 在JDK1.9的時候變成了: 默認垃圾處理器.
12) G1中的持久代(方法區)不見了
方法區從JVM模型中遷移出去了, 完全使用系統的記憶體.
方法區也改名叫: 元資料區.
13) 記憶體溢位的代碼演示
1) 堆記憶體溢位演示: main.java.heap.PrintGC_demo.java
//創建物件多, 導致記憶體溢位.
2) 堆疊記憶體溢位演示:
main.java.stack.StackOverFlow(遞回導致的)
//不設定的話在5000次左右, 設定256K后在1100次左右.
main.java.stack.Thread(不斷創建執行緒導致的)
//這個自行演示即可, 電腦太卡, 影響上課效果.
3) 方法區記憶體溢位演示:
main.java.method.MethodOOM //常量過多
main.java.direct.DirectMenOOM //jar包過大, 直接溢位.
總結:
可能你未來的10年都碰不到JVM性能調優這個事兒, 先不說能不能調優, 而是大多數的
公司上來就擼代碼, 很少會有"JVM調優"這個動作, 即使遇到了"JVM調優", 公司里邊
還有架構師呢, 但是我們馬上要找作業了, 把這些相關的題了解了解, 看看, 對面試會
比較有幫助.
//JVM調優一般是只看, 不用, 目前只是為了面試做準備.
14) 參考地址值比較
直接演示src.main.method.ATest類中的代碼即可.
//講解==比較參考型別的場景.
15) JVM調優案例賞析
百度搜索 --> JVM調優實踐, 一搜一大堆的案例.
16) GC的調優工具jstat //主要針對于GC的.
1) 通過Dos命令運行 D:\compile\Worker.java
2) 重新開啟一個Dos視窗:
//可以通過jps指令查看pid值.
jstat -class 2041(Java程式的PID值) //查看加載了多少個類
jstat -compiler 2041(Java程式的PID值) //查看編譯的情況
jstat -gc 2041(Java程式的PID值) //查看垃圾回收的統計
jstat -gc 2041 1000 5 //1秒列印1次, 總共列印5次
17) GC的調優工具jmap //主要針對于記憶體使用情況的.
1) 通過Dos命令運行 D:\compile\Worker.java
2) jmap -heap 2041(Java程式的PID值) //查看記憶體使用情況
jmap -histo 2041 | more //查看記憶體中物件數量及大小
jmap -dump:format=b,file=d:/compile/dump.dat 2041 //將記憶體使用情況dump到檔案中
jhat -port 9999 d:/compile/dump.dat //通過jhat對dump檔案進行分析
//埠號可以自定義, 然后在瀏覽器中通過127.0.0.1:9999就可以訪問了.
18) GC的調優工具jstack-死鎖 //針對于執行緒的.
1) 執行緒的六種狀態:
新建, 就緒, 運行(運行的時候會發生等待或者阻塞), 死亡.
2) 撰寫一個死鎖的代碼.
//兩個執行緒, 兩把鎖, 一個先拿鎖1, 再拿鎖2, 另一個先拿鎖2, 在拿鎖1.
3) 通過jstack命令可以查看Java程式狀態.
jstack 2041 //查看死鎖狀態
19) GC的可視化調優工具 //jstat, jmap, jstack
1) 本地調優.
1.1) 該工具位于 JDK安裝目錄/bin/jvisualvm.exe
//雙擊可以直接使用.
1.2) 以IntelliJ Platform為例, 演示下各個模塊的作用.
1.3) 該工具涵蓋了上述所有的命令.
2) 遠程調優. //自行測驗(目前先了解即可).
java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=9999 DeadLock
這幾個引數的意思是:
-Dcom.sun.management.jmxremote :允許使用JMX遠程管理
-Dcom.sun.management.jmxremote.port=9999 :JMX遠程連接埠
-Dcom.sun.management.jmxremote.authenticate=false :不進行身份認證,任何用戶都可以連接
-Dcom.sun.management.jmxremote.ssl=false :不使用ssl
20) JVM的總結
1) 什么是JVM?
2) JVM類加載機制.
//bootstrap, ext, app
3) JVM記憶體模型.
4) 垃圾回收演算法.
復制演算法:
針對于年輕代.
標記清除演算法:
標記整理演算法:
針對于老年代
5) JVM垃圾回收器.
Serial單執行緒.
ParNew多執行緒.
Parallel Scavenge: 并發多執行緒.
CMS: 以獲取"最短垃圾回收停頓時間"為目標的收集器.
G1: JDK1.8出現的, JDK1.9被設定成默認垃圾回收器.
6) JVM調優工具:
jstat, jmap, jstack, 可視化調優工具(jvisualvm.exe).
//以下內容是為了面試用, 找作業前一周, 看看下面的題即可.
21) JVM的執行緒安全與鎖的兩種方式
執行緒安全:
多執行緒, 并發, 操作同一資料, 就有可能引發安全問題, 需要用到"同步"解決.
"同步"分類:
同步代碼塊:
格式:
synchronized(鎖物件) {
//要加鎖的代碼
}
注意:
1) 同步代碼塊的鎖物件可以是任意型別的物件.
//物件多, 類鎖均可.
2) 必須使用同一把鎖, 否則可能出現鎖不住的情況. //String.class
同步方法:
靜態同步方法:
鎖物件是: 該類的位元組碼檔案物件. //類鎖
非靜態同步方法:
鎖物件是: this //物件鎖
22) 臟讀-高圓圓是男的
1) 演示main.java.thread.DirtyRead.java類的代碼即可.
2) 自定義執行緒修改姓名后, 要休眠3秒, 而主執行緒休眠1秒后即呼叫getValue()列印姓名和年齡,
如果getValue()方法沒加同步, 會出現"臟讀"的情況.
23) 了解Lock鎖.
1) Lock和synchronized的區別
1.1) synchronized是java內置的語言,是java的關鍵字
1.2) synchronized不需要手動去釋放鎖,當synchronized方法或者synchronized代碼塊執行完畢,
系統會自動釋放對該鎖的占用,
而lock必須手動的釋放鎖,如果沒有主動的釋放鎖,則可能造成死鎖的問題
2) 示例代碼
public class Demo02 {
private Lock lock = new ReentrantLock();
public void method01() {
lock.lock();
System.out.print("i");
System.out.print("t");
System.out.print("c");
System.out.print("a");
System.out.print("s");
System.out.print("t");
System.out.println();
lock.unlock();
}
public void method02() {
lock.lock();
System.out.print("我");
System.out.print("愛");
System.out.print("你");
System.out.print("中");
System.out.print("國");
System.out.println();
lock.unlock();
}
}
https://blogs.oracle.com/jonthecollector/our-collectors
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/46501.html
標籤:大數據
