JVM探究
一、面試題
1.1、請談談你對JVM的理解?java8虛擬機和之前的變化更新?
1.2、什麼是OOM,什麼是棧溢出StackOverFlowError?怎麼分析?
1.3、JVM的常用調優參數有哪些?
1.4、內存快照如何抓取,怎麼分析Dump文件?知道嗎?
1.5、談談JVM中,類加載器的認知?
二、參考學習的視頻和文章:
本篇主要學自b站狂神,知識淺顯,待進一步學習和完善,學無止盡!
2.1視頻:
2.2文章:
三、核心知識
3.1、JVM的位置
3.2、JVM的體系結構
3.3、類加載器
- Bootstrap ClassLoader:最頂層的加載類,主要加載核心類庫,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等;
- Extention ClassLoader:擴展的類加載器,加載目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件;
- Appclass Loader:也稱爲SystemAppClass,加載當前應用的classpath的所有類;
package com.lxf.demo5;
public class Car {
public static void main(String[] args) {
//類是模板
Car car1=new Car();
Car car2=new Car();
Car car3=new Car();
//打印每個car對象的hashCode,發現不一樣
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
//打印每個car對象的class,發現一樣
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
ClassLoader classLoader = aClass1.getClassLoader();
//打印類加載器
System.out.println(classLoader);//AppClassLoader
System.out.println(classLoader.getParent());//ExtClassLoader 目錄位置:\jre\lib\ext
System.out.println(classLoader.getParent().getParent());//null java程序獲取不到(底層c、c++寫的) rt.jar
}
}
3.4、雙親委派機制
package java.lang;
public class String {
/*
*JVM加載一個class時先查看是否已經加載過,沒有則通過父加載器,然後遞歸下去,直到BootstrapClassLoader,如果 *BootstrapClassloader找到了,直接返回,如果沒有找到,則一級一級返回(查看規定加載路徑),最後到達自身去查找這些對象。這 *種機制就叫做雙親委託
*
*作用:
*1.避免重複加載
*2.防止惡意加載
*3.編寫惡意類java.lang.Object,自定義加載替換系統原生類;
*/
public String toString(){
return "hello";
}
public static void main(String[] args) {
String s=new String();
s.toString();
}
}
3.5、沙箱安全機制
學習文章:沙箱安全機制
3.6、Native(本地方法)
package com.lxf.demo5;
public class Demo {
public static void main(String[] args) {
new Thread(()->{
},"my thread name").start();
}
//native:凡是帶了native 關鍵字的,說明java的作用域範圍抵達不到,會去調用底層c語言的庫!
//會進入本地方法棧
//調用本地方法接口 JNI
//JNI作用:擴展java的使用,融合不同的編程語言爲java所用! 最初:c,c++
//java誕生的時候:C、C++ 橫行,想要立足,必須要有調用C、C++的程序
//它在內存區域中專門開啓一塊標記區域:Native Method Stack,登記了native方法
//最終執行的時候,通過JNI加載本地方法庫中的方法
//例如Java程序驅動打印機,管理系統
private native void start0();
}
3.7、PC寄存器
程序計數器:Program Counter Register
每個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區的方法字節碼(用來存儲指向一條指令的地址,也將要執行的指令代碼),在執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不計。
3.8、方法區
Method Area方法區:
方法區是被所有的線程共享,所有字段和方法字節碼,以及一些特殊方法,如構造函數,接口代碼也在此定義,簡單說,所有 定義的方法的信息都保存在該區域,此區域屬於共享空間;
靜態變量、常量、類信息(構造函數、接口定義)、運行時的常量池存在方法區中,但是實例變量存在堆內存中,和方法區無關
比如:static、final、Class、常量池(jdk1.7在堆,jdk1.8在元空間)
3.9、棧
存放:八大基本類型、類對象的引用類型
一:在方法中聲明的變量,即該變量是局部變量,每當程序調用方法時,系統都會爲該方法建立一個方法棧,其所在方法中聲明的變量就放在方法棧中,當方法結束系統會釋放方法棧,其對應在該方法中聲明的變量隨着棧的銷燬而結束,這就局部變量只能在方法中有效的原因
在方法中聲明的變量可以是基本類型的變量,也可以是引用類型的變量。
- 當聲明是基本類型的變量的時,其變量名及值(變量名及值是兩個概念)是放在JAVA虛擬機棧中
- 當聲明的是引用變量時,所聲明的變量(該變量實際上是在方法中存儲的是內存地址值)是放在JAVA虛擬機的棧中,該變量所指向的對象是放在堆類存中的。
二:在類中聲明的變量是成員變量,也叫全局變量,放在堆中的(因爲全局變量不會隨着某個方法執行結束而銷燬)。
同樣在類中聲明的變量即可是基本類型的變量 也可是引用類型的變量
- 當聲明的是基本類型的變量其變量名及其值放在堆內存中的
- 引用類型時,其聲明的變量仍然會存儲一個內存地址值,該內存地址值指向所引用的對象。引用變量名和對應的對象仍然存儲在相應的堆中
3.10、三種JVM
- Sun公司
HotSpot
(常用) - BEA
JRocjit
- IBM
J9 VM
3.11、堆
一個JVM只有一個堆內存,堆內存的大小是可以調節的
類、方法、常量、變量,保存我們所有的引用類型的真實對象;
堆內存中還要細分爲三個區域:
- 新生代
- 伊甸園區
- from區
- to區
- 老年代
- 永久區(jdk1.8後改爲元空間)
2.12、新生代
- 類:誕生和成長的地方,甚至死亡
- 伊甸園區:所有的對象都是在伊甸園區創建的
2.13、老年代
當年輕帶隨着不斷地Minor GC ,from survivor中的對象會不斷成長,當from survivor中的對象成長大15歲的時候,就會進入老年代,在老年代的就能存儲很久了,只有重GC時纔會來清理老年代中的垃圾
3.14、永久區(jdk改名元空間,效果也有改變)
這個區域常駐內存的。用來存放JDK自身攜帶的Class對象。Interface元數據,存儲的是Java運行時的一些環境,這個區域不存在垃圾回收!關閉VM虛擬機就會釋放這個區域的內存
當一個啓動類加載了大量的第三方jar包。tomcat部署了太多的應用,大量動態生成的反射類,不斷的被加載。直到內存滿,就會出現OOM;
- jdk1.6之前:永久代,常量池是在方法區
- jdk1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中
- jdk1.8之後:無永久代,常量池在元空間
3.15、堆內存調優
1.設置jvm最大內存和初始化內存大小
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
-Xms:設置初始化內存分配大小(默認1/64)
-Xms:設置最大分配內存(默認1/4)
PrintGCDetails :打印GC垃圾回收線程清理信息
package com.lxf.demo5;
public class MyTest {
public static void main(String[] args) {
long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
System.out.println("max = " + max/1024/1024);
System.out.println("total = " + total/1024/1024);
}
}
2.內存快照分析:
MAT插件:eclipse集成的
Jprofiler插件
MAT,Jprofiler作用:
- 分析Dump內存文件,快速定位內存泄漏
- 獲得堆中的數據
- 獲得大的對象
- …
下載Jprofiler
- 在Jprofier官網下載安裝
- idea下載jprofiler插件
- 註冊碼:[email protected]#36573-fdkscp15axjj6#25257
- 配置idea:
測試:
package com.lxf.demo5;
import java.util.ArrayList;
public class Demo2 {
byte[] array=new byte[1*1024*1024];//1m
public static void main(String[] args) {
ArrayList<Demo2> list=new ArrayList<Demo2>();
int count=0;
try {
while(true){
list.add(new Demo2());
count++;
}
} catch (OutOfMemoryError e) {
System.out.println("count = " + count);
e.printStackTrace();
}
}
}
報錯:
count = 819
java.lang.OutOfMemoryError: Java heap space//堆內存滿了
at com.lxf.demo5.Demo2.<init>(Demo2.java:6)
at com.lxf.demo5.Demo2.main(Demo2.java:13)
打印Dump文件配置:
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
HeapDumpOnOutOfMemoryError:報OOM錯時生成Dump文件
再次運行Demo2:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid21752.hprof ...
Heap dump file created [7901177 bytes in 0.036 secs]//生成Dump文件
count = 6
java.lang.OutOfMemoryError: Java heap space
at com.lxf.demo5.Demo2.<init>(Demo2.java:7)
at com.lxf.demo5.Demo2.main(Demo2.java:14)
打開Demo.java文件的位置,向上一直找到src平級,找到java_pid21752.hprof文件,雙擊打開
3.16、GC(垃圾回收)
概述:
-
常用算法:複製算法、標記整理(壓縮)、引用計數器、標記清除法
-
JVM在進行GC時,並不是對這三個區域統一回收。大部分的時候,回收都是新生代
-
GC兩種類:輕GC(普通的GC)、重GC(全局GC)
題目:
- JVM的內存模型和分區-詳細到每個區
- 堆裏面的分區有哪些?Eden,from,to,老年代,說說他們的特點!
- GC的算法有哪些?複製算法、標記整理(壓縮)、引用計數器、標記清除法,怎麼用?
- 輕GC和重GC分別在什麼時候發生?
引用計數法:
複製算法:
解釋:年輕代每次GC後,倖存下來的會進入一個倖存區,這個倖存區就叫from,第二次GC後,再將from還在用的和Eden Space倖存下來的交給to區(from中的內容通過複製算法轉移),此時to變from,from變to(清空變to)。
-
好處:沒有內存的碎片
-
壞處:浪費了內存空間:多了一半空間永遠是空to。假設對象100%存活(極端情況),就極大浪費資源
-
複製算法最佳使用場景:對象存活度較低的時候(新生區);
標記清除法:
- 優點:不需要額外的空間!
- 缺點:兩次掃描,嚴重浪費時間,會產生內存碎片
標記壓縮(標記清除的優化):
總結:
- 內存效率:複製算法>標記清除算法>標記壓縮算法(時間複雜度)
- 內存整齊度:複製算法=標記清除算法>標記清除算法
- 內存利用率:標記壓縮算法=標記清除算法>複製算法
思考:
- 問題:難道沒有最優的算法?
- 答案:沒有,沒有最好的算法,只有最合適的算法–>GC:分代收集算法
- 年輕代:存活率底–>複製算法
- 老年代:區域大,存活率高–>標記清除+標記壓縮混合 實現