3-JVM虛擬機(jī)
靈魂三問(wèn):
JVM是什么?
- JVM廣義上指的是一種規(guī)范。狹義上的是JDK中的JVM虛擬機(jī)。
為什么要學(xué)習(xí)JVM?
-
面試過(guò)程中,經(jīng)常會(huì)被問(wèn)到JVM。
-
研發(fā)過(guò)程中,肯定會(huì)面臨一些重難點(diǎn)問(wèn)題與JVM有關(guān)系。例如:線程死鎖、內(nèi)存溢出、項(xiàng)目性能優(yōu)化等等。
-
基礎(chǔ)不牢,地動(dòng)山搖。想深入掌握J(rèn)ava這門(mén)語(yǔ)言,JVM始終是繞不過(guò)去的那座大山,早晚得攀。
怎么學(xué)習(xí)JVM?
JVM虛擬機(jī)部分,我們是這么安排的:
-
JVM基本常識(shí)
-
類(lèi)加載子系統(tǒng)
-
運(yùn)行時(shí)數(shù)據(jù)區(qū)
-
一個(gè)對(duì)象的一生(出生、死亡與內(nèi)涵)
-
GC垃圾收集器
-
JVM調(diào)優(yōu)相關(guān)工具與可調(diào)參數(shù)
-
調(diào)優(yōu)實(shí)戰(zhàn)案例
1.JVM虛擬機(jī)概述
1.1 JVM 基本常識(shí)
什么是JVM?
平時(shí)我們所說(shuō)的JVM廣義上指的是一種規(guī)范。狹義上的是JDK中的JVM虛擬機(jī)。JVM的實(shí)現(xiàn)是由各個(gè)廠商來(lái)做的。比如現(xiàn)在流傳最廣泛的是hotspot。其他實(shí)現(xiàn):BEA公司 JRocket、IBM j9、zing 號(hào)稱(chēng)世界最快JVM、taobao.vm。從廣義上講Java,Kotlin、Clojure、JRuby、Groovy等運(yùn)行于Java虛擬機(jī)上的編程語(yǔ)言及其相關(guān)的程序都屬于Java技術(shù)體系中的一員。
Java技術(shù)體系主要包括如下四個(gè)方面。
-
Java程序設(shè)計(jì)語(yǔ)言
-
Java類(lèi)庫(kù)API
-
來(lái)自商業(yè)機(jī)構(gòu)和開(kāi)源社區(qū)的第三方Java類(lèi)庫(kù)
- Apache
- 等等
-
Java虛擬機(jī):各種硬件平臺(tái)上的Java虛擬機(jī)實(shí)現(xiàn)
可以簡(jiǎn)單類(lèi)比一下:Java虛擬機(jī)是宿主,Java代碼開(kāi)發(fā)的程序則寄生在宿主上!
JVM架構(gòu)圖
Java和JVM的關(guān)系:
1.2 類(lèi)加載子系統(tǒng)
1.2.1 類(lèi)加載的時(shí)機(jī)
類(lèi)加載主要有四個(gè)時(shí)機(jī):
- 遇到new 、 getstatic 、 putstatic和invokestatic這四條指令時(shí),如果對(duì)應(yīng)的類(lèi)沒(méi)有初始化,則要對(duì)對(duì)應(yīng)的類(lèi)先進(jìn)行初始化。
public class Student{
private static int age ;
public static void method(){
}
}
//Student.age
//Student.method();
//new Student();
- 使用java.lang.reflect 包方法時(shí),對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候。
Class c = Class.forname("com.hero.Student");
-
初始化一個(gè)類(lèi)的時(shí)候發(fā)現(xiàn)其父類(lèi)還沒(méi)初始化,要先初始化其父類(lèi)
-
當(dāng)虛擬機(jī)開(kāi)始啟動(dòng)時(shí),用戶需要指定一個(gè)主類(lèi)(main),虛擬機(jī)會(huì)先執(zhí)行這個(gè)主類(lèi)的初始化。
1.2.2 類(lèi)加載的過(guò)程
類(lèi)加載主要做三件事:
-
全限定名稱(chēng) ==> 二進(jìn)制字節(jié)流加載class文件
-
字節(jié)流的靜態(tài)數(shù)據(jù)結(jié)構(gòu) ==> 方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
-
創(chuàng)建字節(jié)碼Class對(duì)象
一個(gè)類(lèi)的一生:
可以從哪些途徑加載字節(jié)碼?
1.2.3 類(lèi)加載器
JVM的類(lèi)加載是通過(guò)ClassLoader及其子類(lèi)來(lái)完成的。
-
檢查順序是自底向上:加載過(guò)程中會(huì)先檢查類(lèi)是否被已加載,從Custom
ClassLoader到BootStrap ClassLoader逐層檢查,只要某個(gè)classloader已加載就視為已加載此類(lèi),保證此類(lèi)只所有 ClassLoader加載一次。 -
加載的順序是自頂向下:也就是由上層來(lái)逐層嘗試加載此類(lèi)。
-
啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader):
- 負(fù)責(zé)加載 JAVA_HOME\lib 目錄中的,或通過(guò)-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機(jī)認(rèn)可(按文件名識(shí)別,如rt.jar)的類(lèi)。由C++實(shí)現(xiàn),不是ClassLoader的子類(lèi)
-
擴(kuò)展類(lèi)加載器(Extension ClassLoader):
- 負(fù)責(zé)加載 JAVA_HOME\lib\ext 目錄中的,或通過(guò)java.ext.dirs系統(tǒng)變量指定路徑中的類(lèi)庫(kù)。
-
應(yīng)用程序類(lèi)加載器(Application ClassLoader):負(fù)責(zé)加載用戶路徑classpath上的類(lèi)庫(kù)
-
自定義類(lèi)加載器(User ClassLoader):
- 作用:JVM自帶的三個(gè)加載器只能加載指定路徑下的類(lèi)字節(jié)碼,如果某些情況下,我們需要加載應(yīng)用程序之外的類(lèi)文件呢?就需要用到自定義類(lèi)加載器,就像是在汽車(chē)行駛的時(shí)候,為汽車(chē)更換輪子。
- 比如本地D盤(pán)下的,或者去加載網(wǎng)絡(luò)上的某個(gè)類(lèi)文件,這種情況就可以使用自定義加載器了。
- 舉個(gè)栗子:JRebel
自定義類(lèi)加載器案例:
目標(biāo):自定義類(lèi)加載器,加載指定路徑在D盤(pán)下的lib文件夾下的類(lèi)。
步驟:
-
新建一個(gè)需要被加載的類(lèi)Test.jave
-
編譯Test.jave到指定lib目錄
-
自定義類(lèi)加載器HeroClassLoader繼承ClassLoader:重寫(xiě)findClass()方法調(diào)用defineClass()方法
-
測(cè)試自定義類(lèi)加載器
實(shí)現(xiàn):
(1)新建一個(gè)Test.java 類(lèi),代碼如下:
package com.hero.jvm.classloader;
public class Test {
public void say(){
System.out.println("Hello HeroClassLoader");
}
}
(2)使用javac Test.java 命令,將生成的Test.class 文件放到D:/lib/com/hero/jvm/classloader 文件夾下。
(3)自定義類(lèi)加載器,代碼如下:
package com.hero.jvm.classloader;
import java.io.*;
public class HeroClassLoader extends ClassLoader {
private String classpath;
public HeroClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
try {
//輸入流,通過(guò)類(lèi)的全限定名稱(chēng)加載文件到字節(jié)數(shù)組
byte[] classDate = getData(name);
if (classDate != null) {
//defineClass方法將字節(jié)數(shù)組數(shù)據(jù) 轉(zhuǎn)為 字節(jié)碼對(duì)象
return defineClass(name, classDate, 0, classDate.length);
}
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
//加載類(lèi)的字節(jié)碼數(shù)據(jù)
private byte[] getData(String className) throws IOException {
String path = classpath + File.separatorChar +className.replace('.',File.separatorChar) + ".class";
try (InputStream in = new FileInputStream(path);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
(4)測(cè)試,代碼如下:
package com.hero.jvm.classloader;
import java.lang.reflect.Method;
public class TestMyClassLoader {
public static void main(String[] args) throws Exception{
//自定義類(lèi)加載器的加載路徑
HeroClassLoader hClassLoader=new HeroClassLoader("D:\\lib");
//包名+類(lèi)名
Class c=hClassLoader.loadClass("com.hero.jvm.classloader.Test");
if(c!=null){
Object obj=c.newInstance();
Method method=c.getMethod("say", null);
method.invoke(obj, null);
System.out.println(c.getClassLoader().toString());
}
}
}
輸出結(jié)果如下:
1.2.4 雙親委派模型與打破雙親委派
1)什么是雙親委派?
- 當(dāng)一個(gè)類(lèi)加載器收到類(lèi)加載任務(wù),會(huì)先交給其父類(lèi)加載器去完成。因此,最終加載任務(wù)都會(huì)傳遞到頂層的啟動(dòng)類(lèi)加載器,只有當(dāng)父類(lèi)加載器無(wú)法完成加載任務(wù)時(shí),子類(lèi)才會(huì)嘗試執(zhí)行加載任務(wù)。
Oracle 官網(wǎng)文檔描述:
The Java Class Loading Mechanism
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
------ Oracel Document
https://docs.oracle.com/javase/tutorial/ext/basics/load.html
看到這里,應(yīng)該叫父親委派對(duì)吧?那么為什么要叫雙親委派呢,因?yàn)樽钤绲姆g者,導(dǎo)致雙親委派的概念流行起來(lái)了。
2)為什么需要雙親委派呢?
-
考慮到安全因素,雙親委派可以避免重復(fù)加載,當(dāng)父親已經(jīng)加載了該類(lèi)的時(shí)候,就沒(méi)有必要子 ClassLoader再加載一次。
-
比如:加載位于rt.jar包中的類(lèi)java.lang.Object,不管是哪個(gè)加載器加載這個(gè)類(lèi),最終都是委托給頂層的啟動(dòng)類(lèi)加載器進(jìn)行加載,這樣就保證了使用不同的類(lèi)加載器最終得到的都是同樣一個(gè)Object對(duì)象。
3)雙親委派機(jī)制源碼:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 首先, 檢查class是否被加載,如果沒(méi)有加載則進(jìn)行加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//如果父類(lèi)加載不為空,則交給父類(lèi)加載器加載
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//父類(lèi)加載器沒(méi)有加載到,則由子類(lèi)進(jìn)行加載
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
4)為什么還需要破壞雙親委派?
-
在實(shí)際應(yīng)用中,雙親委派解決了Java基礎(chǔ)類(lèi)統(tǒng)一加載的問(wèn)題,但是卻存在著缺陷。JDK中的基礎(chǔ)類(lèi)作為典型的api被用戶調(diào)用,但是也存在api調(diào)用用戶代碼的情況,典型的如:SPI代碼。這種情況就需要打破雙親委派模式。
-
舉個(gè)栗子:數(shù)據(jù)庫(kù)驅(qū)動(dòng)DriverManager。以Driver接口為例,Driver接口定義在JDK中,其實(shí)現(xiàn)由各個(gè)數(shù)據(jù)庫(kù)的服務(wù)商來(lái)提供,由系統(tǒng)類(lèi)加載器加載。這個(gè)時(shí)候就需要啟動(dòng)類(lèi)加載器來(lái)委托子類(lèi)來(lái)加載Driver實(shí)現(xiàn),這就破壞了雙親委派。類(lèi)似情況還有很多
5)如何破壞雙親委派?
第一種方式
在 jdk 1.2 之前,那時(shí)候還沒(méi)有雙親委派模型,不過(guò)已經(jīng)有了 ClassLoader這個(gè)抽象類(lèi),所以已經(jīng)有人繼承這個(gè)抽象類(lèi),重寫(xiě) loadClass方法來(lái)實(shí)現(xiàn)用戶自定義類(lèi)加載器。
而在 1.2 的時(shí)候要引入雙親委派模型,為了向前兼容, loadClass 這個(gè)方法還得保留著使之得以重 寫(xiě),新搞了個(gè) findClass 方法讓用戶去重寫(xiě),并呼吁大家不要重寫(xiě) loadClass只要重寫(xiě) findClass。這就是第一次對(duì)雙親委派模型的破壞,因?yàn)殡p親委派的邏輯在 loadClass 上,但是又允許重寫(xiě) loadClass,重寫(xiě)了之后就可以破壞委派邏輯了。
第二種方式:
雙親委派機(jī)制是一種自上而下的加載需求,越往上類(lèi)越基礎(chǔ)。
SPI代碼打破了雙親委派
DriverManager源碼
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers =ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
//在這里需要加載各個(gè)廠商實(shí)現(xiàn)的數(shù)據(jù)庫(kù)驅(qū)動(dòng)com.mysql.jdbc.Driver
Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
如果出現(xiàn)SPI相關(guān)代碼時(shí),我們應(yīng)該如何解決基礎(chǔ)類(lèi)去加載用戶代碼類(lèi)呢?
這個(gè)時(shí)候,JVM不得不妥協(xié),推出線程上下文類(lèi)加載器的概念,去解決該問(wèn)題。這樣也就打破了雙親委派
線程上下文類(lèi)加載器 (ThreadContextClassLoader)
設(shè)置線程上下文類(lèi)加載器源碼
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
// 擴(kuò)展類(lèi)加載器
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError("Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
// 應(yīng)用類(lèi)加載器/系統(tǒng)類(lèi)加載器
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError("Could not create application class loader", e);
}
// 線程上下文類(lèi)加載器
// 同時(shí)為原始線程設(shè)置上下文類(lèi)加載器
Thread.currentThread().setContextClassLoader(loader);
// 最后,如果需要,安裝安全管理器
String s = System.getProperty("java.security.manager");
if (s != null) {
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager) loader.loadClass(s).newInstance();
} catch (IllegalAccessException | InstantiationException | ClassNotFoundException | ClassCastException e) {
// 處理異常,此處省略具體操作
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError("Could not create SecurityManager: " + s);
}
}
獲取線程上下文類(lèi)加載器源碼
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
第三種方式
為了滿足熱部署、不停機(jī)更新需求。OSGI 就是利用自定義的類(lèi)加載器機(jī)制來(lái)完成模塊化熱部署,而它實(shí)現(xiàn)的類(lèi)加載機(jī)制就沒(méi)有完全遵循自下而上的委托,有很多平級(jí)之間的類(lèi)加載器查找,具體就不展開(kāi)了,有興趣可以自行研究一下。
1.3 運(yùn)行時(shí)數(shù)據(jù)區(qū)
整個(gè)JVM構(gòu)成里面,由三部分組成:類(lèi)加載系統(tǒng)、運(yùn)行時(shí)數(shù)據(jù)區(qū)、執(zhí)行引擎
按照線程使用情況和職責(zé)分成兩大類(lèi)
- 線程獨(dú)享 (程序執(zhí)行區(qū)域)
- 不需要垃圾回收
- 虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器
- 線程共享 (數(shù)據(jù)存儲(chǔ)區(qū)域)
- 垃圾回收
- 存儲(chǔ)類(lèi)的靜態(tài)數(shù)據(jù)和對(duì)象數(shù)據(jù)
- 堆和方法區(qū)
1.3.1 堆
Java堆在JVM啟動(dòng)時(shí)創(chuàng)建內(nèi)存區(qū)域去實(shí)現(xiàn)對(duì)象、數(shù)組與運(yùn)行時(shí)常量的內(nèi)存分配,它是虛擬機(jī)管理最大的,也是垃圾回收的主要內(nèi)存區(qū)域。
內(nèi)存劃分:
核心邏輯就是三大假說(shuō),基于程序運(yùn)行情況進(jìn)行不斷的優(yōu)化設(shè)計(jì)。
堆內(nèi)存為什么會(huì)存在新生代和老年代?
分代收集理論:當(dāng)前商業(yè)虛擬機(jī)的垃圾收集器,大多數(shù)都遵循了"分代收集"(Generational Collection)的理論進(jìn)行設(shè)計(jì),分代收集名為理論,實(shí)質(zhì)是一套符合大多數(shù)程序運(yùn)行實(shí)際情況的經(jīng)驗(yàn)法則,它建立在兩個(gè)分代假說(shuō)之上:
-
弱分代假說(shuō)(Weak Generational Hypothesis):絕大多數(shù)對(duì)象都是朝生夕滅的。
-
強(qiáng)分代假說(shuō)(Strong Generational Hypothesis):熬過(guò)越多次垃圾收集過(guò)程的對(duì)象就越難以消亡。
這兩個(gè)分代假說(shuō)共同奠定了多款常用的垃圾收集器的一致的設(shè)計(jì)原則:收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后將回收對(duì)象依據(jù)其年齡(年齡即對(duì)象熬過(guò)垃圾收集過(guò)程的次數(shù))分配到不同的區(qū)域之中存儲(chǔ)。
-
如果一個(gè)區(qū)域中大多數(shù)對(duì)象都是朝生夕滅,難以熬過(guò)垃圾收集過(guò)程的話,那么把它們集中放在一起,每次回收時(shí)只關(guān)注如何保留少量存活而不是去標(biāo)記那些大量將要被回收的對(duì)象,就能以較低代價(jià)回收到大量的空間;
-
如果剩下的都是難以消亡的對(duì)象,那把它們集中放在一塊,虛擬機(jī)便可以使用較低的頻率來(lái)回收這個(gè)區(qū)域。
這就同時(shí)兼顧了垃圾收集的時(shí)間開(kāi)銷(xiāo)和內(nèi)存的空間有效利用。
為什么新生代里面需要有兩個(gè)Survivor區(qū)域呢?
- 這個(gè)咱們?cè)诶占餍」?jié)進(jìn)行解釋
內(nèi)存模型變遷:
JDK1.7:
-
Young 年輕區(qū) :主要保存年輕對(duì)象,分為三部分,Eden區(qū)、兩個(gè)Survivor區(qū)。
-
Tenured 年老區(qū):主要保存年長(zhǎng)對(duì)象,當(dāng)對(duì)象在Young復(fù)制轉(zhuǎn)移一定的次數(shù)后,對(duì)象就會(huì)被轉(zhuǎn)移到Tenured區(qū)。
-
Perm 永久區(qū) :主要保存class、method、filed對(duì)象,這部份的空間一般不會(huì)溢出,除非一次性加載了很多的類(lèi),不過(guò)在涉及到熱部署的應(yīng)用服務(wù)器的時(shí)候,有時(shí)候會(huì)遇到OOM : PermGen space 的錯(cuò)誤。
-
Virtual區(qū): 最大內(nèi)存和初始內(nèi)存的差值,就是Virtual區(qū)。
JDK1.8:
-
由2部分組成,新生代(Eden + 2\Survivor ) + 年老代(OldGen )
-
JDK1.8中變化最大的是Perm永久區(qū)用Metaspace進(jìn)行了替換
注意:Metaspace所占用的內(nèi)存空間不是在虛擬機(jī)內(nèi)部,而是在本地內(nèi)存空間中。區(qū)別于JDK1.7
JDK1.9:
-
取消新生代、老年代的物理劃分
-
將堆劃分為若干個(gè)區(qū)域(Region),這些區(qū)域中包含了有邏輯上的新生代、老年代區(qū)域
儲(chǔ)物收納
內(nèi)存信息案例:
package com.hero.jvm.memory;
/**
* -Xms100m -Xmx100m
*/
public class HeapDemo {
public static void main(String[] args) {
System.out.println("======start=========");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("========end=========");
}
}
JDK6堆內(nèi)存結(jié)構(gòu)
C:\develop\java\jdk1.6.0_45\bin\javac HeapDemo.java
C:\develop\java\jdk1.6.0_45\bin\java -Xms100m -Xmx100m HeapDemo
C:\develop\java\jdk1.6.0_45\bin\jmap -heap 3612
JDK7堆內(nèi)存結(jié)構(gòu)
C:\develop\java\jdk1.7.0_80\bin\javac HeapDemo.java
C:\develop\java\jdk1.7.0_80\bin\java -Xms100m -Xmx100m HeapDemo
C:\develop\java\jdk1.7.0_80\bin\jmap -heap 10420
JDK8堆內(nèi)存結(jié)構(gòu)
C:\develop\java\jdk1.8.0_251\bin\javac HeapDemo.java
C:\develop\java\jdk1.8.0_251\bin\java -Xms100m -Xmx100m HeapDemo
C:\develop\java\jdk1.8.0_251\bin\jmap -heap 18276
JDK11堆內(nèi)存結(jié)構(gòu)
C:\develop\java\jdk-11.0.7\bin\javac HeapDemo.java
C:\develop\java\jdk-11.0.7\bin\java -Xms100m -Xmx100m HeapDemo
C:\develop\java\jdk-11.0.7\bin\jhsdb jmap --heap --pid 19380
1.3.2 虛擬機(jī)棧
1)棧幀是什么?
棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。
棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息。每一個(gè)方法從調(diào)用至執(zhí)行完成的過(guò)程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里從入棧到出棧的過(guò)程。
棧內(nèi)存為線程私有的空間,每個(gè)線程都會(huì)創(chuàng)建私有的棧內(nèi)存,生命周期與線程相同,每個(gè)Java方法在執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)。棧內(nèi)存大小決定了方法調(diào)用的深度,棧內(nèi)存過(guò)小則會(huì)導(dǎo)致方法調(diào)用的深度較小,如遞歸調(diào)用的次數(shù)較少。
2)當(dāng)前棧幀
一個(gè)線程中方法的調(diào)用鏈可能會(huì)很長(zhǎng),所以會(huì)有很多棧幀。只有位于JVM虛擬機(jī)棧棧頂?shù)脑夭攀怯行У?,即稱(chēng)為當(dāng)前棧幀,與這個(gè)棧幀相關(guān)連的方法稱(chēng)為當(dāng)前方法,定義這個(gè)方法的類(lèi)叫做當(dāng)前類(lèi)。
執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作。如果當(dāng)前方法調(diào)用了其他方法,或者當(dāng)前方法執(zhí)行結(jié)束,那這個(gè)方法的棧幀就不再是當(dāng)前棧幀了。
3)什么時(shí)候創(chuàng)建棧幀
調(diào)用新的方法時(shí),新的棧幀也會(huì)隨之創(chuàng)建。并且隨著程序控制權(quán)轉(zhuǎn)移到新方法,新的棧幀成為了當(dāng)前棧幀。方法返回之際,原棧幀會(huì)返回方法的執(zhí)行結(jié)果給之前的棧幀(返回給方法調(diào)用者),隨后虛擬機(jī)將會(huì)丟棄此棧幀。
4)棧異常的兩種情況:
如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度(Xss默認(rèn)1m),會(huì)拋出StackOverflowError異常如果在創(chuàng)建新的線程時(shí),沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧,會(huì)拋出OutOfMemoryError異常
【不一定】
5)棧異常案例:
如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將會(huì)拋出StackOverflowError異常(-Xss);
package com.hero.jvm.memory;
public class StackErrorMock {
private static int index = 1;
public void call() {
index++;
call();
}
public static void main(String[] args) {
StackErrorMock mock = new StackErrorMock();
try {
mock.call();
} catch (Throwable e) {
System.out.println("Stack deep: " + index);
e.printStackTrace();
}
}
}
C:\develop\java\jdk1.8.0_251\bin\javac StackErrorMock.java
C:\develop\java\jdk1.8.0_251\bin\java -Xss1m StackErrorMock
C:\develop\java\jdk1.8.0_251\bin\java -Xss256k StackErrorMock
補(bǔ)充案例:用來(lái)演示大量創(chuàng)建線程撐爆內(nèi)存會(huì)發(fā)生什么!
思考題:如果創(chuàng)建海量線程線程的時(shí)候,同時(shí)每個(gè)線程瘋狂遞歸,請(qǐng)問(wèn)到底是先OOM還是 StackOverflowError?
public class TestThread {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Thread("Thread-" + i) {
@Override
public void run() {
try {
String name = Thread.currentThread().getName();
System.out.println(name);
recursive(30000);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
}
}.start();
}
}
public static void recursive(double d) {
if (d == 0)
return;
recursive(d - 1);
}
}
1.3.3 本地方法棧
本地方法棧和虛擬機(jī)棧相似,區(qū)別就是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java服務(wù)(字節(jié)碼服務(wù)),而本地方法棧為虛擬機(jī)使用到的Native方法(比如C++方法)服務(wù)。
簡(jiǎn)單地講,一個(gè)Native Method就是一個(gè)Java調(diào)用非Java代碼的接口。
public class IHaveNatives {
native public void Native1(int x);
native static public long Native2();
native synchronized private float Native3(Object o);
native void Native4(int[] ary) throws Exception;
}
為什么需要本地方法?
Java是一門(mén)高級(jí)語(yǔ)言,我們不直接與操作系統(tǒng)資源、系統(tǒng)硬件打交道。如果想要直接與操作系統(tǒng)與硬件打交道,就需要使用到本地方法了。說(shuō)白了,Java可以直接通過(guò)native方法調(diào)用cpp編寫(xiě)的接口!多線程底層就是這么實(shí)現(xiàn)的,在多線程部分我們會(huì)看一下Thread實(shí)現(xiàn)的源碼,到時(shí)候就可以理解了。
1.3.4 方法區(qū)
方法區(qū)(Method Area)是可供各個(gè)線程共享的運(yùn)行時(shí)內(nèi)存區(qū)域,方法區(qū)本質(zhì)上是Java語(yǔ)言編譯后代碼存儲(chǔ)區(qū)域,它存儲(chǔ)每一個(gè)類(lèi)的結(jié)構(gòu)信息,例如:運(yùn)行時(shí)常量池、成員變量、方法數(shù)據(jù)、構(gòu)造方法和普通方法的字節(jié)碼指令等內(nèi)容。很多語(yǔ)言都有類(lèi)似區(qū)域。
方法區(qū)的具體實(shí)現(xiàn)有兩種:永久代(PermGen)、元空間(Metaspace)
1)方法區(qū)存儲(chǔ)什么數(shù)據(jù)?
{width=“4.999998906386701in”
height=“3.3958333333333335in”}
主要有如下三種類(lèi)型第一:Class
-
類(lèi)型信息,比如Class(com.hero.User類(lèi))
-
方法信息,比如Method(方法名稱(chēng)、方法參數(shù)列表、方法返回值信息)
-
字段信息,比如Field(字段類(lèi)型,字段名稱(chēng)需要特殊設(shè)置才能保存的?。?/p>
-
類(lèi)變量(靜態(tài)變量):JDK1.7之后,轉(zhuǎn)移到堆中存儲(chǔ)
-
方法表(方法調(diào)用的時(shí)候)
在A類(lèi)的main方法中去調(diào)用B類(lèi)的method1方法,是根據(jù)B類(lèi)的方法表去查找合適的方法,進(jìn)行調(diào)用的。
第二:運(yùn)行時(shí)常量池(字符串常量池):從class中的常量池加載而來(lái),JDK1.7之后,轉(zhuǎn)移到堆中存儲(chǔ)
-
字面量類(lèi)型
-
引用類(lèi)型–>內(nèi)存地址
第三:JIT編譯器編譯之后的代碼緩存
如果需要訪問(wèn)方法區(qū)中類(lèi)的其他信息,都必須先獲得Class對(duì)象,才能取訪問(wèn)該Class對(duì)象關(guān)聯(lián)的方法信息或者字段信息。
2)永久代和元空間的區(qū)別是什么?
-
JDK1.8之前使用的方法區(qū)實(shí)現(xiàn)是永久代,JDK1.8及以后使用的方法區(qū)實(shí)現(xiàn)是元空間。
-
存儲(chǔ)位置不同:
-
永久代所使用的內(nèi)存區(qū)域是JVM進(jìn)程所使用的區(qū)域,它的大小受整個(gè)JVM的大小所限制。
-
元空間所使用的內(nèi)存區(qū)域是物理內(nèi)存區(qū)域。那么元空間的使用大小只會(huì)受物理內(nèi)存大小的限制。
- 存儲(chǔ)內(nèi)容不同:
-
永久代存儲(chǔ)的信息基本上就是上面方法區(qū)存儲(chǔ)內(nèi)容中的數(shù)據(jù)。
-
元空間只存儲(chǔ)類(lèi)的元信息,而靜態(tài)變量和運(yùn)行時(shí)常量池都挪到堆中。
3)為什么要使用元空間來(lái)替換永久代?
-
字符串存在永久代中,容易出現(xiàn)性能問(wèn)題和永久代內(nèi)存溢出。
-
類(lèi)及方法的信息等比較難確定其大小,因此對(duì)于永久代的大小指定比較困難,太小容易出現(xiàn)永久代溢出,太大則容易導(dǎo)致老年代溢出。
-
永久代會(huì)為 GC 帶來(lái)不必要的復(fù)雜度,并且回收效率偏低。
-
Oracle 計(jì)劃將HotSpot 與 JRockit 合二為一。
方法區(qū)實(shí)現(xiàn)變遷歷史:
{width=“6.160161854768154in”
height=“3.147603893263342in”}
移除永久代的工作從JDK1.7就開(kāi)始了。JDK1.7中,存儲(chǔ)在永久代的部分?jǐn)?shù)據(jù)就已經(jīng)轉(zhuǎn)移到了Java Heap。但永久代仍存在于JDK1.7中,并沒(méi)完全移除,譬如:字面量轉(zhuǎn)移到了java heap;類(lèi)的靜態(tài)變量(class statics)轉(zhuǎn)移到了java heap。
4)字符串OOM異常案例案例代碼
以下這段程序以2的指數(shù)級(jí)不斷的生成新的字符串,這樣可以比較快速的消耗內(nèi)存:
package com.hero.jvm.memory;
import java.util.ArrayList;
import java.util.List;
public class StringOomMock {
static String base = "string";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = base + base;
base = str;
list.add(str.intern());
}
}
}
JDK1.6
JDK 1.6 的運(yùn)行結(jié)果:{width=“5.867308617672791in”
height=“0.5523392388451444in”}
在JDK 1.6下,會(huì)出現(xiàn)永久代的內(nèi)存溢出。
JDK1.7
C:\develop\java\jdk1.7.0_80\bin\javac StringOomMock.java
C:\develop\java\jdk1.7.0_80\bin\java -XX:PermSize=8m -XX:MaxPermSize=8m -
Xmx16m StringOomMock
JDK 1.7的運(yùn)行結(jié)果:{width=“5.902015529308836in”
height=“0.9854155730533684in”}
在JDK 1.7中,會(huì)出現(xiàn)堆內(nèi)存溢出。
結(jié)論是:JDK 1.7 已經(jīng)將字符串常量由永久代轉(zhuǎn)移到堆中。
JDK1.8+
C:\develop\java\jdk1.8.0_251\bin\javac StringOomMock.java
C:\develop\java\jdk1.8.0_251\bin\java -XX:PermSize=8m -XX:MaxPermSize=8m -
Xmx16m StringOomMock
JDK 1.8的運(yùn)行結(jié)果:
{width=“5.782699037620297in”
height=“1.0887489063867017in”}
在JDK 1.8 中,也會(huì)出現(xiàn)堆內(nèi)存溢出,并且顯示 JDK 1.8中 PermSize 和 MaxPermGen 已經(jīng)無(wú)效。
結(jié)論是:可以驗(yàn)證 JDK 1.8 中已經(jīng)不存在永久代的結(jié)論。
1.3.5 字符串常量池
1)三種常量池的比較
class常量池:一個(gè)class文件只有一個(gè)class常量池
字面量:數(shù)值型(int、float、long、double)、雙引號(hào)引起來(lái)的字符串值等符號(hào)引用:Class、Method、Field等
運(yùn)行時(shí)常量池:一個(gè)class對(duì)象有一個(gè)運(yùn)行時(shí)常量池
字面量:數(shù)值型(int、float、long、double)、雙引號(hào)引起來(lái)的字符串值等符號(hào)引用:Class、Method、Field等
字符串常量池:全局只有一個(gè)字符串常量池
雙引號(hào)引起來(lái)的字符串值
{width=“6.202195975503062in”
height=“3.19in”}
2)字符串常量池如何存儲(chǔ)數(shù)據(jù)?
為了提高匹配速度, 即更快的查找某個(gè)字符串是否存在于常量池 Java 在設(shè)計(jì)字符串常量池的時(shí)候,還搞了一張StringTable, StringTable里面保存了字符串的引用。StringTable類(lèi)似于HashTable(哈希表)。在JDK1.7+,StringTable可以通過(guò)參數(shù)指定-XX:StringTableSize=99991
什么是哈希表呢?
哈希表(Hash table,也叫散列表),是根據(jù)關(guān)鍵碼值(Key value)而直接進(jìn)行訪問(wèn)的數(shù)據(jù)結(jié)構(gòu)。也就是說(shuō),它通過(guò)把關(guān)鍵碼值映射到表中一個(gè)位置來(lái)訪問(wèn)記錄,以加快查找的速度。這個(gè)映射函數(shù)叫做散列函數(shù),存放記錄的數(shù)組叫做散列表。
哈希表本質(zhì)上是一個(gè)數(shù)組+鏈表
目的 : 為了加快數(shù)據(jù)查找的速度。
存在問(wèn)題:hash沖突問(wèn)題,一旦出現(xiàn)沖突,那么就會(huì)形成鏈表,鏈表的特點(diǎn)是增刪快,但查詢(xún)慢。數(shù)組下標(biāo)計(jì)算公式:hash(字符串) % 數(shù)組長(zhǎng)度
數(shù)組中存儲(chǔ)的是Entry,通過(guò)指針next形成鏈表
{width=“4.741064085739283in”
height=“3.6678116797900264in”}
HashMap<String, Integer> map = new HashMap<>();
map.put("hello", 53);
map.put("world", 35);
map.put("java", 55);
map.put("world", 52);
map.put("通話", 51);
map.put("重地", 55);
3)字符串常量池如何查找字符串:
根據(jù)字符串的hashcode找到對(duì)應(yīng)entry如果沒(méi)有沖突,它可能只是一個(gè)entry
如何有沖突,它可能是一個(gè)entry的鏈表,然后Java再遍歷鏈表,匹配引用對(duì)應(yīng)的字符串如果找到字符串,返回引用
如果找不到字符串,在使用intern()方法的時(shí)候,會(huì)將intern()方法調(diào)用者的引用放入到stringtable中
{width=“4.354166666666667in”
height=“3.8333333333333335in”}
4)字符串常量池案例
import java.util.HashMap;
public class StringTableDemo {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("hello", 53);
map.put("world", 35);
map.put("java", 55);
map.put("world", 52);
map.put("通話", 51);
map.put("重地", 55);
test();
}
public static void test() {
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1 == str2); // false
String str3 = new String("abc");
System.out.println(str3 == str2); // false
String str4 = "a" + "b";
System.out.println(str4 == "ab"); // true
String s1 = "a";
String s2 = "b";
String str6 = s1 + s2;
System.out.println(str6 == "ab"); // false
String str7 = "abc".substring(0, 2);
System.out.println(str7 == "ab"); // false
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC"); // false
String s5 = "a";
String s6 = "abc";
String s7 = s5 + "bc";
System.out.println(s6 == s7.intern()); // true
}
}
總結(jié):
單獨(dú)使用"“引號(hào)創(chuàng)建的字符串都是常量,編譯期就已經(jīng)確定存儲(chǔ)到String Pool中。使用new String(”")創(chuàng)建的對(duì)象會(huì)存儲(chǔ)到heap中,是運(yùn)行期新創(chuàng)建的。
使用只包含常量的字符串連接符如"aa"+"bb"創(chuàng)建的也是常量,編譯期就能確定已經(jīng)存儲(chǔ)到StringPool中。
使用包含變量的字符串連接如"aa"+s創(chuàng)建的對(duì)象是運(yùn)行期才創(chuàng)建的,存儲(chǔ)到heap中。
運(yùn)行期調(diào)用String的intern()方法可以向String Pool中動(dòng)態(tài)添加對(duì)象。
1.3.6 程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register),也叫PC寄存器,是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼指令的行號(hào)指示器。字節(jié)碼解釋器的工作就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令。分支,循環(huán),跳轉(zhuǎn),異常處理,線程回復(fù)等都需要依賴(lài)這個(gè)計(jì)數(shù)器來(lái)完成。
為什么需要程序計(jì)數(shù)器?
由于Java虛擬機(jī)的多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(針對(duì)多核處理器來(lái)說(shuō)是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換(系統(tǒng)上下文切換)后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱(chēng)這類(lèi)內(nèi)存區(qū)域?yàn)?線程私有"的內(nèi)存。
存儲(chǔ)的什么數(shù)據(jù)?
如果一個(gè)線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是一個(gè)Native方法,這個(gè)計(jì)數(shù)器的值則為空。
異常:此內(nèi)存區(qū)域是唯一一個(gè)在Java的虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError異常情況的區(qū)域。
1.3.7 直接內(nèi)存
直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。在JDK1.4 中新加入了NIO(New Input/Output)類(lèi),引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O 方式,它可以使用native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來(lái)回復(fù)制數(shù)據(jù)。
本機(jī)直接內(nèi)存的分配不會(huì)受到Java 堆大小的限制,受到本機(jī)總內(nèi)存大小限制。
直接內(nèi)存(堆外內(nèi)存)與堆內(nèi)存比較:
-
直接內(nèi)存申請(qǐng)空間耗費(fèi)更高的性能,當(dāng)頻繁申請(qǐng)到一定量時(shí)尤為明顯
-
直接內(nèi)存IO讀寫(xiě)的性能要優(yōu)于普通的堆內(nèi)存,在多次讀寫(xiě)操作的情況下差異明顯
直接內(nèi)存案例:
package com.hero.jvm.memory;
import java.nio.ByteBuffer;
public class ByteBufferCompare {
public static void main(String[] args) {
//allocateCompare(); //分配比較
operateCompare(); //讀寫(xiě)比較
}
/**
* 直接內(nèi)存和堆內(nèi)存的分配空間比較
* 結(jié)論:在數(shù)據(jù)量提升時(shí),直接內(nèi)存相比非直接內(nèi)存的申請(qǐng),有很?chē)?yán)重的性能問(wèn)題
*/
public static void allocateCompare() {
int time = 1000 * 10000; //操作次數(shù),1千萬(wàn)
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
//ByteBuffer.allocate(int capacity) 分配一個(gè)新的字節(jié)緩沖區(qū)。
ByteBuffer buffer = ByteBuffer.allocate(2); //非直接內(nèi)存分配申請(qǐng)
}
long et = System.currentTimeMillis();
System.out.println("在進(jìn)行" + time + "次分配操作時(shí),堆內(nèi)存分配耗時(shí):" + (et - st) + "ms");
long st_heap = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
//ByteBuffer.allocateDirect(int capacity) 分配新的直接字節(jié)緩沖區(qū)。
ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接內(nèi)存分配申請(qǐng)
}
long et_direct = System.currentTimeMillis();
System.out.println("在進(jìn)行" + time + "次分配操作時(shí),直接內(nèi)存分配耗時(shí):" + (et_direct- st_heap) + "ms");
}
/**
* 直接內(nèi)存和堆內(nèi)存的讀寫(xiě)性能比較
* 結(jié)論:直接內(nèi)存在直接的IO操作上,在頻繁的讀寫(xiě)時(shí)會(huì)有顯著的性能提升
*/
public static void operateCompare() {
int time = 10 * 10000 * 10000; //操作次數(shù),10億
ByteBuffer buffer = ByteBuffer.allocate(2 * time);
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// putChar(char value) 用來(lái)寫(xiě)入 char 值的相對(duì) put 方法
buffer.putChar('a');
}
buffer.flip();
for (int i = 0; i < time; i++) {
buffer.getChar();
}
long et = System.currentTimeMillis();
System.out.println("在進(jìn)行" + time + "次讀寫(xiě)操作時(shí),非直接內(nèi)存讀寫(xiě)耗時(shí):" + (et - st) + "ms");
ByteBuffer buffer_d = ByteBuffer.allocateDirect(2 * time);
long st_direct = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// putChar(char value) 用來(lái)寫(xiě)入 char 值的相對(duì) put 方法
buffer_d.putChar('a');
}
buffer_d.flip();
for (int i = 0; i < time; i++) {
buffer_d.getChar();
}
long et_direct = System.currentTimeMillis();
System.out.println("在進(jìn)行" + time + "次讀寫(xiě)操作時(shí),直接內(nèi)存讀寫(xiě)耗時(shí):" + (et_direct - st_direct) + "ms");
}
}
輸出:
在進(jìn)行10000000次分配操作時(shí),堆內(nèi)存 分配耗時(shí):82ms
在進(jìn)行10000000次分配操作時(shí),直接內(nèi)存分配耗時(shí):6817ms
在進(jìn)行1000000000次讀寫(xiě)操作時(shí),堆內(nèi)存讀寫(xiě)耗時(shí):1137ms
在進(jìn)行1000000000次讀寫(xiě)操作時(shí),直接內(nèi)存讀寫(xiě)耗時(shí):512ms
為什么會(huì)是這樣?
從數(shù)據(jù)流的角度,來(lái)看
-
非直接內(nèi)存作用鏈:本地IO–>直接內(nèi)存–>非直接內(nèi)存–>直接內(nèi)存–>本地IO
-
直接內(nèi)存作用鏈:本地IO–>直接內(nèi)存–>本地IO
直接內(nèi)存的使用場(chǎng)景:
- 有很大的數(shù)據(jù)需要存儲(chǔ),它的生命周期很長(zhǎng)
- 適合頻繁的IO操作,例如:網(wǎng)絡(luò)并發(fā)場(chǎng)景
今日總結(jié)
0 1-JVM基本常識(shí)
什么是JVM?廣義上的JVM是指一種規(guī)范,狹義上的JVM指的是Hotspot類(lèi)的虛擬機(jī)實(shí)現(xiàn)
Java語(yǔ)言與JVM的關(guān)系:Java語(yǔ)言編寫(xiě)程序生成class字節(jié)碼在JVM虛擬機(jī)里執(zhí)行。其他語(yǔ)言也可以比如Scala、Groovy
學(xué)習(xí)JVM主要學(xué)啥?類(lèi)加載子系統(tǒng) --\ 運(yùn)行時(shí)數(shù)據(jù)區(qū) --\一個(gè)對(duì)象的一生–\ GC垃圾收集器
學(xué)了JVM可以干啥?JVM調(diào)優(yōu),底層能力 決定 上層建筑
0 2-類(lèi)加載子系統(tǒng)
類(lèi)加載四個(gè)時(shí)機(jī):1.new、getstatic、putstatic、invokestatic。2.反射。3.初始化子類(lèi)發(fā)現(xiàn)父類(lèi)沒(méi)有初始化時(shí)。4.main函數(shù)的類(lèi)
類(lèi)加載主要過(guò)程:加載 -> 驗(yàn)證 -> 準(zhǔn)備 -> 解析 ->初始化 ->使用 ->卸載
類(lèi)加載主要做了三件事:
全限定名稱(chēng) ==> 二進(jìn)制字節(jié)流加載class文件字節(jié)流的靜態(tài)數(shù)據(jù)結(jié)構(gòu) ==>
方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)創(chuàng)建字節(jié)碼Class對(duì)象
可以從哪些途徑加載字節(jié)碼:
Jar、war
JSP生成的class
數(shù)據(jù)庫(kù)中二進(jìn)制字節(jié)流網(wǎng)絡(luò)中二進(jìn)制字節(jié)流
動(dòng)態(tài)代理生成的二進(jìn)制字節(jié)流
類(lèi)加載器有哪些?
啟動(dòng)類(lèi)加載器BootstrapClassLoader,擴(kuò)展類(lèi)加載器ExtensionClassLoader應(yīng)用類(lèi)加載器ApplicationClassLoader,自定義類(lèi)加載器UserClassLoader檢查順序自底向上,加載順序自頂向下
什么是雙親委派?當(dāng)一個(gè)類(lèi)加載器收到加載任務(wù),會(huì)先交給其父類(lèi)加載器去加載
為何要打破雙親委派?父類(lèi)加載器加載范圍受限,無(wú)法加載的類(lèi)需要委托子類(lèi)加載器去完成加載
0 3-運(yùn)行時(shí)數(shù)據(jù)區(qū)
堆:JVM啟動(dòng)是創(chuàng)建的最大的一塊內(nèi)存區(qū)域,對(duì)象,數(shù)組,運(yùn)行時(shí)常量池都在這里內(nèi)存劃分:Eden、2個(gè)Survivor、老年代
為什么要?jiǎng)澐中律c老年代?基于分代收集理論里的量大假說(shuō),弱分代和強(qiáng)分代假說(shuō)。提升垃圾收集的效率。
內(nèi)存模型變遷史:JDK1.7 —取消永久代,多了元空間—> JDK1.8
—取消新生代與老年代物理劃分—> JDK1.9
虛擬機(jī)棧:??臻g為線程私有,每個(gè)線程都會(huì)創(chuàng)建棧內(nèi)存,生命周期與線程相同
線程內(nèi)的棧內(nèi)存占滿了會(huì)出現(xiàn)StackOverflowError
棧幀是什么?棧幀(StackFrame)是用于支持虛擬機(jī)進(jìn)行方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。
本地方法棧:與虛擬機(jī)棧類(lèi)似,區(qū)別在于本地方法棧為本地方法服務(wù),也就是native方法方法區(qū):方法區(qū)的實(shí)現(xiàn)有兩種:永久代(PermGen)、元空間(Metaspace)
方法區(qū)存什么數(shù)據(jù)?類(lèi)型信息,方法信息,字段信息,類(lèi)變量信息,方法表,指向類(lèi)加載器的引用,指向Class實(shí)例的引用
永久代和元空間有什么區(qū)別?
存儲(chǔ)位置不同存儲(chǔ)內(nèi)容不同
為什么要使用元空間來(lái)替換永久代?
基于性能、穩(wěn)定性、GC垃圾收集的復(fù)雜度考慮,當(dāng)然也有Oracle收購(gòu)了Java原因
字符串常量池 {#字符串常量池-1}
三種常量池:class常量池、運(yùn)行時(shí)常量池、字符串常量池
字符串常量池如何存儲(chǔ)數(shù)據(jù)?使用哈希表【哈希沖突,哈希碰撞…】字符串常量池如何查找字符串?類(lèi)似于HashMap
程序計(jì)數(shù)器
存儲(chǔ)什么數(shù)據(jù)?當(dāng)前線程執(zhí)行時(shí)的字節(jié)碼指令地址為什么需要程序計(jì)數(shù)器?因?yàn)橄到y(tǒng)的上下文切換
直接內(nèi)存
相對(duì)堆內(nèi)存,直接內(nèi)存申請(qǐng)空間更耗時(shí)文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-792760.html
直接內(nèi)存IO讀寫(xiě)的性能要優(yōu)于普通的堆內(nèi)存文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-792760.html
到了這里,關(guān)于03-JVM虛擬機(jī)-課堂筆記的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!