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