1 JVM組成
1.1 JVM由那些部分組成,運(yùn)行流程是什么?
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆☆
JVM是什么
Java Virtual Machine Java程序的運(yùn)行環(huán)境(java二進(jìn)制字節(jié)碼的運(yùn)行環(huán)境)
好處:
- 一次編寫(xiě),到處運(yùn)行
- 自動(dòng)內(nèi)存管理,垃圾回收機(jī)制
JVM由哪些部分組成,運(yùn)行流程是什么?
從圖中可以看出 JVM 的主要組成部分
- ClassLoader(類(lèi)加載器)
- Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū),內(nèi)存分區(qū))
- Execution Engine(執(zhí)行引擎)
- Native Method Library(本地庫(kù)接口)
運(yùn)行流程:
(1)類(lèi)加載器(ClassLoader)把Java代碼轉(zhuǎn)換為字節(jié)碼
(2)運(yùn)行時(shí)數(shù)據(jù)區(qū)(Runtime Data Area)把字節(jié)碼加載到內(nèi)存中,而字節(jié)碼文件只是JVM的一套指令集規(guī)范,并不能直接交給底層系統(tǒng)去執(zhí)行,而是有執(zhí)行引擎運(yùn)行
(3)執(zhí)行引擎(Execution Engine)將字節(jié)碼翻譯為底層系統(tǒng)指令,再交由CPU執(zhí)行去執(zhí)行,此時(shí)需要調(diào)用其他語(yǔ)言的本地庫(kù)接口(Native Method Library)來(lái)實(shí)現(xiàn)整個(gè)程序的功能。
1.2 什么是程序計(jì)數(shù)器?
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆☆
程序計(jì)數(shù)器:線(xiàn)程私有的(每一個(gè)線(xiàn)程內(nèi)部都有一個(gè)程序計(jì)數(shù)器),內(nèi)部保存的字節(jié)碼的行號(hào)。用于記錄正在執(zhí)行的字節(jié)碼指令的地址。
javap -verbose xx.class 打印堆棧大小,局部變量的數(shù)量和方法的參數(shù)。
java虛擬機(jī)對(duì)于多線(xiàn)程是通過(guò)線(xiàn)程輪流切換并且分配線(xiàn)程執(zhí)行時(shí)間。在任何的一個(gè)時(shí)間點(diǎn)上,一個(gè)處理器只會(huì)處理執(zhí)行一個(gè)線(xiàn)程,如果當(dāng)前被執(zhí)行的這個(gè)線(xiàn)程它所分配的執(zhí)行時(shí)間用完了【掛起】。處理器會(huì)切換到另外的一個(gè)線(xiàn)程上來(lái)進(jìn)行執(zhí)行。并且這個(gè)線(xiàn)程的執(zhí)行時(shí)間用完了,接著處理器就會(huì)又來(lái)執(zhí)行被掛起的這個(gè)線(xiàn)程。
那么現(xiàn)在有一個(gè)問(wèn)題就是,當(dāng)前處理器如何能夠知道,對(duì)于這個(gè)被掛起的線(xiàn)程,它上一次執(zhí)行到了哪里?那么這時(shí)就需要從程序計(jì)數(shù)器中來(lái)回去到當(dāng)前的這個(gè)線(xiàn)程他上一次執(zhí)行的行號(hào),然后接著繼續(xù)向下執(zhí)行。
程序計(jì)數(shù)器是JVM規(guī)范中唯一一個(gè)沒(méi)有規(guī)定出現(xiàn)OOM的區(qū)域,所以這個(gè)空間也不會(huì)進(jìn)行GC。
1.3 你能給我詳細(xì)的介紹Java堆嗎?
Java堆事線(xiàn)程共享的區(qū)域,主要用來(lái)保存實(shí)例、數(shù)組等, 當(dāng)堆中沒(méi)有內(nèi)存空間可分配給實(shí)例,也無(wú)法擴(kuò)展的時(shí)候,會(huì)拋出OOM異常。
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆☆
線(xiàn)程共享的區(qū)域:主要用來(lái)保存對(duì)象實(shí)例,數(shù)組等,當(dāng)堆中沒(méi)有內(nèi)存空間可分配給實(shí)例,也無(wú)法再擴(kuò)展時(shí),則拋出OutOfMemoryError異常。
- 年輕代被劃分為三部分,Eden區(qū)和兩個(gè)大小嚴(yán)格相同的Survivor區(qū),根據(jù)JVM的策略,在經(jīng)過(guò)幾次垃圾收集后,任然存活于Survivor的對(duì)象將被移動(dòng)到老年代區(qū)間。
- 老年代主要保存生命周期長(zhǎng)的對(duì)象,一般是一些老的對(duì)象
- 元空間保存的類(lèi)信息、靜態(tài)變量、常量、編譯后的代碼
為了避免方法區(qū)出現(xiàn)OOM,所以在java8中將堆上的方法區(qū)【永久代】給移動(dòng)到了本地內(nèi)存上,重新開(kāi)辟了一塊空間,叫做元空間。那么現(xiàn)在就可以避免掉OOM的出現(xiàn)了。
元空間(MetaSpace)介紹
在 HotSpot JVM 中,永久代( ≈ 方法區(qū))中用于存放類(lèi)和方法的元數(shù)據(jù)以及常量池,比如Class 和 Method。每當(dāng)一個(gè)類(lèi)初次被加載的時(shí)候,它的元數(shù)據(jù)都會(huì)放到永久代中。
永久代是有大小限制的,因此如果加載的類(lèi)太多,很有可能導(dǎo)致永久代內(nèi)存溢出,即OutOfMemoryError,為此不得不對(duì)虛擬機(jī)做調(diào)優(yōu)。
那么,Java 8 中 PermGen 為什么被移出 HotSpot JVM 了?
官網(wǎng)給出了解釋?zhuān)篽ttp://openjdk.java.net/jeps/122
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
移除永久代是為融合HotSpot JVM與 JRockit VM而做出的努力,因?yàn)镴Rockit沒(méi)有永久代,不需要配置永久代。
1)由于 PermGen 內(nèi)存經(jīng)常會(huì)溢出,引發(fā)OutOfMemoryError,因此 JVM 的開(kāi)發(fā)者希望這一塊內(nèi)存可以更靈活地被管理,不要再經(jīng)常出現(xiàn)這樣的 OOM。
2)移除 PermGen 可以促進(jìn) HotSpot JVM 與 JRockit VM 的融合,因?yàn)?JRockit 沒(méi)有永久代。
準(zhǔn)確來(lái)說(shuō),Perm 區(qū)中的字符串常量池被移到了堆內(nèi)存中是在 Java7 之后,Java 8 時(shí),PermGen 被元空間代替,其他內(nèi)容比如**類(lèi)元信息、字段、靜態(tài)屬性、方法、常量**等都移動(dòng)到元空間區(qū)。比如 java/lang/Object 類(lèi)元信息、靜態(tài)屬性 System.out、整型常量等。
元空間的本質(zhì)和永久代類(lèi)似,都是對(duì) JVM 規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。
1.4 什么是虛擬機(jī)棧
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆☆
Java Virtual machine Stacks (java 虛擬機(jī)棧)
- 每個(gè)線(xiàn)程運(yùn)行時(shí)所需要的內(nèi)存,稱(chēng)為虛擬機(jī)棧,先進(jìn)后出
- 每個(gè)棧由多個(gè)棧幀(frame)組成,對(duì)應(yīng)著每次方法調(diào)用時(shí)所占用的內(nèi)存
- 每個(gè)線(xiàn)程只能有一個(gè)活動(dòng)棧幀,對(duì)應(yīng)著當(dāng)前正在執(zhí)行的那個(gè)方法
-
垃圾回收是否涉及棧內(nèi)存?
垃圾回收主要指就是堆內(nèi)存,當(dāng)棧幀彈棧以后,內(nèi)存就會(huì)釋放 -
棧內(nèi)存分配越大越好嗎?
未必,默認(rèn)的棧內(nèi)存通常為1024k(1M)
棧幀過(guò)大會(huì)導(dǎo)致線(xiàn)程數(shù)變少,例如,機(jī)器總內(nèi)存為512m,目前能活動(dòng)的線(xiàn)程數(shù)則為512個(gè),如果把棧內(nèi)存改為2048k,那么能活動(dòng)的棧幀就會(huì)減半 - **方法內(nèi)的局部變量是否線(xiàn)程安全? **
- 如果方法內(nèi)局部變量沒(méi)有逃離方法的作用范圍,它是線(xiàn)程安全的
- 如果是局部變量引用了對(duì)象,并逃離方法的作用范圍,需要考慮線(xiàn)程安全
- 比如以下代碼:
棧內(nèi)存溢出情況
- 棧幀過(guò)多導(dǎo)致棧內(nèi)存溢出,典型問(wèn)題:遞歸調(diào)用
- 棧幀過(guò)大導(dǎo)致棧內(nèi)存溢出
1.5 堆和棧的區(qū)別
難易程度:☆☆☆
方面:線(xiàn)程是否私有, 異常, 處理的問(wèn)題
出現(xiàn)頻率:☆☆☆
堆主要是解決實(shí)例管理的問(wèn)題,
棧解決的是程序運(yùn)行的問(wèn)題
本地方法棧與棧功能相同,是一個(gè)Java調(diào)用接口調(diào)用非Java的代碼
- 棧內(nèi)存一般會(huì)用來(lái)存儲(chǔ)局部變量和方法調(diào)用,但堆內(nèi)存是用來(lái)存儲(chǔ)Java對(duì)象和數(shù)組的的。堆會(huì)GC垃圾回收,而棧不會(huì)。
- 棧內(nèi)存是線(xiàn)程私有的,而堆內(nèi)存是線(xiàn)程共有的。
- 兩者異常錯(cuò)誤不同,但如果棧內(nèi)存或者堆內(nèi)存不足都會(huì)拋出異常。
??臻g不足:java.lang.StackOverFlowError。
堆空間不足:java.lang.OutOfMemoryEr
組成部分:堆、方法區(qū)、棧、本地方法棧、程序計(jì)數(shù)器
1、堆解決的是對(duì)象實(shí)例存儲(chǔ)的問(wèn)題,垃圾回收器管理的主要區(qū)域。
2、方法區(qū)可以認(rèn)為是堆的一部分,用于存儲(chǔ)已被虛擬機(jī)加載的信息,常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼。
3、棧解決的是程序運(yùn)行的問(wèn)題,棧里面存的是棧幀,棧幀里面存的是局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
4、本地方法棧與棧功能相同,本地方法棧執(zhí)行的是本地方法,一個(gè)Java調(diào)用非Java代碼的接口。
5、程序計(jì)數(shù)器(PC寄存器)程序計(jì)數(shù)器中存放的是當(dāng)前線(xiàn)程所執(zhí)行的字節(jié)碼的行數(shù)。JVM工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一個(gè)需要執(zhí)行的字節(jié)碼指令。
1.6 能不能解釋一下方法區(qū)?
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆
1.5.1 概述
- 方法區(qū)(Method Area)是各個(gè)線(xiàn)程共享的內(nèi)存區(qū)域
- 主要存儲(chǔ)類(lèi)的信息、運(yùn)行時(shí)常量池
- 虛擬機(jī)啟動(dòng)的時(shí)候創(chuàng)建,關(guān)閉虛擬機(jī)時(shí)釋放
- 如果方法區(qū)域中的內(nèi)存無(wú)法滿(mǎn)足分配請(qǐng)求,則會(huì)拋出OutOfMemoryError: Metaspace
1.5.2 常量池
可以看作是一張表,虛擬機(jī)指令根據(jù)這張常量表找到要執(zhí)行的類(lèi)名、方法名、參數(shù)類(lèi)型、字面量等信息
查看字節(jié)碼結(jié)構(gòu)(類(lèi)的基本信息、常量池、方法定義)javap -v xx.class
比如下面是一個(gè)Application類(lèi)的main方法執(zhí)行,源碼如下:
public class Application {
public static void main(String[] args) {
System.out.println("hello world");
}
}
找到類(lèi)對(duì)應(yīng)的class文件存放目錄,執(zhí)行命令:javap -v Application.class
查看字節(jié)碼結(jié)構(gòu)
D:\code\jvm-demo\target\classes\com\heima\jvm>javap -v Application.class
Classfile /D:/code/jvm-demo/target/classes/com/heima/jvm/Application.class
Last modified 2023-05-07; size 564 bytes //最后修改的時(shí)間
MD5 checksum c1b64ed6491b9a16c2baab5061c64f88 //簽名
Compiled from "Application.java" //從哪個(gè)源碼編譯
public class com.heima.jvm.Application //包名,類(lèi)名
minor version: 0
major version: 52 //jdk版本
flags: ACC_PUBLIC, ACC_SUPER //修飾符
Constant pool: //常量池
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/heima/jvm/Application
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/heima/jvm/Application;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Application.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/heima/jvm/Application
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.heima.jvm.Application(); //構(gòu)造方法
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/heima/jvm/Application;
public static void main(java.lang.String[]); //main方法
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "Application.java"
下圖,左側(cè)是main方法的指令信息,右側(cè)constant pool 是常量池
main方法按照指令執(zhí)行的時(shí)候,需要到常量池中查表翻譯找到具體的類(lèi)和方法地址去執(zhí)行
1.5.3 運(yùn)行時(shí)常量池
常量池是 *.class 文件中的,當(dāng)該類(lèi)被加載,它的常量池信息就會(huì)放入運(yùn)行時(shí)常量池,并把里面的符號(hào)地址變?yōu)檎鎸?shí)地址
1.7 你聽(tīng)過(guò)直接內(nèi)存嗎?
- 并不屬于JVM中的內(nèi)存結(jié)構(gòu),不由JVM進(jìn)行管理。是虛擬機(jī)的系統(tǒng)內(nèi)存
- 常見(jiàn)于 NIO 操作時(shí),用于數(shù)據(jù)緩沖區(qū),分配回收成本較高,但讀寫(xiě)性能高(減少了從系統(tǒng)內(nèi)存到Java堆內(nèi)存的拷貝),不受JVM內(nèi)存回收管理
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆
不受 JVM 內(nèi)存回收管理,是虛擬機(jī)的系統(tǒng)內(nèi)存,常見(jiàn)于 NIO 操作時(shí),用于數(shù)據(jù)緩沖區(qū),分配回收成本較高,但讀寫(xiě)性能高,不受 JVM 內(nèi)存回收管理
舉例:
需求,在本地電腦中的一個(gè)較大的文件(超過(guò) 100m)從一個(gè)磁盤(pán)挪到另外一個(gè)磁盤(pán)
代碼如下:
/**
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
static final String FROM = "E:\\編程資料\\第三方教學(xué)視頻\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用時(shí):1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用時(shí):479.295165 702.291454 562.56592
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用時(shí):" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用時(shí):" + (end - start) / 1000_000.0);
}
}
io用時(shí):182.7593
directBuffer用時(shí):
98.4438T
可以發(fā)現(xiàn),使用傳統(tǒng)的IO的時(shí)間要比NIO操作的時(shí)間長(zhǎng)了很多了,也就說(shuō)NIO的讀性能更好。
這個(gè)是跟我們的JVM的直接內(nèi)存是有一定關(guān)系,如下圖,是傳統(tǒng)阻塞IO的數(shù)據(jù)傳輸流程
下圖是NIO傳輸數(shù)據(jù)的流程,在這個(gè)里面主要使用到了一個(gè)直接內(nèi)存,不需要在堆中開(kāi)辟空間進(jìn)行數(shù)據(jù)的拷貝,jvm可以直接操作直接內(nèi)存,從而使數(shù)據(jù)讀寫(xiě)傳輸更快。
1.8 堆棧的區(qū)別是什么?
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆☆
1、棧內(nèi)存一般會(huì)用來(lái)存儲(chǔ)局部變量和方法調(diào)用,但堆內(nèi)存是用來(lái)存儲(chǔ)Java對(duì)象和數(shù)組的的。堆會(huì)GC垃圾回收,而棧不會(huì)。
2、棧內(nèi)存是線(xiàn)程私有的,而堆內(nèi)存是線(xiàn)程共有的。
3,、兩者異常錯(cuò)誤不同,但如果棧內(nèi)存或者堆內(nèi)存不足都會(huì)拋出異常。
棧空間不足:java.lang.StackOverFlowError。
堆空間不足:java.lang.OutOfMemoryError。
2 類(lèi)加載器
2.1 什么是類(lèi)加載器,類(lèi)加載器有哪些?
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆
要想理解類(lèi)加載器的話(huà),務(wù)必要先清楚對(duì)于一個(gè)Java文件,它從編譯到執(zhí)行的整個(gè)過(guò)程。
- 類(lèi)加載器:用于裝載字節(jié)碼文件(.class文件)
- 運(yùn)行時(shí)數(shù)據(jù)區(qū):用于分配存儲(chǔ)空間
- 執(zhí)行引擎:執(zhí)行字節(jié)碼文件或本地方法
- 垃圾回收器:用于對(duì)JVM中的垃圾內(nèi)容進(jìn)行回收
類(lèi)加載器
JVM只會(huì)運(yùn)行二進(jìn)制文件,而類(lèi)加載器(ClassLoader)的主要作用就是將字節(jié)碼文件加載到JVM中,從而讓Java程序能夠啟動(dòng)起來(lái)。現(xiàn)有的類(lèi)加載器基本上都是java.lang.ClassLoader的子類(lèi),該類(lèi)的主要職責(zé)就是用于將指定的類(lèi)找到或生成對(duì)應(yīng)的字節(jié)碼文件,同時(shí)類(lèi)加載器還會(huì)負(fù)責(zé)加載程序所需要的資源
類(lèi)加載器種類(lèi)
類(lèi)加載器根據(jù)各自加載范圍的不同,劃分為四種類(lèi)加載器:
-
啟動(dòng)類(lèi)加載器(BootStrap ClassLoader):
該類(lèi)并不繼承ClassLoader類(lèi),其是由C++編寫(xiě)實(shí)現(xiàn)。用于加載JAVA_HOME/jre/lib目錄下的類(lèi)庫(kù)。 -
擴(kuò)展類(lèi)加載器(ExtClassLoader):
該類(lèi)是ClassLoader的子類(lèi),主要加載JAVA_HOME/jre/lib/ext目錄中的類(lèi)庫(kù)。 -
應(yīng)用類(lèi)加載器(AppClassLoader):
該類(lèi)是ClassLoader的子類(lèi),主要用于加載classPath下的類(lèi),也就是加載開(kāi)發(fā)者自己編寫(xiě)的Java類(lèi)。 -
自定義類(lèi)加載器:
開(kāi)發(fā)者自定義類(lèi)繼承ClassLoader,實(shí)現(xiàn)自定義類(lèi)加載規(guī)則。
上述三種類(lèi)加載器的層次結(jié)構(gòu)如下如下:
類(lèi)加載器的體系并不是“繼承”體系,而是委派體系,類(lèi)加載器首先會(huì)到自己的parent中查找類(lèi)或者資源,如果找不到才會(huì)到自己本地查找。類(lèi)加載器的委托行為動(dòng)機(jī)是為了避免相同的類(lèi)被加載多次。
2.2 什么是雙親委派模型?
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆☆
如果一個(gè)類(lèi)加載器在接到加載類(lèi)的請(qǐng)求時(shí),它首先不會(huì)自己嘗試去加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求任務(wù)委托給父類(lèi)加載器去完成,**依次遞歸,**如果父類(lèi)加載器可以完成類(lèi)加載任務(wù),就返回成功;只有父類(lèi)加載器無(wú)法完成此加載任務(wù)時(shí),才由下一級(jí)去加載。
2.3 JVM為什么采用雙親委派機(jī)制
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆
(1)通過(guò)雙親委派機(jī)制可以避免某一個(gè)類(lèi)被重復(fù)加載,當(dāng)父類(lèi)已經(jīng)加載后則無(wú)需重復(fù)加載,保證唯一性。
(2)為了安全,保證類(lèi)庫(kù)API不會(huì)被修改
在工程中新建java.lang包,接著在該包下新建String類(lèi),并定義main函數(shù)
public class String {
public static void main(String[] args) {
System.out.println("demo info");
}
}
此時(shí)執(zhí)行main函數(shù),會(huì)出現(xiàn)異常,在類(lèi) java.lang.String 中找不到 main 方法
出現(xiàn)該信息是因?yàn)橛呻p親委派的機(jī)制,java.lang.String的在啟動(dòng)類(lèi)加載器(Bootstrap classLoader)得到加載,因?yàn)樵诤诵膉re庫(kù)中有其相同名字的類(lèi)文件,但該類(lèi)中并沒(méi)有main方法。這樣就能**防止惡意篡改核心API庫(kù)**。
2.4 說(shuō)一下類(lèi)裝載的執(zhí)行過(guò)程?
難易程度:☆☆☆☆☆
出現(xiàn)頻率:☆☆☆
類(lèi)從加載到虛擬機(jī)中開(kāi)始,直到卸載為止,它的整個(gè)生命周期包括了:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載這7個(gè)階段。其中,驗(yàn)證、準(zhǔn)備和解析這三個(gè)部分統(tǒng)稱(chēng)為連接(linking)。
類(lèi)加載過(guò)程詳解
1.加載
- 通過(guò)類(lèi)的全名,獲取類(lèi)的**二進(jìn)制數(shù)據(jù)流。 **
- 解析類(lèi)的二進(jìn)制數(shù)據(jù)流為方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)(Java類(lèi)模型)
- 創(chuàng)建java.lang.Class類(lèi)的實(shí)例,表示該類(lèi)型。作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪(fǎng)問(wèn)入口
2.驗(yàn)證
驗(yàn)證類(lèi)是否符合JVM規(guī)范,安全性檢查
(1)文件格式驗(yàn)證:是否符合Class文件的規(guī)范
(2)元數(shù)據(jù)驗(yàn)證
這個(gè)類(lèi)是否有父類(lèi)(除了Object這個(gè)類(lèi)之外,其余的類(lèi)都應(yīng)該有父類(lèi))
這個(gè)類(lèi)是否繼承(extends)了被final修飾過(guò)的類(lèi)(被final修飾過(guò)的類(lèi)表示類(lèi)不能被繼承)
類(lèi)中的字段、方法是否與父類(lèi)產(chǎn)生矛盾。(被final修飾過(guò)的方法或字段是不能覆蓋的)
(3)字節(jié)碼驗(yàn)證
主要的目的是通過(guò)對(duì)數(shù)據(jù)流和控制流的分析,確定程序語(yǔ)義是合法的、符合邏輯的。
(4)符號(hào)引用驗(yàn)證:符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo)(判斷是否存在),符號(hào)可以是任何形式的字面量
比如:int i = 3;
字面量:3
符號(hào)引用:i
3.準(zhǔn)備
為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值
- static變量,分配空間在準(zhǔn)備階段完成(設(shè)置默認(rèn)值),賦值在初始化階段完成
- static變量是final的基本類(lèi)型,以及字符串常量,值已確定,賦值在準(zhǔn)備階段完成
- static變量是final的引用類(lèi)型,那么賦值也會(huì)在初始化階段完成
4.解析
把類(lèi)中的符號(hào)引用轉(zhuǎn)換為直接引用
比如:方法中調(diào)用了其他方法,方法名可以理解為符號(hào)引用,而直接引用就是使用指針直接指向方法。
5.初始化
對(duì)類(lèi)的靜態(tài)變量,靜態(tài)代碼塊執(zhí)行初始化操作
前面的準(zhǔn)備階段做的是復(fù)制默認(rèn)的便利,比如0,null,
這個(gè)階段則是賦值變量所期待的值
- 如果初始化一個(gè)類(lèi)的時(shí)候,其父類(lèi)尚未初始化,則優(yōu)先初始化其父類(lèi)。
- 如果同時(shí)包含多個(gè)靜態(tài)變量和靜態(tài)代碼塊,則按照自上而下的順序依次執(zhí)行。
6.使用
JVM 開(kāi)始從入口方法開(kāi)始執(zhí)行用戶(hù)的程序代碼
- 調(diào)用靜態(tài)類(lèi)成員信息(比如:靜態(tài)字段、靜態(tài)方法)
- 使用new關(guān)鍵字為其創(chuàng)建對(duì)象實(shí)例
7.卸載
當(dāng)用戶(hù)程序代碼執(zhí)行完畢后,JVM 便開(kāi)始銷(xiāo)毀創(chuàng)建的 Class 對(duì)象,最后負(fù)責(zé)運(yùn)行的 JVM 也退出內(nèi)存
3 垃圾收回
3.1 簡(jiǎn)述Java垃圾回收機(jī)制?(GC是什么?為什么要GC)
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆
為了讓程序員更專(zhuān)注于代碼的實(shí)現(xiàn),而不用過(guò)多的考慮內(nèi)存釋放的問(wèn)題,所以,在Java語(yǔ)言中,有了自動(dòng)的垃圾回收機(jī)制,也就是我們熟悉的GC(Garbage Collection)。
有了垃圾回收機(jī)制后,程序員只需要關(guān)心內(nèi)存的申請(qǐng)即可,內(nèi)存的釋放由系統(tǒng)自動(dòng)識(shí)別完成。
在進(jìn)行垃圾回收時(shí),不同的對(duì)象引用類(lèi)型,GC會(huì)采用不同的回收時(shí)機(jī)
換句話(huà)說(shuō),自動(dòng)的垃圾回收的算法就會(huì)變得非常重要了,如果因?yàn)樗惴ǖ牟缓侠?,?dǎo)致內(nèi)存資源一直沒(méi)有釋放,同樣也可能會(huì)導(dǎo)致內(nèi)存溢出的。
當(dāng)然,除了Java語(yǔ)言,C#、Python等語(yǔ)言也都有自動(dòng)的垃圾回收機(jī)制。
3.2 對(duì)象什么時(shí)候可以被垃圾器回收
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆☆
簡(jiǎn)單一句就是:如果一個(gè)或多個(gè)對(duì)象沒(méi)有任何的引用指向它了,那么這個(gè)對(duì)象現(xiàn)在就是垃圾,如果定位了垃圾,則有可能會(huì)被垃圾回收器回收。
如果要定位什么是垃圾,有兩種方式來(lái)確定,第一個(gè)是引用計(jì)數(shù)法,第二個(gè)是可達(dá)性分析算法
3.2.1 引用計(jì)數(shù)法
一個(gè)對(duì)象被引用了一次,在當(dāng)前的對(duì)象頭上遞增一次引用次數(shù),如果這個(gè)對(duì)象的引用次數(shù)為0,代表這個(gè)對(duì)象可回收
String demo = new String("123");
String demo = null;
當(dāng)對(duì)象間出現(xiàn)了循環(huán)引用的話(huà),則引用計(jì)數(shù)法就會(huì)失效
先執(zhí)行右側(cè)代碼的前4行代碼
目前上方的引用關(guān)系和計(jì)數(shù)都是沒(méi)問(wèn)題的,但是,如果代碼繼續(xù)往下執(zhí)行,如下圖
雖然a和b都為null,但是由于a和b存在循環(huán)引用,這樣a和b永遠(yuǎn)都不會(huì)被回收。
優(yōu)點(diǎn):
- 實(shí)時(shí)性較高,無(wú)需等到內(nèi)存不夠的時(shí)候,才開(kāi)始回收,運(yùn)行時(shí)根據(jù)對(duì)象的計(jì)數(shù)器是否為0,就可以直接回收。
- 在垃圾回收過(guò)程中,應(yīng)用無(wú)需掛起。如果申請(qǐng)內(nèi)存時(shí),內(nèi)存不足,則立刻報(bào)OOM錯(cuò)誤。
- 區(qū)域性,更新對(duì)象的計(jì)數(shù)器時(shí),只是影響到該對(duì)象,不會(huì)掃描全部對(duì)象。
缺點(diǎn):
- 每次對(duì)象被引用時(shí),都需要去更新計(jì)數(shù)器,有一點(diǎn)時(shí)間開(kāi)銷(xiāo)。
- 浪費(fèi)CPU資源,即使內(nèi)存夠用,仍然在運(yùn)行時(shí)進(jìn)行計(jì)數(shù)器的統(tǒng)計(jì)。
- 無(wú)法解決循環(huán)引用問(wèn)題,會(huì)引發(fā)內(nèi)存泄露。(最大的缺點(diǎn))
3.2.2 可達(dá)性分析算法
現(xiàn)在的虛擬機(jī)采用的都是通過(guò)可達(dá)性分析算法來(lái)確定哪些內(nèi)容是垃圾。
會(huì)存在一個(gè)根節(jié)點(diǎn)【GC Roots】,引出它下面指向的下一個(gè)節(jié)點(diǎn),再以下一個(gè)節(jié)點(diǎn)節(jié)點(diǎn)開(kāi)始找出它下面的節(jié)點(diǎn),依次往下類(lèi)推。直到所有的節(jié)點(diǎn)全部遍歷完畢。
根對(duì)象是那些肯定不能當(dāng)做垃圾回收的對(duì)象,就可以當(dāng)做根對(duì)象
局部變量,靜態(tài)方法,靜態(tài)變量,類(lèi)信息
核心是:判斷某對(duì)象是否與根對(duì)象有直接或間接的引用,如果沒(méi)有被引用,則可以當(dāng)做垃圾回收
X,Y這兩個(gè)節(jié)點(diǎn)是可回收的,但是**并不會(huì)馬上的被回收??!** 對(duì)象中存在一個(gè)方法【finalize】。當(dāng)對(duì)象被標(biāo)記為可回收后,當(dāng)發(fā)生GC時(shí),首先**會(huì)判斷這個(gè)對(duì)象是否執(zhí)行了finalize方法**,如果這個(gè)方法還沒(méi)有被執(zhí)行的話(huà),那么就會(huì)先來(lái)執(zhí)行這個(gè)方法,接著在這個(gè)方法執(zhí)行中,可以設(shè)置當(dāng)前這個(gè)對(duì)象與GC ROOTS產(chǎn)生關(guān)聯(lián),那么這個(gè)方法執(zhí)行完成之后,GC會(huì)再次判斷對(duì)象是否可達(dá),如果仍然不可達(dá),則會(huì)進(jìn)行回收,如果可達(dá)了,則不會(huì)進(jìn)行回收。
finalize方法對(duì)于每一個(gè)對(duì)象來(lái)說(shuō),只會(huì)執(zhí)行一次。如果第一次執(zhí)行這個(gè)方法的時(shí)候,設(shè)置了當(dāng)前對(duì)象與RC ROOTS關(guān)聯(lián),那么這一次不會(huì)進(jìn)行回收。 那么等到這個(gè)對(duì)象第二次被標(biāo)記為可回收時(shí),那么該對(duì)象的finalize方法就不會(huì)再次執(zhí)行了。
GC ROOTS:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
“局部變量的引用存儲(chǔ)在虛擬機(jī)棧中,而對(duì)象存儲(chǔ)在堆中?!?/p>
/**
* demo是棧幀中的本地變量,當(dāng) demo = null 時(shí),由于此時(shí) demo 充當(dāng)了 GC Root 的作用,demo與原來(lái)指向的實(shí)例 new Demo() 斷開(kāi)了連接,對(duì)象被回收。
*/
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo = null;
}
}
- 方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象
“靜態(tài)變量的引用通常存儲(chǔ)在元空間,而對(duì)象仍然存儲(chǔ)在堆中?!?/p>
/**
* 當(dāng)棧幀中的本地變量 b = null 時(shí),
* 由于 b 原來(lái)指向的對(duì)象與 GC Root (變量 b) 斷開(kāi)了連接,
* 所以 b 原來(lái)指向的對(duì)象會(huì)被回收,而由于我們給 a 賦值了變量的引用,
* a在此時(shí)是類(lèi)靜態(tài)屬性引用,充當(dāng)了 GC Root 的作用,
* 它指向的對(duì)象依然存活!
*/
public class Demo {
public static Demo a;
public static void main(String[] args) {
Demo b = new Demo();
b.a = new Demo();
b = null;
}
}
- 方法區(qū)中常量引用的對(duì)象
常量和這里的demo沒(méi)有什么關(guān)系,所以不會(huì)因?yàn)閐emo指向的對(duì)象被回收而回收
/**
* 常量 a 指向的對(duì)象并不會(huì)因?yàn)?demo 指向的對(duì)象被回收而回收
*/
public class Demo {
public static final Demo a = new Demo();
public static void main(String[] args) {
Demo demo = new Demo();
demo = null;
}
}
- 本地方法棧中 JNI(即一般說(shuō)的 Native 方法)引用的對(duì)象
“ // 只要 nativeMethod 還持有它的引用,它就不會(huì)被垃圾回收?!?/p>
3.3 JVM 垃圾回收算法有哪些?
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆☆
3.3.1 標(biāo)記清除算法
標(biāo)記清除算法,是將垃圾回收分為2個(gè)階段,分別是標(biāo)記和清除。
1.根據(jù)可達(dá)性分析算法得出的垃圾進(jìn)行標(biāo)記
2.對(duì)這些標(biāo)記為可回收的內(nèi)容進(jìn)行垃圾回收
可以看到,標(biāo)記清除算法解決了引用計(jì)數(shù)算法中的循環(huán)引用的問(wèn)題,沒(méi)有從root節(jié)點(diǎn)引用的對(duì)象都會(huì)被回收。
同樣,標(biāo)記清除算法也是有缺點(diǎn)的:
- 效率較低,標(biāo)記和清除兩個(gè)動(dòng)作都需要遍歷所有的對(duì)象,并且在GC時(shí),需要停止應(yīng)用程序,對(duì)于交互性要求比較高的應(yīng)用而言這個(gè)體驗(yàn)是非常差的。
- (重要)通過(guò)標(biāo)記清除算法清理出來(lái)的內(nèi)存,碎片化較為嚴(yán)重,因?yàn)楸换厥盏膶?duì)象可能存在于內(nèi)存的各個(gè)角落,所以清理出來(lái)的內(nèi)存是不連貫的。
3.3.2 復(fù)制算法
復(fù)制算法的核心就是,**將原有的內(nèi)存空間一分為二,每次只用其中的一塊**,在垃圾回收時(shí),將正在使用的對(duì)象復(fù)制到另一個(gè)內(nèi)存空間中,然后將該內(nèi)存空間清空,交換兩個(gè)內(nèi)存的角色,完成垃圾的回收。
如果內(nèi)存中的垃圾對(duì)象較多,需要復(fù)制的對(duì)象就較少,這種情況下適合使用該方式并且效率比較高,反之,則不適合。
1)將內(nèi)存區(qū)域分成兩部分,每次操作其中一個(gè)。
2)當(dāng)進(jìn)行垃圾回收時(shí),將正在使用的內(nèi)存區(qū)域中的存活對(duì)象移動(dòng)到未使用的內(nèi)存區(qū)域。當(dāng)移動(dòng)完對(duì)這部分內(nèi)存區(qū)域一次性清除。
3)周而復(fù)始。
優(yōu)點(diǎn):
- 在垃圾對(duì)象多的情況下,效率較高
- 清理后,內(nèi)存無(wú)碎片
缺點(diǎn):
- 分配的2塊內(nèi)存空間,在同一個(gè)時(shí)刻,只能使用一半,內(nèi)存使用率較低
- 對(duì)于存活率高的對(duì)象進(jìn)行復(fù)制就會(huì)非常消耗時(shí)間
3.3.3 標(biāo)記整理算法
標(biāo)記壓縮算法是在標(biāo)記清除算法的基礎(chǔ)之上,做了優(yōu)化改進(jìn)的算法。和標(biāo)記清除算法一樣,也是從根節(jié)點(diǎn)開(kāi)始,對(duì)對(duì)象的引用進(jìn)行標(biāo)記,在清理階段,并不是簡(jiǎn)單的直接清理可回收對(duì)象,而是將存活對(duì)象都向內(nèi)存另一端移動(dòng),然后清理邊界以外的垃圾,從而解決了碎片化的問(wèn)題。
1)標(biāo)記垃圾。
2)需要清除向右邊走,不需要清除的向左邊走。
3)清除邊界以外的垃圾。
優(yōu)缺點(diǎn)同標(biāo)記清除算法,解決了標(biāo)記清除算法的碎片化的問(wèn)題,同時(shí),標(biāo)記壓縮算法多了一步,對(duì)象移動(dòng)內(nèi)存位置的步驟,其效率也有有一定的影響。
與復(fù)制算法對(duì)比:復(fù)制算法標(biāo)記完就復(fù)制,但標(biāo)記整理算法得等把所有存活對(duì)象都標(biāo)記完畢,再進(jìn)行整理
如上三種GC算法則是JVM虛擬機(jī)的基礎(chǔ)GC算法,綜合對(duì)比來(lái)看:
- 收集速度:復(fù)制算法 > 標(biāo)-清算法 > 標(biāo)-整算法
- 內(nèi)存整齊度:復(fù)制算法 = 標(biāo)-整算法 > 標(biāo)-清算法
- 內(nèi)存利用率:標(biāo)-整算法 > 標(biāo)-清算法 > 復(fù)制算法
在GC算法中,速度快的需要用空間來(lái)?yè)Q,空間利用率高且整齊度高的,則需要犧牲時(shí)間來(lái)?yè)Q取,所以相對(duì)而言,在絕大部分算法中,時(shí)間與空間不可兼得。
在上述三種算法中,復(fù)制算法和標(biāo)-整算法都是基于標(biāo)-清算法進(jìn)行優(yōu)化的,所以在現(xiàn)代的高性能JVM中絕大多數(shù)虛擬機(jī)都采用復(fù)制或標(biāo)-整算法進(jìn)行垃圾收集工作。
3.4 分代收集算法
說(shuō)一下JM中的分代回收一、堆的區(qū)域劃分
- 堆被分為了兩份:新生代和老年代【1:2】
- 對(duì)于新生代,內(nèi)部又被分為了三個(gè)區(qū)域。Eden區(qū),幸存者區(qū)survivor(分成from和to)【8:1:1】
二、對(duì)象回收分代回收策略
- 新創(chuàng)建的對(duì)象,都會(huì)先分配到eden區(qū)
- 當(dāng)伊甸園內(nèi)存不足,標(biāo)記伊甸園與from(現(xiàn)階段沒(méi)有)的存活對(duì)象
- 將存活對(duì)象采用復(fù)制算法復(fù)制到to中,復(fù)制完畢后,伊甸園和 from 內(nèi)存都得到釋放
- 經(jīng)過(guò)一段時(shí)間后伊甸園的內(nèi)存又出現(xiàn)不足,標(biāo)記eden區(qū)域to區(qū)存活的對(duì)象,將其復(fù)制到from區(qū)
- 當(dāng)幸存區(qū)對(duì)象熬過(guò)幾次回收(最多15次),晉升到老年代(幸存區(qū)內(nèi)存不足或大對(duì)象會(huì)提前晉升)
MinorGC、Mixed GC、FullGC的區(qū)別是什么
- MinorGC 【young GC】發(fā)生在新生代的垃圾回收,暫停時(shí)間短(STW)
- Mixed GC新生代+老年代部分區(qū)域的垃圾回收,G1 收集器特有
- FWGC:新生代+老年代完整垃圾回收,暫
3.4.1 概述
在java8時(shí),堆被分為了兩份:新生代和老年代【1:2】,在java7時(shí),還存在一個(gè)永久代。
對(duì)于新生代,內(nèi)部又被分為了三個(gè)區(qū)域。Eden區(qū),S0區(qū),S1區(qū)【8:1:1】
當(dāng)對(duì)新生代產(chǎn)生GC:MinorGC【young GC】
當(dāng)對(duì)老年代代產(chǎn)生GC:Major GC
當(dāng)對(duì)新生代和老年代產(chǎn)生FullGC: 新生代 + 老年代完整垃圾回收,暫停時(shí)間長(zhǎng),應(yīng)盡力避免
3.4.2工作機(jī)制
- 新創(chuàng)建的對(duì)象,都會(huì)先分配到eden區(qū)
- 當(dāng)伊甸園內(nèi)存不足,標(biāo)記伊甸園與 from(現(xiàn)階段沒(méi)有)的存活對(duì)象
- 將存活對(duì)象采用復(fù)制算法復(fù)制到 to 中,復(fù)制完畢后,伊甸園和 from 內(nèi)存都得到釋放
- 經(jīng)過(guò)一段時(shí)間后伊甸園的內(nèi)存又出現(xiàn)不足,標(biāo)記eden區(qū)域to區(qū)存活的對(duì)象,將存活的對(duì)象復(fù)制到from區(qū)
- 當(dāng)幸存區(qū)對(duì)象熬過(guò)幾次回收(最多15次),晉升到老年代(幸存區(qū)內(nèi)存不足或大對(duì)象會(huì)導(dǎo)致提前晉升)
MinorGC、 Mixed GC 、 FullGC的區(qū)別是什么
- MinorGC【young GC】發(fā)生在新生代的垃圾回收,暫停時(shí)間短(STW)
- Mixed GC 新生代 + 老年代部分區(qū)域的垃圾回收,G1 收集器特有
- FullGC: 新生代 + 老年代完整垃圾回收,暫停時(shí)間長(zhǎng)(STW),應(yīng)盡力避免?
名詞解釋?zhuān)?/p>
STW(Stop-The-World):暫停所有應(yīng)用程序線(xiàn)程,等待垃圾回收的完成
3.5 說(shuō)一下 JVM 有哪些垃圾回收器?
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆☆
在jvm中,實(shí)現(xiàn)了多種垃圾收集器,包括:
- 串行垃圾收集器:Serial GC、Serial Old GC
- 并行垃圾收集器:Parallel Old GC、ParNew GC
- CMS (并發(fā))垃圾收集器:CMS GC,作用在老年代
- G1垃圾回收器
3.5.1 串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用單線(xiàn)程進(jìn)行垃圾回收,堆內(nèi)存較小,適合個(gè)人電腦
- Serial 作用于新生代,采用復(fù)制算法
- Serial Old 作用于老年代,采用標(biāo)記-整理算法
垃圾回收時(shí),只有一個(gè)線(xiàn)程在工作,并且java應(yīng)用中的所有線(xiàn)程都要暫停(STW),等待垃圾回收的完成。
3.5.2 并行垃圾收集器
Parallel New和Parallel Old是一個(gè)并行垃圾回收器,JDK8默認(rèn)使用此垃圾回收器
- Parallel New作用于新生代,采用復(fù)制算法
- Parallel Old作用于老年代,采用標(biāo)記-整理算法
垃圾回收時(shí),多個(gè)線(xiàn)程在工作,并且java應(yīng)用中的所有線(xiàn)程都要暫停(STW),等待垃圾回收的完成。
3.5.2 CMS(并發(fā))垃圾收集器
CMS全稱(chēng) Concurrent Mark Sweep,是一款并發(fā)的、使用標(biāo)記-清除算法的垃圾回收器,該回收器是針對(duì)老年代垃圾回收的,是一款以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,停頓時(shí)間短,用戶(hù)體驗(yàn)就好。其最大特點(diǎn)是在進(jìn)行垃圾回收時(shí),應(yīng)用仍然能正常運(yùn)行。
3.6 詳細(xì)聊一下G1垃圾回收器
總結(jié)
詳細(xì)聊一下G1垃圾回收器
- 應(yīng)用于新生代和老年代,在JDK9之后默認(rèn)使用G1
- 劃分成多個(gè)區(qū)域,每個(gè)區(qū)域都可以充當(dāng) eden,survivor,old,humongous
,其中 humongous 專(zhuān)為大對(duì)象準(zhǔn)備 - 采用復(fù)制算法
- 響應(yīng)時(shí)間與吞吐量兼顧
- 分成三個(gè)階段:新生代回收(stw)、并發(fā)標(biāo)記(重新標(biāo)記stw)、混合收集
- 如果并發(fā)失?。椿厥账俣融s不上創(chuàng)建新對(duì)象
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆☆
3.6.1 概述
- 應(yīng)用于新生代和老年代,在JDK9之后默認(rèn)使用G1
- 劃分成多個(gè)區(qū)域,每個(gè)區(qū)域都可以充當(dāng) eden,survivor,old, humongous,其中 humongous 專(zhuān)為大對(duì)象準(zhǔn)備
- 采用復(fù)制算法
- 響應(yīng)時(shí)間與吞吐量兼顧
- 分成三個(gè)階段:新生代回收、并發(fā)標(biāo)記、混合收集
- 如果并發(fā)失敗(即回收速度趕不上創(chuàng)建新對(duì)象速度),會(huì)觸發(fā) Full GC
3.6.2 Young Collection(年輕代垃圾回收)
- 初始時(shí),所有區(qū)域都處于空閑狀態(tài)
- 創(chuàng)建了一些對(duì)象,挑出一些空閑區(qū)域作為伊甸園區(qū)存儲(chǔ)這些對(duì)象
- 當(dāng)伊甸園需要垃圾回收時(shí),挑出一個(gè)空閑區(qū)域作為幸存區(qū),用復(fù)制算法復(fù)制存活對(duì)象,需要暫停用戶(hù)線(xiàn)程
- 隨著時(shí)間流逝,伊甸園的內(nèi)存又有不足
- 將伊甸園以及之前幸存區(qū)中的存活對(duì)象,采用復(fù)制算法,復(fù)制到新的幸存區(qū),其中較老對(duì)象晉升至老年代
3.6.3 Young Collection + Concurrent Mark (年輕代垃圾回收+并發(fā)標(biāo)記)
當(dāng)老年代占用內(nèi)存超過(guò)閾值(默認(rèn)是45%)后,觸發(fā)并發(fā)標(biāo)記,這時(shí)無(wú)需暫停用戶(hù)線(xiàn)程
- 并發(fā)標(biāo)記之后,會(huì)有重新標(biāo)記階段解決漏標(biāo)問(wèn)題,此時(shí)需要暫停用戶(hù)線(xiàn)程。
- 這些都完成后就知道了老年代有哪些存活對(duì)象,隨后進(jìn)入混合收集階段。此時(shí)不會(huì)對(duì)所有老年代區(qū)域進(jìn)行回收,而是根據(jù)暫停時(shí)間目標(biāo)優(yōu)先回收價(jià)值高(存活對(duì)象少)的區(qū)域(這也是 Gabage First 名稱(chēng)的由來(lái))。
3.6.4 Mixed Collection (混合垃圾回收)
復(fù)制完成,內(nèi)存得到釋放。進(jìn)入下一輪的新生代回收、并發(fā)標(biāo)記、混合收集
其中H叫做巨型對(duì)象,如果對(duì)象非常大,會(huì)開(kāi)辟一塊連續(xù)的空間存儲(chǔ)巨型對(duì)象
3.7 強(qiáng)引用、軟引用、弱引用、虛引用的區(qū)別?
強(qiáng)引用、軟引用、弱引用、虛引用的區(qū)別?
- 強(qiáng)引用:只要所有 GC Roots 能找到,就不會(huì)被回收
- 軟引用:需要配合SoftReference使用,當(dāng)垃圾多次回收,內(nèi)存依然不夠的時(shí)候會(huì)回收軟引用對(duì)象
- 弱引用:需要配合WeakReference使用,只要進(jìn)行了垃圾回收,就會(huì)把弱引用對(duì)象回收
- 虛引用:必須配合引用隊(duì)列使用,被引用對(duì)象回收時(shí),會(huì)將虛引用入隊(duì),由 Reference Handler 線(xiàn)程調(diào)用虛引
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆
3.7.1 強(qiáng)引用
強(qiáng)引用:只有所有 GC Roots 對(duì)象都不通過(guò)【強(qiáng)引用】引用該對(duì)象,該對(duì)象才能被垃圾回收
User user = new User();
3.7.2 軟引用
軟引用:僅有軟引用引用該對(duì)象時(shí),在垃圾回收后,內(nèi)存仍不足時(shí)會(huì)再次出發(fā)垃圾回收
User user = new User();
SoftReference softReference = new SoftReference(user);
3.7.3 弱引用
弱引用:僅有弱引用引用該對(duì)象時(shí),在垃圾回收時(shí),無(wú)論內(nèi)存是否充足,都會(huì)回收弱引用對(duì)象
User user = new User();
WeakReference weakReference = new WeakReference(user);
延伸話(huà)題:ThreadLocal內(nèi)存泄漏問(wèn)題
ThreadLocal用的就是弱引用,看以下源碼:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v; //強(qiáng)引用,不會(huì)被回收
}
}
Entry
的key是當(dāng)前ThreadLocal,value值是我們要設(shè)置的數(shù)據(jù)。
WeakReference
表示的是弱引用,當(dāng)JVM進(jìn)行GC時(shí),一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象,不管當(dāng)前內(nèi)存空間是否足夠,都會(huì)回收它的內(nèi)存。但是value
是強(qiáng)引用,它不會(huì)被回收掉。
ThreadLocal使用建議:使用完畢后注意調(diào)用清理方法。
3.7.4 虛引用
虛引用:必須配合引用隊(duì)列使用,被引用對(duì)象回收時(shí),會(huì)將虛引用入隊(duì),由 Reference Handler 線(xiàn)程調(diào)用虛引用相關(guān)方法釋放直接內(nèi)存
4 JVM實(shí)踐(調(diào)優(yōu))
4.1 JVM 調(diào)優(yōu)的參數(shù)可以在哪里設(shè)置參數(shù)值?
- 難易程度:☆☆
出現(xiàn)頻率:☆☆☆
4.1.1 tomcat的設(shè)置vm參數(shù)
修改TOMCAT_HOME/bin/catalina.sh文件,如下圖
JAVA_OPTS="-Xms512m -Xmx1024m"
4.1.2 springboot項(xiàng)目jar文件啟動(dòng)
通常在linux系統(tǒng)下直接加參數(shù)啟動(dòng)springboot項(xiàng)目
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
nohup : 用于在系統(tǒng)后臺(tái)不掛斷地運(yùn)行命令,退出終端不會(huì)影響程序的運(yùn)行
參數(shù) & :讓命令在后臺(tái)執(zhí)行,終端退出后命令仍舊執(zhí)行。
4.2 用的 JVM 調(diào)優(yōu)的參數(shù)都有哪些?
- 設(shè)置堆空間大小
- 虛擬機(jī)棧的設(shè)置
- 年輕代中Eden區(qū)和兩個(gè)Survivor區(qū)的大小比例
- 年輕代晉升老年代閾值
難易程度:☆☆☆
出現(xiàn)頻率:☆☆☆☆
對(duì)于JVM調(diào)優(yōu),主要就是調(diào)整年輕代、年老大、元空間的內(nèi)存空間大小及使用的垃圾回收器類(lèi)型。
https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html
1)設(shè)置堆的初始大小和最大大小,為了防止垃圾收集器在初始大小、最大大小之間收縮堆而產(chǎn)生額外的時(shí)間,通常把最大、初始大小設(shè)置為相同的值。
-Xms:設(shè)置堆的初始化大小
-Xmx:設(shè)置堆的最大大小
2) 設(shè)置年輕代中Eden區(qū)和兩個(gè)Survivor區(qū)的大小比例。該值如果不設(shè)置,則默認(rèn)比例為8:1:1。Java官方通過(guò)增大Eden區(qū)的大小,來(lái)減少YGC發(fā)生的次數(shù),但有時(shí)我們發(fā)現(xiàn),雖然次數(shù)減少了,但Eden區(qū)滿(mǎn)的時(shí)候,由于占用的空間較大,導(dǎo)致釋放緩慢,此時(shí)STW的時(shí)間較長(zhǎng),因此需要按照程序情況去調(diào)優(yōu)。
-XXSurvivorRatio=3,表示年輕代中的分配比率:survivor:eden = 2:3
3)年輕代和老年代默認(rèn)比例為1:2??梢酝ㄟ^(guò)調(diào)整二者空間大小比率來(lái)設(shè)置兩者的大小。
-XX:newSize 設(shè)置年輕代的初始大小
-XX:MaxNewSize 設(shè)置年輕代的最大大小, 初始大小和最大大小兩個(gè)值通常相同
4)線(xiàn)程堆棧的設(shè)置:每個(gè)線(xiàn)程默認(rèn)會(huì)開(kāi)啟1M的堆棧,用于存放棧幀、調(diào)用參數(shù)、局部變量等,但一般256K就夠用。通常減少每個(gè)線(xiàn)程的堆棧,可以產(chǎn)生更多的線(xiàn)程,但這實(shí)際上還受限于操作系統(tǒng)。
-Xss 對(duì)每個(gè)線(xiàn)程stack大小的調(diào)整,-Xss128k
5)一般來(lái)說(shuō),當(dāng)survivor區(qū)不夠大或者占用量達(dá)到50%,就會(huì)把一些對(duì)象放到老年區(qū)。通過(guò)設(shè)置合理的eden區(qū),survivor區(qū)及使用率,可以將年輕對(duì)象保存在年輕代,從而避免full GC,使用-Xmn設(shè)置年輕代的大小
6)系統(tǒng)CPU持續(xù)飆高的話(huà),首先先排查代碼問(wèn)題,如果代碼沒(méi)問(wèn)題,則咨詢(xún)運(yùn)維或者云服務(wù)器供應(yīng)商,通常服務(wù)器重啟或者服務(wù)器遷移即可解決。
7)對(duì)于占用內(nèi)存比較多的大對(duì)象,一般會(huì)選擇在老年代分配內(nèi)存。如果在年輕代給大對(duì)象分配內(nèi)存,年輕代內(nèi)存不夠了,就要在eden區(qū)移動(dòng)大量對(duì)象到老年代,然后這些移動(dòng)的對(duì)象可能很快消亡,因此導(dǎo)致full GC。通過(guò)設(shè)置參數(shù):-XX:PetenureSizeThreshold=1000000,單位為B,標(biāo)明對(duì)象大小超過(guò)1M時(shí),在老年代(tenured)分配內(nèi)存空間。
8)一般情況下,年輕對(duì)象放在eden區(qū),當(dāng)?shù)谝淮蜧C后,如果對(duì)象還存活,放到survivor區(qū),此后,每GC一次,年齡增加1,當(dāng)對(duì)象的年齡達(dá)到閾值,就被放到tenured老年區(qū)。這個(gè)閾值可以同構(gòu)-XX:MaxTenuringThreshold設(shè)置。如果想讓對(duì)象留在年輕代,可以設(shè)置比較大的閾值。
(1)-XX:+UseParallelGC:年輕代使用并行垃圾回收收集器。這是一個(gè)關(guān)注吞吐量的收集器,可以盡可能的減少垃圾回收時(shí)間。
(2)-XX:+UseParallelOldGC:設(shè)置老年代使用并行垃圾回收收集器。
9)嘗試使用大的內(nèi)存分頁(yè):使用大的內(nèi)存分頁(yè)增加CPU的內(nèi)存尋址能力,從而系統(tǒng)的性能。
-XX:+LargePageSizeInBytes 設(shè)置內(nèi)存頁(yè)的大小
10)使用非占用的垃圾收集器。
-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停頓。
4.3 說(shuō)一下 JVM 調(diào)優(yōu)的工具?
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆☆
4.3.1 命令工具
4.3.1.1 jps(Java Process Status)
輸出JVM中運(yùn)行的進(jìn)程狀態(tài)信息(現(xiàn)在一般使用jconsole)
4.3.1.2 jstack
查看java進(jìn)程內(nèi)線(xiàn)程的堆棧信息。
jstack [option] <pid>
java案例
package com.heima.jvm;
public class Application {
public static void main(String[] args) throws InterruptedException {
while (true){
Thread.sleep(1000);
System.out.println("哈哈哈");
}
}
}
使用jstack查看進(jìn)行堆棧運(yùn)行信息
4.3.1.3 jmap
用于生成堆轉(zhuǎn)存快照
jmap [options] pid 內(nèi)存映像信息
jmap -heap pid 顯示Java堆的信息
jmap -dump:format=b,file=heap.hprof pid
format=b表示以hprof二進(jìn)制格式轉(zhuǎn)儲(chǔ)Java堆的內(nèi)存
file=用于指定快照dump文件的文件名。
例:顯示了某一個(gè)java運(yùn)行的堆信息
C:\Users\yuhon>jmap -heap 53280
Attaching to process ID 53280, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.321-b07
using thread-local object allocation.
Parallel GC with 8 thread(s) //并行的垃圾回收器
Heap Configuration: //堆配置
MinHeapFreeRatio = 0 //空閑堆空間的最小百分比
MaxHeapFreeRatio = 100 //空閑堆空間的最大百分比
MaxHeapSize = 8524922880 (8130.0MB) //堆空間允許的最大值
NewSize = 178257920 (170.0MB) //新生代堆空間的默認(rèn)值
MaxNewSize = 2841640960 (2710.0MB) //新生代堆空間允許的最大值
OldSize = 356515840 (340.0MB) //老年代堆空間的默認(rèn)值
NewRatio = 2 //新生代與老年代的堆空間比值,表示新生代:老年代=1:2
SurvivorRatio = 8 //兩個(gè)Survivor區(qū)和Eden區(qū)的堆空間比值為8,表示S0:S1:Eden=1:1:8
MetaspaceSize = 21807104 (20.796875MB) //元空間的默認(rèn)值
CompressedClassSpaceSize = 1073741824 (1024.0MB) //壓縮類(lèi)使用空間大小
MaxMetaspaceSize = 17592186044415 MB //元空間允許的最大值
G1HeapRegionSize = 0 (0.0MB)//在使用 G1 垃圾回收算法時(shí),JVM 會(huì)將 Heap 空間分隔為若干個(gè) Region,該參數(shù)用來(lái)指定每個(gè) Region 空間的大小。
Heap Usage:
PS Young Generation
Eden Space: //Eden使用情況
capacity = 134217728 (128.0MB)
used = 10737496 (10.240074157714844MB)
free = 123480232 (117.75992584228516MB)
8.000057935714722% used
From Space: //Survivor-From 使用情況
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
To Space: //Survivor-To 使用情況
capacity = 22020096 (21.0MB)
used = 0 (0.0MB)
free = 22020096 (21.0MB)
0.0% used
PS Old Generation //老年代 使用情況
capacity = 356515840 (340.0MB)
used = 0 (0.0MB)
free = 356515840 (340.0MB)
0.0% used
3185 interned Strings occupying 261264 bytes.
4.3.1.4 jhat
用于分析jmap生成的堆轉(zhuǎn)存快照(一般不推薦使用,而是使用Ecplise Memory Analyzer)
4.3.1.5 jstat
是JVM統(tǒng)計(jì)監(jiān)測(cè)工具??梢杂脕?lái)顯示垃圾回收信息、類(lèi)加載信息、新生代統(tǒng)計(jì)信息等。
常見(jiàn)參數(shù):
①總結(jié)垃圾回收統(tǒng)計(jì)
jstat -gcutil pid
字段 | 含義 |
---|---|
S0 | 幸存1區(qū)當(dāng)前使用比例 |
S1 | 幸存2區(qū)當(dāng)前使用比例 |
E | 伊甸園區(qū)使用比例 |
O | 老年代使用比例 |
M | 元數(shù)據(jù)區(qū)使用比例 |
CCS | 壓縮使用比例 |
YGC | 年輕代垃圾回收次數(shù) |
YGCT | 年輕代垃圾回收消耗時(shí)間 |
FGC | 老年代垃圾回收次數(shù) |
FGCT | 老年代垃圾回收消耗時(shí)間 |
GCT | 垃圾回收消耗總時(shí)間 |
②垃圾回收統(tǒng)計(jì)
jstat -gc pid
4.3.2 可視化工具
4.3.2.1 jconsole
用于對(duì)jvm的內(nèi)存,線(xiàn)程,類(lèi) 的監(jiān)控,是一個(gè)基于 jmx 的 GUI 性能監(jiān)控工具
打開(kāi)方式:java 安裝目錄 bin目錄下 直接啟動(dòng) jconsole.exe 就行
可以?xún)?nèi)存、線(xiàn)程、類(lèi)等信息
還可以在線(xiàn)程部分檢查死鎖問(wèn)題
4.3.2.2 VisualVM:故障處理工具
能夠監(jiān)控線(xiàn)程,內(nèi)存情況,查看方法的CPU時(shí)間和內(nèi)存中的對(duì) 象,已被GC的對(duì)象,反向查看分配的堆棧
打開(kāi)方式:java 安裝目錄 bin目錄下 直接啟動(dòng) jvisualvm.exe就行
監(jiān)控程序運(yùn)行情況
查看運(yùn)行中的dump
查看堆中的信息
4.4 java內(nèi)存泄露的排查思路?
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆☆
原因:
如果線(xiàn)程請(qǐng)求分配的棧容量超過(guò)java虛擬機(jī)棧允許的最大容量的時(shí)候,java虛擬機(jī)將拋出一個(gè)StackOverFlowError異常
如果java虛擬機(jī)??梢詣?dòng)態(tài)拓展,并且擴(kuò)展的動(dòng)作已經(jīng)嘗試過(guò),但是目前無(wú)法申請(qǐng)到足夠的內(nèi)存去完成拓展,或者在建立新線(xiàn)程的時(shí)候沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧,那java虛擬機(jī)將會(huì)拋出一個(gè)OutOfMemoryError異常
如果一次加載的類(lèi)太多,元空間內(nèi)存不足,則會(huì)報(bào)OutOfMemoryError: Metaspace
1、通過(guò)jmap指定打印他的內(nèi)存快照 dump
有的情況是內(nèi)存溢出之后程序則會(huì)直接中斷,而jmap只能打印在運(yùn)行中的程序,所以建議通過(guò)參數(shù)的方式的生成dump文件,配置如下:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/app/dumps/ 指定生成后文件的保存目錄
2、通過(guò)工具, VisualVM(Ecplise MAT)去分析 dump文件
VisualVM可以加載離線(xiàn)的dump文件,如下圖
文件–>裝入—>選擇dump文件即可查看堆快照信息
如果是linux系統(tǒng)中的程序,則需要把dump文件下載到本地(windows環(huán)境)下,打開(kāi)VisualVM工具分析。VisualVM目前只支持在windows環(huán)境下運(yùn)行可視化
3、通過(guò)查看堆信息的情況,可以大概定位內(nèi)存溢出是哪行代碼出了問(wèn)題
4、找到對(duì)應(yīng)的代碼,通過(guò)閱讀上下文的情況,進(jìn)行修復(fù)即可
4.5 CPU飆高排查方案與思路?
難易程度:☆☆☆☆
出現(xiàn)頻率:☆☆☆☆
1.使用top命令查看占用cpu的情況
2.通過(guò)top命令查看后,可以查看是哪一個(gè)進(jìn)程占用cpu較高,上圖所示的進(jìn)程為:30978
3.查看當(dāng)前線(xiàn)程中的進(jìn)程信息
ps H -eo pid,tid,%cpu | grep 40940
pid 進(jìn)行id
tid 進(jìn)程中的線(xiàn)程id
% cpu使用率
4.通過(guò)上圖分析,在進(jìn)程30978中的線(xiàn)程30979占用cpu較高
注意:上述的線(xiàn)程id是一個(gè)十進(jìn)制,我們需要把這個(gè)線(xiàn)程id轉(zhuǎn)換為16進(jìn)制才行,因?yàn)橥ǔT谌罩局姓故镜亩际?6進(jìn)制的線(xiàn)程id名稱(chēng)
轉(zhuǎn)換方式:
在linux中執(zhí)行命令
printf "%x\n" 30979
5.可以根據(jù)線(xiàn)程 id 找到有問(wèn)題的線(xiàn)程,進(jìn)一步定位到問(wèn)題代碼的源碼行號(hào)
執(zhí)行命令
jstack 30978 此處是進(jìn)程id
5.面試現(xiàn)場(chǎng)
5.1 JVM組成
面試官:JVM由那些部分組成,運(yùn)行流程是什么?
候選人:
嗯,好的~~
在JVM中共有四大部分,分別是ClassLoader(類(lèi)加載器)、Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū),內(nèi)存分區(qū))、Execution Engine(執(zhí)行引擎)、Native Method Library(本地庫(kù)接口)
它們的運(yùn)行流程是:
第一,類(lèi)加載器(ClassLoader)把Java代碼轉(zhuǎn)換為字節(jié)碼
第二,運(yùn)行時(shí)數(shù)據(jù)區(qū)(Runtime Data Area)把字節(jié)碼加載到內(nèi)存中,而字節(jié)碼文件只是JVM的一套指令集規(guī)范,并不能直接交給底層系統(tǒng)去執(zhí)行,而是有執(zhí)行引擎運(yùn)行
第三,執(zhí)行引擎(Execution Engine)將字節(jié)碼翻譯為底層系統(tǒng)指令,再交由CPU執(zhí)行去執(zhí)行,此時(shí)需要調(diào)用其他語(yǔ)言的本地庫(kù)接口(Native Method Library)來(lái)實(shí)現(xiàn)整個(gè)程序的功能。
面試官:好的,你能詳細(xì)說(shuō)一下 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)嗎?
候選人:
嗯,好~
運(yùn)行時(shí)數(shù)據(jù)區(qū)包含了堆、方法區(qū)、棧、本地方法棧、程序計(jì)數(shù)器這幾部分,每個(gè)功能作用不一樣。
- 堆解決的是對(duì)象實(shí)例存儲(chǔ)的問(wèn)題,垃圾回收器管理的主要區(qū)域。
- 方法區(qū)可以認(rèn)為是堆的一部分,用于存儲(chǔ)已被虛擬機(jī)加載的信息,常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼。
- 棧解決的是程序運(yùn)行的問(wèn)題,棧里面存的是棧幀,棧幀里面存的是局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
- 本地方法棧與棧功能相同,本地方法棧執(zhí)行的是本地方法,一個(gè)Java調(diào)用非Java代碼的接口。
- 程序計(jì)數(shù)器(PC寄存器)程序計(jì)數(shù)器中存放的是當(dāng)前線(xiàn)程所執(zhí)行的字節(jié)碼的行數(shù)。JVM工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一個(gè)需要執(zhí)行的字節(jié)碼指令。
面試官:好的,你再詳細(xì)介紹一下程序計(jì)數(shù)器的作用?
候選人:
嗯,是這樣~~
java虛擬機(jī)對(duì)于多線(xiàn)程是通過(guò)線(xiàn)程輪流切換并且分配線(xiàn)程執(zhí)行時(shí)間。在任何的一個(gè)時(shí)間點(diǎn)上,一個(gè)處理器只會(huì)處理執(zhí)行一個(gè)線(xiàn)程,如果當(dāng)前被執(zhí)行的這個(gè)線(xiàn)程它所分配的執(zhí)行時(shí)間用完了【掛起】。處理器會(huì)切換到另外的一個(gè)線(xiàn)程上來(lái)進(jìn)行執(zhí)行。并且這個(gè)線(xiàn)程的執(zhí)行時(shí)間用完了,接著處理器就會(huì)又來(lái)執(zhí)行被掛起的這個(gè)線(xiàn)程。這時(shí)候程序計(jì)數(shù)器就起到了關(guān)鍵作用,程序計(jì)數(shù)器在來(lái)回切換的線(xiàn)程中記錄他上一次執(zhí)行的行號(hào),然后接著繼續(xù)向下執(zhí)行。
面試官:你能給我詳細(xì)的介紹Java堆嗎?
候選人:
好的~
Java中的堆術(shù)語(yǔ)線(xiàn)程共享的區(qū)域。主要用來(lái)保存對(duì)象實(shí)例,數(shù)組等,當(dāng)堆中沒(méi)有內(nèi)存空間可分配給實(shí)例,也無(wú)法再擴(kuò)展時(shí),則拋出OutOfMemoryError異常。
在JAVA8中堆內(nèi)會(huì)存在年輕代、老年代
1)Young區(qū)被劃分為三部分,Eden區(qū)和兩個(gè)大小嚴(yán)格相同的Survivor區(qū),其中,Survivor區(qū)間中,某一時(shí)刻只有其中一個(gè)是被使用的,另外一個(gè)留做垃圾收集時(shí)復(fù)制對(duì)象用。在Eden區(qū)變滿(mǎn)的時(shí)候, GC就會(huì)將存活的對(duì)象移到空閑的Survivor區(qū)間中,根據(jù)JVM的策略,在經(jīng)過(guò)幾次垃圾收集后,任然存活于Survivor的對(duì)象將被移動(dòng)到Tenured區(qū)間。
2)Tenured區(qū)主要保存生命周期長(zhǎng)的對(duì)象,一般是一些老的對(duì)象,當(dāng)一些對(duì)象在Young復(fù)制轉(zhuǎn)移一定的次數(shù)以后,對(duì)象就會(huì)被轉(zhuǎn)移到Tenured區(qū)。
面試官:能不能解釋一下方法區(qū)?
候選人:
好的~
與虛擬機(jī)棧類(lèi)似。本地方法棧是為虛擬機(jī)執(zhí)行本地方法時(shí)提供服務(wù)的。不需要進(jìn)行GC。本地方法一般是由其他語(yǔ)言編寫(xiě)。
面試官:你聽(tīng)過(guò)直接內(nèi)存嗎?
候選人:
嗯~~
它又叫做堆外內(nèi)存,線(xiàn)程共享的區(qū)域,在 Java 8 之前有個(gè)永久代的概念,實(shí)際上指的是 HotSpot 虛擬機(jī)上的永久代,它用永久代實(shí)現(xiàn)了 JVM 規(guī)范定義的方法區(qū)功能,主要存儲(chǔ)類(lèi)的信息,常量,靜態(tài)變量,即時(shí)編譯器編譯后代碼等,這部分由于是在堆中實(shí)現(xiàn)的,受 GC 的管理,不過(guò)由于永久代有 -XX:MaxPermSize 的上限,所以如果大量動(dòng)態(tài)生成類(lèi)(將類(lèi)信息放入永久代),很容易造成 OOM,有人說(shuō)可以把永久代設(shè)置得足夠大,但很難確定一個(gè)合適的大小,受類(lèi)數(shù)量,常量數(shù)量的多少影響很大。
所以在 Java 8 中就把方法區(qū)的實(shí)現(xiàn)移到了本地內(nèi)存中的元空間中,這樣方法區(qū)就不受 JVM 的控制了,也就不會(huì)進(jìn)行 GC,也因此提升了性能。
面試官:什么是虛擬機(jī)棧
候選人:
虛擬機(jī)棧是描述的是方法執(zhí)行時(shí)的內(nèi)存模型,是線(xiàn)程私有的,生命周期與線(xiàn)程相同,每個(gè)方法被執(zhí)行的同時(shí)會(huì)創(chuàng)建棧楨。保存執(zhí)行方法時(shí)的局部變量、動(dòng)態(tài)連接信息、方法返回地址信息等等。方法開(kāi)始執(zhí)行的時(shí)候會(huì)進(jìn)棧,方法執(zhí)行完會(huì)出?!鞠喈?dāng)于清空了數(shù)據(jù)】,所以這塊區(qū)域不需要進(jìn)行 GC。
面試官:能說(shuō)一下堆棧的區(qū)別是什么嗎?
候選人:
嗯,好的,有這幾個(gè)區(qū)別
第一,棧內(nèi)存一般會(huì)用來(lái)存儲(chǔ)局部變量和方法調(diào)用,但堆內(nèi)存是用來(lái)存儲(chǔ)Java對(duì)象和數(shù)組的的。堆會(huì)GC垃圾回收,而棧不會(huì)。
第二、棧內(nèi)存是線(xiàn)程私有的,而堆內(nèi)存是線(xiàn)程共有的。
第三、兩者異常錯(cuò)誤不同,但如果棧內(nèi)存或者堆內(nèi)存不足都會(huì)拋出異常。
??臻g不足:java.lang.StackOverFlowError。
堆空間不足:java.lang.OutOfMemoryError。
5.2 類(lèi)加載器
面試官:什么是類(lèi)加載器,類(lèi)加載器有哪些?
候選人:
嗯,是這樣的
JVM只會(huì)運(yùn)行二進(jìn)制文件,而類(lèi)加載器(ClassLoader)的主要作用就是將字節(jié)碼文件加載到JVM中,從而讓Java程序能夠啟動(dòng)起來(lái)。
常見(jiàn)的類(lèi)加載器有4個(gè)
第一個(gè)是啟動(dòng)類(lèi)加載器(BootStrap ClassLoader):其是由C++編寫(xiě)實(shí)現(xiàn)。用于加載JAVA_HOME/jre/lib目錄下的類(lèi)庫(kù)。
第二個(gè)是擴(kuò)展類(lèi)加載器(ExtClassLoader):該類(lèi)是ClassLoader的子類(lèi),主要加載JAVA_HOME/jre/lib/ext目錄中的類(lèi)庫(kù)。
第三個(gè)是應(yīng)用類(lèi)加載器(AppClassLoader):該類(lèi)是ClassLoader的子類(lèi),主要用于加載classPath下的類(lèi),也就是加載開(kāi)發(fā)者自己編寫(xiě)的Java類(lèi)。
第四個(gè)是自定義類(lèi)加載器:開(kāi)發(fā)者自定義類(lèi)繼承ClassLoader,實(shí)現(xiàn)自定義類(lèi)加載規(guī)則。
面試官:說(shuō)一下類(lèi)裝載的執(zhí)行過(guò)程?
候選人:
嗯,這個(gè)過(guò)程還是挺多的。
類(lèi)從加載到虛擬機(jī)中開(kāi)始,直到卸載為止,它的整個(gè)生命周期包括了:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載這7個(gè)階段。其中,驗(yàn)證、準(zhǔn)備和解析這三個(gè)部分統(tǒng)稱(chēng)為連接(linking)
1.加載:查找和導(dǎo)入class文件
2.驗(yàn)證:保證加載類(lèi)的準(zhǔn)確性
3.準(zhǔn)備:為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值
4.解析:把類(lèi)中的符號(hào)引用轉(zhuǎn)換為直接引用
5.初始化:對(duì)類(lèi)的靜態(tài)變量,靜態(tài)代碼塊執(zhí)行初始化操作
6.使用:JVM 開(kāi)始從入口方法開(kāi)始執(zhí)行用戶(hù)的程序代碼
7.卸載:當(dāng)用戶(hù)程序代碼執(zhí)行完畢后,JVM 便開(kāi)始銷(xiāo)毀創(chuàng)建的 Class 對(duì)象,最后負(fù)責(zé)運(yùn)行的 JVM 也退出內(nèi)存
面試官:什么是雙親委派模型?
候選人:
嗯,它是是這樣的。
如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己嘗試加載這個(gè)類(lèi),而是把這請(qǐng)求委派給父類(lèi)加載器去完成,每一個(gè)層次的類(lèi)加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳說(shuō)到頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父類(lèi)加載器返回自己無(wú)法完成這個(gè)加載請(qǐng)求(它的搜索返回中沒(méi)有找到所需的類(lèi))時(shí),子類(lèi)加載器才會(huì)嘗試自己去加載
面試官:JVM為什么采用雙親委派機(jī)制
候選人:
主要有兩個(gè)原因。
第一、通過(guò)雙親委派機(jī)制可以避免某一個(gè)類(lèi)被重復(fù)加載,當(dāng)父類(lèi)已經(jīng)加載后則無(wú)需重復(fù)加載,保證唯一性。
第二、為了安全,保證類(lèi)庫(kù)API不會(huì)被修改
5.3 垃圾回收
面試官:簡(jiǎn)述Java垃圾回收機(jī)制?(GC是什么?為什么要GC)
候選人:
嗯,是這樣~~
為了讓程序員更專(zhuān)注于代碼的實(shí)現(xiàn),而不用過(guò)多的考慮內(nèi)存釋放的問(wèn)題,所以,在Java語(yǔ)言中,有了自動(dòng)的垃圾回收機(jī)制,也就是我們熟悉的GC(Garbage Collection)。
有了垃圾回收機(jī)制后,程序員只需要關(guān)心內(nèi)存的申請(qǐng)即可,內(nèi)存的釋放由系統(tǒng)自動(dòng)識(shí)別完成。
在進(jìn)行垃圾回收時(shí),不同的對(duì)象引用類(lèi)型,GC會(huì)采用不同的回收時(shí)機(jī)
面試官:強(qiáng)引用、軟引用、弱引用、虛引用的區(qū)別?
候選人:
嗯嗯~
強(qiáng)引用最為普通的引用方式,表示一個(gè)對(duì)象處于有用且必須的狀態(tài),如果一個(gè)對(duì)象具有強(qiáng)引用,則GC并不會(huì)回收它。即便堆中內(nèi)存不足了,寧可出現(xiàn)OOM,也不會(huì)對(duì)其進(jìn)行回收
軟引用表示一個(gè)對(duì)象處于有用且非必須狀態(tài),如果一個(gè)對(duì)象處于軟引用,在內(nèi)存空間足夠的情況下,GC機(jī)制并不會(huì)回收它,而在內(nèi)存空間不足時(shí),則會(huì)在OOM異常出現(xiàn)之間對(duì)其進(jìn)行回收。但值得注意的是,因?yàn)镚C線(xiàn)程優(yōu)先級(jí)較低,軟引用并不會(huì)立即被回收。
弱引用表示一個(gè)對(duì)象處于可能有用且非必須的狀態(tài)。在GC線(xiàn)程掃描內(nèi)存區(qū)域時(shí),一旦發(fā)現(xiàn)弱引用,就會(huì)回收到弱引用相關(guān)聯(lián)的對(duì)象。對(duì)于弱引用的回收,無(wú)關(guān)內(nèi)存區(qū)域是否足夠,一旦發(fā)現(xiàn)則會(huì)被回收。同樣的,因?yàn)镚C線(xiàn)程優(yōu)先級(jí)較低,所以弱引用也并不是會(huì)被立刻回收。
虛引用表示一個(gè)對(duì)象處于無(wú)用的狀態(tài)。在任何時(shí)候都有可能被垃圾回收。虛引用的使用必須和引用隊(duì)列Reference Queue聯(lián)合使用
面試官:對(duì)象什么時(shí)候可以被垃圾器回收
候選人:
思考一會(huì)~~
如果一個(gè)或多個(gè)對(duì)象沒(méi)有任何的引用指向它了,那么這個(gè)對(duì)象現(xiàn)在就是垃圾,如果定位了垃圾,則有可能會(huì)被垃圾回收器回收。
如果要定位什么是垃圾,有兩種方式來(lái)確定,第一個(gè)是引用計(jì)數(shù)法,第二個(gè)是可達(dá)性分析算法
通常都使用可達(dá)性分析算法來(lái)確定是不是垃圾
面試官: JVM 垃圾回收算法有哪些?
候選人:
我記得一共有四種,分別是標(biāo)記清除算法、復(fù)制算法、標(biāo)記整理算法、分代回收
面試官: 你能詳細(xì)聊一下分代回收嗎?
候選人:
關(guān)于分代回收是這樣的
在java8時(shí),堆被分為了兩份:新生代和老年代,它們默認(rèn)空間占用比例是1:2
對(duì)于新生代,內(nèi)部又被分為了三個(gè)區(qū)域。Eden區(qū),S0區(qū),S1區(qū)默認(rèn)空間占用比例是8:1:1
具體的工作機(jī)制是有些情況:
1)當(dāng)創(chuàng)建一個(gè)對(duì)象的時(shí)候,那么這個(gè)對(duì)象會(huì)被分配在新生代的Eden區(qū)。當(dāng)Eden區(qū)要滿(mǎn)了時(shí)候,觸發(fā)YoungGC。
2)當(dāng)進(jìn)行YoungGC后,此時(shí)在Eden區(qū)存活的對(duì)象被移動(dòng)到S0區(qū),并且當(dāng)前對(duì)象的年齡會(huì)加1,清空Eden區(qū)。
3)當(dāng)再一次觸發(fā)YoungGC的時(shí)候,會(huì)把Eden區(qū)中存活下來(lái)的對(duì)象和S0中的對(duì)象,移動(dòng)到S1區(qū)中,這些對(duì)象的年齡會(huì)加1,清空Eden區(qū)和S0區(qū)。
4)當(dāng)再一次觸發(fā)YoungGC的時(shí)候,會(huì)把Eden區(qū)中存活下來(lái)的對(duì)象和S1中的對(duì)象,移動(dòng)到S0區(qū)中,這些對(duì)象的年齡會(huì)加1,清空Eden區(qū)和S1區(qū)。
5)對(duì)象的年齡達(dá)到了某一個(gè)限定的值(默認(rèn)15歲 ),那么這個(gè)對(duì)象就會(huì)進(jìn)入到老年代中。
當(dāng)然也有特殊情況,如果進(jìn)入Eden區(qū)的是一個(gè)大對(duì)象,在觸發(fā)YoungGC的時(shí)候,會(huì)直接存放到老年代
當(dāng)老年代滿(mǎn)了之后,觸發(fā)FullGC。FullGC同時(shí)回收新生代和老年代,當(dāng)前只會(huì)存在一個(gè)FullGC的線(xiàn)程進(jìn)行執(zhí)行,其他的線(xiàn)程全部會(huì)被掛起。 我們?cè)诔绦蛑幸M量避免FullGC的出現(xiàn)。
面試官:講一下新生代、老年代、永久代的區(qū)別?
候選人:
嗯!是這樣的,簡(jiǎn)單說(shuō)就是
新生代主要用來(lái)存放新生的對(duì)象。
老年代主要存放應(yīng)用中生命周期長(zhǎng)的內(nèi)存對(duì)象。
永久代指的是永久保存區(qū)域。主要存放Class和Meta(元數(shù)據(jù))的信息。在Java8中,永久代已經(jīng)被移除,取而代之的是一個(gè)稱(chēng)之為“元數(shù)據(jù)區(qū)”(元空間)的區(qū)域。元空間和永久代類(lèi)似,不過(guò)元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存的限制。
面試官:說(shuō)一下 JVM 有哪些垃圾回收器?
候選人:
在jvm中,實(shí)現(xiàn)了多種垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器(JDK8默認(rèn))、CMS(并發(fā))垃圾收集器、G1垃圾收集器(JDK9默認(rèn))
面試官:Minor GC、Major GC、Full GC是什么
候選人:
嗯,其實(shí)它們指的是不同代之間的垃圾回收
Minor GC 發(fā)生在新生代的垃圾回收,暫停時(shí)間短
Major GC 老年代區(qū)域的垃圾回收,老年代空間不足時(shí),會(huì)先嘗試觸發(fā)Minor GC。Minor GC之后空間還不足,則會(huì)觸發(fā)Major GC,Major GC速度比較慢,暫停時(shí)間長(zhǎng)
Full GC 新生代 + 老年代完整垃圾回收,暫停時(shí)間長(zhǎng),應(yīng)盡力避免
5.4 JVM實(shí)踐(調(diào)優(yōu))
面試官:JVM 調(diào)優(yōu)的參數(shù)可以在哪里設(shè)置參數(shù)值?
候選人:
我們當(dāng)時(shí)的項(xiàng)目是springboot項(xiàng)目,可以在項(xiàng)目啟動(dòng)的時(shí)候,java -jar中加入?yún)?shù)就行了
面試官:用的 JVM 調(diào)優(yōu)的參數(shù)都有哪些?
候選人:
嗯,這些參數(shù)是比較多的
我記得當(dāng)時(shí)我們?cè)O(shè)置過(guò)堆的大小,像-Xms和-Xmx
還有就是可以設(shè)置年輕代中Eden區(qū)和兩個(gè)Survivor區(qū)的大小比例
還有就是可以設(shè)置使用哪種垃圾回收器等等。具體的指令還真記不太清楚。
面試官:嗯,好的,你們平時(shí)調(diào)試 JVM都用了哪些工具呢?
候選人:
嗯,我們一般都是使用jdk自帶的一些工具,比如
jps 輸出JVM中運(yùn)行的進(jìn)程狀態(tài)信息
jstack查看java進(jìn)程內(nèi)線(xiàn)程的堆棧信息。
jmap 用于生成堆轉(zhuǎn)存快照
jstat用于JVM統(tǒng)計(jì)監(jiān)測(cè)工具
還有一些可視化工具,像jconsole和VisualVM等
面試官:假如項(xiàng)目中產(chǎn)生了java內(nèi)存泄露,你說(shuō)一下你的排查思路?
候選人:
嗯,這個(gè)我在之前項(xiàng)目排查過(guò)
第一呢可以通過(guò)jmap指定打印他的內(nèi)存快照 dump文件,不過(guò)有的情況打印不了,我們會(huì)設(shè)置vm參數(shù)讓程序自動(dòng)生成dump文件
第二,可以通過(guò)工具去分析 dump文件,jdk自帶的VisualVM就可以分析
第三,通過(guò)查看堆信息的情況,可以大概定位內(nèi)存溢出是哪行代碼出了問(wèn)題
第四,找到對(duì)應(yīng)的代碼,通過(guò)閱讀上下文的情況,進(jìn)行修復(fù)即可
面試官:好的,那現(xiàn)在再來(lái)說(shuō)一種情況,就是說(shuō)服務(wù)器CPU持續(xù)飆高,你的排查方案與思路?
候選人:
嗯,我思考一下~~
可以這么做~~
第一可以使用使用top命令查看占用cpu的情況
第二通過(guò)top命令查看后,可以查看是哪一個(gè)進(jìn)程占用cpu較高,記錄這個(gè)進(jìn)程id
第三可以通過(guò)ps 查看當(dāng)前進(jìn)程中的線(xiàn)程信息,看看哪個(gè)線(xiàn)程的cpu占用較高
第四可以jstack命令打印進(jìn)行的id,找到這個(gè)線(xiàn)程,就可以進(jìn)一步定位問(wèn)題代碼的行號(hào)文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-826936.html
學(xué)習(xí)視頻:【新版Java面試專(zhuān)題視頻教程,java八股文面試全套真題+深度詳解(含大廠高頻面試真題)】 https://www.bilibili.com/video/BV1yT411H7YK/?p=134&share_source=copy_web&vd_source=fcae3ca58a4c2446a58b5aaacbaa4bbe文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-826936.html
到了這里,關(guān)于JVM常見(jiàn)問(wèn)題筆記分享的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!