什么是JVM?
- JVM全稱(Java Virtual Machine),中譯為:Java虛擬機
- 本質:是一個運行在計算機上的程序
- 職責:運行Java字節碼文件(因為計算機只能認識機器碼文件,所以需要JVM將自字節碼轉為機器碼)
- 功能:
- 解釋和運行:字節碼轉機器碼
- 內存管理:自動為對象/方法分配內存,垃圾回收
- 即時編譯:對熱點代碼進行優化
常見的JVM
JVM的組成部分
類加載器:加載class文件
運行時數據區域:管理內存
執行引擎:垃圾回收器/即時編譯器/解釋器
本地接口:java使用native修飾的方法
字節碼文件的組成
- 打開方式:
- 字節碼是以二進制存儲,不能直接使用文本軟件打開
- 使用軟件:jclasslib-bytecode-viewer
MAC安裝命令: brew install jclasslib-bytecode-viewer
- 組成(5部分)
- 基礎信息:存儲java版本號,訪問標識
- 常量池:存儲字符串常量/類或接口名/字段名
- 字段:存儲變量名/變量類型/標識
- 方法:存儲方法原代碼指令
- 屬性:存儲類的屬性
字節碼文件組成詳解(基本信息/常量池/方法)
- 基本信息包含:Magic魔數/副版本號/主版本號/訪問標識/類等
- Magic魔數(固定字符):是字節碼文件的前幾個字節,用于確定文件的文件類型
- 主副版本號:主(大版本號 )使用當前的主版本號-44得到真正的jdk版本號* 例如:主版本號為:52,減完后得到jdk1.8
- 副版本號:為主版本號相同時,區分不同版本的標識
- 版本號的作用:判斷當前字節碼的版本和運行時jdk是否相兼容* 例如:報錯:<font style="color:#DF2A3F;">字節碼文件為jdk8,運行時jdk為6</font>
- 版本號不同的解決方案:
- 
字節碼文件的組成-常量池
- 常量池作用:避免相同的內容重復定義
字節碼文件的組成-方法
- 字節碼指令
- 字節碼執行過程:
iconst_0 # 現將o放入到操作數棧中,
istore_1 #0從操作數棧彈出,存儲到局部變量表中下標為1的位置中
注意:下標為0中存儲args數組
iload_1 # 將局部變量中的下標為1的內容復制到棧中
iinc1 by 1 將下標為1的值變為1
istore_1 # #0從操作數棧彈出,存儲到局部變量表中下標為1的位置中
return 返回
總結:最后的結果還是為0
字節碼文件-練習題:
0 iconst_01 istore_12 iconst_03 istore_24 iconst_05 istore_36 iinc 1 by 19 iload_2
10 iconst_1
11 iadd
12 istore_2
13 iinc 3 by 1
16 return
字節碼文件常用工具
- 命令:javap -v
- jclasslib插件
- 注意點:想看哪一個文件,先點擊文件后在點擊視圖打開jclasslib
- 注意點:修改后的文件,需要重新編譯后才會有對應的字節碼文件
- 阿里arthas
- jad命令可以將class文件還原java文件
類的生命周期
- 類的生命周期描述了一個類加載/使用/卸載的整個過程
- 概述:
- 生命周期分為5個階段:加載(Loading)》連接(Linking)〉初始化(Init)》使用(Using)〉卸載(Unloading)
- 注意:連接可以在分為三個階段為:驗證》準備〉解析
類的加載階段
- 第一步:類加載器根據類的全限定名通過不同渠道以二進制方式獲取字節碼信息
- 第二步:加載類完成后,JVM會將字節碼信息存到內存中的方法區中
- 生成一個instanceKlass對象,保存了類的所有信息(例如:基本信息,常量池/字段/方法等)
- 第三步:JVM還有在堆中生成一份與方法區數據類似的java.lang.Class對象
- 總結:類加載器根據類的類名會將類的字節碼信息存入到內存中,并在方法區的對區分別分配一個對象,保存類的信息
- 注意:方法區和堆區的對象是有關聯的
類的連接階段
- 驗證階段
- 舉例:
- 魔數校驗
- 舉例:
2. 元信息校驗(類必須有父類)3. 主版本號校驗
- 準備階段
- 準備階段:在堆區分配一塊空間給Student對象,并對屬性value賦默認值0
注意點:如果使用final修飾的話,在準備階段就是賦值
- 解析階段
- 作用:將常量池中的符號引用轉為直接引用
類加載器的初始化階段
- 行為:會執行靜態代碼塊中的代碼,并為靜態變量賦值
- 使用的字節碼命令:clinit
- 示例:
- 先賦值2,在賦值1,最后value中的值為:1
- 類初始化的幾種方式
- 訪問一個類的靜態變量或者靜態方法,
public class Test {public static void main(String[] args) {int i = Test1.i;System.out.println(i);}
}
class Test1{public static int i=0;static{System.out.println("init");}
}
- 調用Class.forName(String className)
public class Test {public static void main(String[] args) throws ClassNotFoundException {Class.forName("Test1");}
}
class Test1{static{System.out.println("init");}
}
- new一個該類的對象時
- 執行Main方法的當前類
public class Test {static {System.out.println("init Test");}public static void main(String[] args) throws ClassNotFoundException {Test1 test = new Test1();}
}
class Test1{static{System.out.println("init Test1");}
}
- 面試題(靜態代碼塊先執行,在執行main方法,在執行構造方法和構造器方法)
- 答案:DACBCB
- clinit不會出現的情況:
- 面試題2
- 練習題1
- 練習題2
- 總結
類加載器ClassLoader
- 用途:將二進制流》加載〉本地接口調用》生成方法區和堆區的對象
- 應用場景:
- SPI機制
- 類的熱部署
- Tomcat類的隔離
- 面試題:雙親委派機制,如何打破雙親,自定義類加載
類加載器的分類
- 兩類
- java虛擬機低層實現
- 例如:hotspot使用C++
- java代碼實現
- 根據需要自定義
- java虛擬機低層實現
- 特點:需要繼承ClassLoder
類加載器-啟動類加載器Bootstrap
- String類是由啟動類加載器加載的,但是在使用String.class.getClassLoader()方法時,返回的結果為null,因為,類加載器存在于jvm中,所以獲取不到。
- 命令:sc -d java.lang.String
class-loader 為空
- 如何使用啟動類加載器加載自定義的jar包
- 練習:使用方法二:
類加載器-擴展類加載器(Extension Class Loader)
- 默認加載Java安裝目錄/jre/lib/ext下的類文件
package cn.varin.Test;import jdk.nashorn.internal.runtime.ScriptEnvironment;import javax.script.ScriptEngineManager;
import java.io.IOException;public class Test {public static void main(String[] args) {ClassLoader classLoader = ScriptEnvironment.class.getClassLoader();System.out.println(classLoader);}}
- 將自己編寫的擴展jar包,添加到ext包下
類加載器-應用程序加載器(Application Class Loader)
- 程序員自己創建的類和第三方類會使用Application加載
package cn.varin.Test;import jdk.nashorn.internal.runtime.ScriptEnvironment;import javax.script.ScriptEngineManager;
import java.io.IOException;public class Test {public static void main(String[] args) {ClassLoader classLoader = Person.class.getClassLoader();System.out.println(classLoader);}}
class Person{private String name;private int age;
}
雙親委派機制
- 核心:解決一個類到底是由誰加載的問題
- 作用:
- 保證類加載的安全性
- 避免重復加載
- 解釋:
1. 示例1:假設cn.varin.Person對象加載,它會先找Application,如果沒有找到加載,繼續找Ext,沒有再繼續找 Boot,找到,返回 2. 示例2:加載cn.varin.Person對象都沒有被三個加載器加載過,它會從boot找,是否在路徑中,沒有的話,繼續找Ext,沒有的話,繼續找Application,找到,返回
問題:如果一個類重復出現在三個類加載器中,誰來加載
答案:啟動類加載器加載,根據雙親委派機制,它的優先級最高
問題2:在自己的項目中創建一個java.lang.String類,會被加載嗎
答案:不能,會返回啟動類加載器加載在rt包中的String類
package cn.varin.Test;import jdk.nashorn.internal.runtime.ScriptEnvironment;import javax.script.ScriptEngineManager;
import java.io.IOException;public class Test {public static void main(String[] args) throws ClassNotFoundException {ClassLoader classLoader = Test.class.getClassLoader();System.out.println(classLoader);Class<?> aClass = classLoader.loadClass("java.lang.String");System.out.println(aClass.getClassLoader());}}
# 結果為:Applicationnull
面試題:類的雙親委派機制是什么?
當一個類加載器區加載某一個類的時候,會自底向上的查詢父類是否加載過,
如果加載過,直接返回,
如果沒有加載過,會自頂向下查詢路徑進行加載。
app 的父為:Ext
Ext 的父為:Boot
打破雙親委派機制
- 打破雙親委派機制的三種方式
- 自定義類加載器
- 利用上下文類加載器
- 使用Osg框架的類加載器
- 需要打破雙親委派機制的場景
- 有兩個應用需要運行,但是兩個應用中的某一個類的全路徑名是相同的,假設A加載了,B再加載,類加載器會認為是A類
打破雙親委派機制-自定義類加載器
實現方法:重寫findClass方法,這樣就不會破壞雙親委派機制
打破雙親委派機制-線程上下問加載器
利用上下文類加載器加載類,比如:JDBC和JNDI
- 快速獲取到線程上下文類加載器
Thread.currentThread().getContextClassLoader()
- 總結
- 思考:JDBC案例是否真正的打破了雙親委派
打破雙親委派機制-Osgi模塊化(了解)
JDK9之后的類加載器
- 區別一
- 區別二:擴展類加載器
- 區別三:應用程序類加載器
類加載器小結
- 類加載器的作用
- 有幾種類加載器?
- 什么是雙親委派機制
- 怎么打破雙親委派機制
運行時數據區域
運行時數據區是:jVM在運行Java程序過程中管理的內存區域
- 分類:
運行時數據區域-程序計數器
程序計數器:
作用一:用于存放下一條指令需要執行的地址
作用二:在多線程執行下,程序計數器可以記錄CPU切換前每個線程解釋執行到那一條指令
問題:程序計數器會發生內存溢出嗎:
答案:不會,因為每個線程只存儲了一個固定長度的地址,是不會發生內存溢出的。
運行時數據區域-棧
- 分為了:
- Java虛擬機棧
- 保存在Java中實現的方法
- 本地方法棧
- 保存方法中有native關鍵字的
- 源代碼保存在c++中
運行時數據區域-棧-Java虛擬機棧
- 存儲順序:先進后出
- 棧幀的組成:
- 局部變量表:
- 方法執行過程中存放所有的局部變量
- 保存的內容:有實例的this對象,方法的參數,方法體中聲明的局部變量。
例題:
答案:6個
2. 操作數棧:1. 存放臨時數據,例如:常量 3. 棧數據1. 存儲:動態鏈接,方法出口,異常表
-
設置修改棧的大小
-
- 注意點:
運行時數據區-堆
- 堆內存是空間最大的一塊內存區域,創建出來的對象都存在于堆上
- 堆空間可以分為:use+total+max
- use:使用空間
- total(默認是系統內容的六十四分之一):剩余空間(可以動態增加, )
- max(默認分配系統內存的四分之一):最大可用空間
- 手動設置total和max
運行時數據區域-方法區
- 存儲 :
- 類的基本信息
- 運行時常量池
- 字符串常量池
- jdk7把方法區存儲在堆區域中的永久代空間
- jdk8將方法區存放在元空間中,云空間位于內存中,只要不超過內存,可以一直存
運行時數據區域-方法區-字符串常量池
- 代碼示例
String s1 = new String(“1”);
String s2 = “1”;
解釋:s1中的字符串是通過new對象創立的,內容會存儲在堆中
s2中的字符串是直接創立的,會存儲在方法區的字符串常量池中
s1和s2都是存儲在棧中,
-
運行時常量池和字符串常量池的區別
-
練習題1:
- false和true
- 練習題2
- 練習題3
- 總結
- 在jdk7及以后版本中,靜態變量是存儲在堆的Class對象中,脫離了永久代
直接內存
- 直接內存的作用:
- 適應NIO機制
- 提升IO操作效率
3. 在jdk8后,直接保存方法區中的數據
- 直接內存設置
運行時數據區域總結
- 運行時數據區分為幾部分,每一部分的作用是什么?
兩大部分:
線程不共享:
程序計數器:記錄當前要執行的字節碼指令的地址(不會出現內存溢出)
Java虛擬機棧:
本地方法棧:
Java虛擬機棧和本地方法棧都是采用棧式存儲,用于保存方法調用的基本數據(局部變量,操作數等)(會出現內存溢出)
線程共享:
方法區:主要存儲類的元信息,以及常量池(會出現內存溢出)
堆區: 存放創建出來的對象(會出現內存溢出 )
- 不同JDK版本直接的運行時數據區的區別是什么?
jdk6:
jdk7:
jdk8:
自動垃圾回收
范圍:負責對堆上的內存進行回收(回收不再使用的對象)
優點:降低程序員實現難度
自動垃圾回收-方法區回收
- 回收條件
- 擴展:System.gc()
- 作用:手動出發垃圾回收器
自動垃圾回收-堆區回收判斷-引用計數法
- 如何判斷堆上的對象有沒有被引用:使用引用計數法
- 每一個對象有一個引用計數器:當對象被引用時+1,取消引用-1
- 存在缺陷:
- 可能產生循環引用
自動垃圾回收-堆區回收判斷-可達性分析算法
- 可達性分析將對象分為兩類:
- 垃圾回收的根對象(GC root) :
- 普通對象
- 回收原理:可達性分析中,存在一個不可回收的GC Root對象, 該對象會引用其他對象,可達分析通過判斷,如果從GC Root開始找,沒有找到的對象就是可以回收的。?
- GC Root對象種類
- 線程Thread對象,
- 系統類加載器加載的Java.lang.Class對象
- 監視器對象,用來保存同步鎖synchroized關鍵字的對象
- 本地方法調用時使用的全局對象
- 可達性分析法中的引用屬于強引用
自動垃圾回收-軟引用
軟引用:如果一個對象只有軟引用,當程序內存不足時,就會將軟引用中的數據進行回收。
常用于:緩存中
如何實現軟引用:提供SoftReference類實現
案例分析:
當先A對象屬于一個強引用,不會不回收
此時:A對象為一個軟引用,可能被回收
- 注意點:
- SoftReferenc對象也需要被GC對象強引用,否則也會被回收
自動垃圾回收-弱引用(WaekReference)
- 弱引用和軟引用類似,不同點就是**軟引用是在內存不足時才會回收,但是弱引用不需要看內存夠不夠直接回收**。
自動垃圾回收-虛引用和終結器引用
- 虛引用作用:當對象被垃圾回收器回收時可以接收到對應的通知
- 終結器引用:
垃圾回收算法的評價標準
核心思想:找到內存中存活的對象 ,把不再存活的對象釋放
常見的垃圾回收算法:
標記-清除算法
復制算法
標記-整理算法
分代GC
評價標準:
STW(stop the world):停止所有的用戶線程的時間
吞吐量(越高效率越好):執行用戶代碼時間?(執行用戶代碼時間+GC使勁)
最大暫停時間:在垃圾回收時,STW時間的最大值
垃圾回收算法-標記清除算法
2個階段:
- 標記階段:將所有存活的對象標記(使用可達性分析法)
- 清除階段:從內存中刪除沒有被標記的對象
優點:實現簡單,第一階段將存活的標記為1,第二階段刪除非1的對象
缺點:
碎片化:對象刪除后,出現多個很小的可用單元;
分配速度慢
垃圾回收算法-復制算法
執行過程:
準備2塊空間(from和To),只使用from空間,
在垃圾回收階段,將存活的對象復制到to中
回收結束后,將from和to的名稱互換。
優點:吞吐量高,不會產生碎片化
缺點:內存使用效率低(每次只能使用一般的空間)
垃圾回收算法-標記-整理算法
2個階段:
- 標記階段:將所有存活的對象標記(使用可達性分析法)
- 整理階段:將存活的對象移動到堆的一端,清除掉間空隙和碎片
優點:內存使用效率高,不會產生碎片化
缺點:整理階段效率不高
垃圾回收算法-分代GC
- 分區
- 年輕代(yong區):使用復制算法
- 分為:
- 伊甸園區
- s0:from
- s1:to
- 老年代(old區)
- 年輕代到老年代:年輕代沒執行一次GC,會在存活對象標記一個屬性并加一,到屬性值達到某一個值,就會轉移到老年代
- 為什么要分為年輕代和老年代?
1.
垃圾回收算器
- 垃圾回收器的種類
組合:
G1垃圾回收器
垃圾回收器的選擇
自動垃圾回收總結
Java中有那幾塊內存需要進行垃圾回收
- 在運行時數據區總堆中的數據需要垃圾回收器進行回收
有哪幾種常見的引用類型
有哪幾種常見的垃圾回收算法
- 標記清除:標記可用的,沒用的清除
- 復制算法:分兩塊空間,將可用的復制同一塊區域中,清除掉另一塊,并交換名字
- 標記整理:將可用的標記并放到另一端后,將另一端清除
- 分代GC:分為年輕代和老年代,可以用多中回收算法
常見的垃圾回收器有哪些?
- serial和serial old:單線程回收,使用單核CPU場景
- parNew和CMS:暫停時間較短,適用于大型互聯網應用中與用戶交互的部分
- parallel Scavenge和Parallel old:吞吐量高,適用于后臺進行大量數據操作
- G1:適用于較大的堆,具有可控的暫停時間