面試中必問(wèn)的jvm與性能優(yōu)化

1. 描述一下 JVM 加載 Class 文件的原理機(jī)制?
在面試java工程師的時(shí)候,這道題經(jīng)常被問(wèn)到,故需特別注意。
Java中的所有類,都需要由類加載器裝載到JVM中才能運(yùn)行。類加載器本身也是一個(gè)類,而它的工作就是把class文件從硬盤讀取到內(nèi)存中。在寫程序的時(shí)候,我們幾乎不需要關(guān)心類的加載,因?yàn)檫@些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。
Java類的加載是動(dòng)態(tài)的,它并不會(huì)一次性將所有類全部加載后再運(yùn)行,而是保證程序運(yùn)行的基礎(chǔ)類(像是基類)完全加載到j(luò)vm中,至于其他類,則在需要的時(shí)候才加載。這當(dāng)然就是為了節(jié)省內(nèi)存開(kāi)銷。
Java的類加載器有三個(gè),對(duì)應(yīng)Java的三種類:

image

三個(gè)加載器各自完成自己的工作,但它們是如何協(xié)調(diào)工作呢?哪一個(gè)類該由哪個(gè)類加載器完成呢?為了解決這個(gè)問(wèn)題,Java采用了委托模型機(jī)制。
委托模型機(jī)制的工作原理很簡(jiǎn)單:當(dāng)類加載器需要加載類的時(shí)候,先請(qǐng)示其Parent(即上一層加載器)在其搜索路徑載入,如果找不到,才在自己的搜索路徑搜索該類。這樣的順序其實(shí)就是加載器層次上自頂而下的搜索,因?yàn)榧虞d器必須保證基礎(chǔ)類的加載。之所以是這種機(jī)制,還有一個(gè)安全上的考慮:如果某人將一個(gè)惡意的基礎(chǔ)類加載到j(luò)vm,委托模型機(jī)制會(huì)搜索其父類加載器,顯然是不可能找到的,自然就不會(huì)將該類加載進(jìn)來(lái)。
我們可以通過(guò)這樣的代碼來(lái)獲取類加載器:

image

注意一個(gè)很重要的問(wèn)題,就是Java在邏輯上并不存在BootstrapKLoader的實(shí)體!因?yàn)樗怯肅++編寫的,所以打印其內(nèi)容將會(huì)得到null。
前面是對(duì)類加載器的簡(jiǎn)單介紹,它的原理機(jī)制非常簡(jiǎn)單,就是下面幾個(gè)步驟:
1.裝載:查找和導(dǎo)入class文件;
2.連接:

image

3.初始化:初始化靜態(tài)變量,靜態(tài)代碼塊。

image

2. 什么是類加載器?
類加載器是一個(gè)用來(lái)加載類文件的類。Java源代碼通過(guò)javac編譯器編譯成類文件。然后JVM來(lái)執(zhí)行類文件中的字節(jié)碼來(lái)執(zhí)行程序。類加載器負(fù)責(zé)加載文件系統(tǒng)、網(wǎng)絡(luò)或其他來(lái)源的類文件。
3. 類加載器有哪些?
有三種默認(rèn)使用的類加載器:Bootstrap類加載器、Extension類加載器和Application類加載器。每種類加載器都有設(shè)定好從哪里加載類。
Bootstrap類加載器負(fù)責(zé)加載rt.jar中的JDK類文件,它是所有類加載器的父加載器。Bootstrap類加載器沒(méi)有任何父類加載器,如果你調(diào)用String.class.getClassLoader(),會(huì)返回null,任何基于此的代碼會(huì)拋出NullPointerException異常。Bootstrap加載器被稱為初始類加載器。
而Extension將加載類的請(qǐng)求先委托給它的父加載器,也就是Bootstrap,如果沒(méi)有成功加載的話,再?gòu)膉re/lib/ext目錄下或者java.ext.dirs系統(tǒng)屬性定義的目錄下加載類。Extension加載器由sun.misc.LauncherExtClassLoader實(shí)現(xiàn)。 第三種默認(rèn)的加載器就是Application類加載器了。它負(fù)責(zé)從classpath環(huán)境變量中加載某些應(yīng)用相關(guān)的類,classpath環(huán)境變量通常由-classpath或-cp命令行選項(xiàng)來(lái)定義,或者是JAR中的Manifest的classpath屬性。Application類加載器是Extension類加載器的子加載器。通過(guò)sun.misc.LauncherAppClassLoader實(shí)現(xiàn)。
4. 什么是tomcat類加載機(jī)制?
在tomcat中類的加載稍有不同,如下圖:

image

當(dāng)tomcat啟動(dòng)時(shí),會(huì)創(chuàng)建幾種類加載器:
1 Bootstrap 引導(dǎo)類加載器
加載JVM啟動(dòng)所需的類,以及標(biāo)準(zhǔn)擴(kuò)展類(位于jre/lib/ext下)
2 System 系統(tǒng)類加載器
加載tomcat啟動(dòng)的類,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。

image

3 Common 通用類加載器
加載tomcat使用以及應(yīng)用通用的一些類,位于CATALINA_HOME/lib下,比如servlet-api.jar

image

4 webapp 應(yīng)用類加載器
每個(gè)應(yīng)用在部署后,都會(huì)創(chuàng)建一個(gè)唯一的類加載器。該類加載器會(huì)加載位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
當(dāng)應(yīng)用需要到某個(gè)類時(shí),則會(huì)按照下面的順序進(jìn)行類加載:
1 使用bootstrap引導(dǎo)類加載器加載
2 使用system系統(tǒng)類加載器加載
3 使用應(yīng)用類加載器在WEB-INF/classes中加載
4 使用應(yīng)用類加載器在WEB-INF/lib中加載
5 使用common類加載器在CATALINA_HOME/lib中加載

5、類加載器雙親委派模型機(jī)制?
什么是雙親委派模型(Parent-Delegation Model)?為什么使用雙親委派模型?
JVM中加載類機(jī)制采用的是雙親委派模型,顧名思義,在該模型中,子類加載器收到的加載請(qǐng)求,不會(huì)先去處理,而是先把請(qǐng)求委派給父類加載器處理,當(dāng)父類加載器處理不了時(shí)再返回給子類加載器加載;
為什么使用雙親委派模型?
因?yàn)榘踩?。使用雙親委派模型來(lái)組織類加載器間的關(guān)系,能夠使類的加載也具有層次關(guān)系,這樣能夠保證核心基礎(chǔ)的Java類會(huì)被根加載器加載,而不會(huì)去加載用戶自定義的和基礎(chǔ)類庫(kù)相同名字的類,從而保證系統(tǒng)的有序、安全。

6.Java 內(nèi)存分配?
一、 基本概念
每運(yùn)行一個(gè)java程序會(huì)產(chǎn)生一個(gè)java進(jìn)程,每個(gè)java進(jìn)程可能包含一個(gè)或者多個(gè)線程,每一個(gè)Java進(jìn)程對(duì)應(yīng)唯一一個(gè)JVM實(shí)例,每一個(gè)JVM實(shí)例唯一對(duì)應(yīng)一個(gè)堆,每一個(gè)線程有一個(gè)自己私有的棧。進(jìn)程所創(chuàng)建的所有類的實(shí)例(也就是對(duì)象)或數(shù)組(指的是數(shù)組的本身,不是引用)都放在堆中,并由該進(jìn)程所有的線程共享。Java中分配堆內(nèi)存是自動(dòng)初始化的,即為一個(gè)對(duì)象分配內(nèi)存的時(shí)候,會(huì)初始化這個(gè)對(duì)象中變量。雖然Java中所有對(duì)象的存儲(chǔ)空間都是在堆中分配的,但是這個(gè)對(duì)象的引用卻是在棧中分配,也就是說(shuō)在建立一個(gè)對(duì)象時(shí)在堆和棧中都分配內(nèi)存,在堆中分配的內(nèi)存實(shí)際存放這個(gè)被創(chuàng)建的對(duì)象的本身,而在棧中分配的內(nèi)存只是存放指向這個(gè)堆對(duì)象的引用而已。局部變量 new 出來(lái)時(shí),在棧空間和堆空間中分配空間,當(dāng)局部變量生命周期結(jié)束后,??臻g立刻被回收,堆空間區(qū)域等待GC回收。
具體的概念:JVM的內(nèi)存可分為3個(gè)區(qū):堆(heap)、棧(stack)和方法區(qū)(method,也叫靜態(tài)區(qū)):

堆區(qū):
存儲(chǔ)的全部是對(duì)象,每個(gè)對(duì)象都包含一個(gè)與之對(duì)應(yīng)的class的信息(class的目的是得到操作指令)
jvm只有一個(gè)堆區(qū)(heap),且被所有線程共享,堆中不存放基本類型和對(duì)象引用,只存放對(duì)象本身和數(shù)組本身;

棧區(qū):
每個(gè)線程包含一個(gè)棧區(qū),棧中只保存基礎(chǔ)數(shù)據(jù)類型本身和自定義對(duì)象的引用;
每個(gè)棧中的數(shù)據(jù)(原始類型和對(duì)象引用)都是私有的,其他棧不能訪問(wèn);
棧分為3個(gè)部分:基本類型變量區(qū)、執(zhí)行環(huán)境上下文、操作指令區(qū)(存放操作指令);

方法區(qū)(靜態(tài)區(qū)):
被所有的線程共享,方法區(qū)包含所有的class(class是指類的原始代碼,要?jiǎng)?chuàng)建一個(gè)類的對(duì)象,首先要把該類的代碼加載到方法區(qū)中,并且初始化)和static變量。
方法區(qū)中包含的都是在整個(gè)程序中永遠(yuǎn)唯一的元素,如class,static變量。

二、實(shí)例演示
AppMain.java

image

運(yùn)行該程序時(shí),首先啟動(dòng)一個(gè)Java虛擬機(jī)進(jìn)程,這個(gè)進(jìn)程首先從classpath中找到AppMain.class文件,讀取這個(gè)文件中的二進(jìn)制數(shù)據(jù),然后把Appmain類的類信息存放到運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)中,這就是AppMain類的加載過(guò)程。
接著,Java虛擬機(jī)定位到方法區(qū)中AppMain類的Main()方法的字節(jié)碼,開(kāi)始執(zhí)行它的指令。這個(gè)main()方法的第一條語(yǔ)句就是:

image

該語(yǔ)句的執(zhí)行過(guò)程:
1、Java虛擬機(jī)到方法區(qū)找到Sample類的類型信息,沒(méi)有找到,因?yàn)镾ample類還沒(méi)有加載到方法區(qū)(這里可以看出,java中的內(nèi)部類是單獨(dú)存在的,而且剛開(kāi)始的時(shí)候不會(huì)跟隨包含類一起被加載,等到要用的時(shí)候才被加載)。Java虛擬機(jī)立馬加載Sample類,把Sample類的類型信息存放在方法區(qū)里。
2、Java虛擬機(jī)首先在堆區(qū)中為一個(gè)新的Sample實(shí)例分配內(nèi)存, 并在Sample實(shí)例的內(nèi)存中存放一個(gè)方法區(qū)中存放Sample類的類型信息的內(nèi)存地址。
3、JVM的進(jìn)程中,每個(gè)線程都會(huì)擁有一個(gè)方法調(diào)用棧,用來(lái)跟蹤線程運(yùn)行中一系列的方法調(diào)用過(guò)程,棧中的每一個(gè)元素就被稱為棧幀,每當(dāng)線程調(diào)用一個(gè)方法的時(shí)候就會(huì)向方法棧壓入一個(gè)新幀。這里的幀用來(lái)存儲(chǔ)方法的參數(shù)、局部變量和運(yùn)算過(guò)程中的臨時(shí)數(shù)據(jù)。
4、位于“=”前的Test1是一個(gè)在main()方法中定義的一個(gè)變量(一個(gè)Sample對(duì)象的引用),因此,它被會(huì)添加到了執(zhí)行main()方法的主線程的JAVA方法調(diào)用棧中。而“=”將把這個(gè)test1變量指向堆區(qū)中的Sample實(shí)例。
5、JVM在堆區(qū)里繼續(xù)創(chuàng)建另一個(gè)Sample實(shí)例,并在main方法的方法調(diào)用棧中添加一個(gè)Test2變量,該變量指向堆區(qū)中剛才創(chuàng)建的Sample新實(shí)例。
6、JVM依次執(zhí)行它們的printName()方法。當(dāng)JAVA虛擬機(jī)執(zhí)行test1.printName()方法時(shí),JAVA虛擬機(jī)根據(jù)局部變量test1持有的引用,定位到堆區(qū)中的Sample實(shí)例,再根據(jù)Sample實(shí)例持有的引用,定位到方法去中Sample類的類型信息,從而獲得printName()方法的字節(jié)碼,接著執(zhí)行printName()方法包含的指令,開(kāi)始執(zhí)行。

三、辨析
在Java語(yǔ)言里堆(heap)和棧(stack)里的區(qū)別 :
棧(stack)與堆(heap)都是Java用來(lái)在Ram中存放數(shù)據(jù)的地方。與C++不同,Java自動(dòng)管理?xiàng):投眩绦騿T不能直接地設(shè)置?;蚨?。
棧的優(yōu)勢(shì)是,存取速度比堆要快,僅次于直接位于CPU中的寄存器。但缺點(diǎn)是,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。另外,棧數(shù)據(jù)可以共享(詳見(jiàn)下面的介紹)。堆的優(yōu)勢(shì)是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)。但缺點(diǎn)是,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,存取速度較慢。
Java中的2種數(shù)據(jù)類型:
一種是基本類型(primitive types), 共有8類,即int, short, long, byte, float, double, boolean, char(注意,并沒(méi)有string的基本類型)。這種類型的定義是通過(guò)諸如int a = 3; long b = 255L;的形式來(lái)定義的,稱為自動(dòng)變量。自動(dòng)變量存的是字面值,不是類的實(shí)例,即不是類的引用,這里并沒(méi)有類的存在。如int a = 3; 這里的a是一個(gè)指向int類型的引用,指向3這個(gè)字面值。這些字面值的數(shù)據(jù),由于大小可知,生存期可知(這些字面值固定定義在某個(gè)程序塊里面,程序塊退出后,字段值就消失了),出于追求速度的原因,就存在于棧中。
棧有一個(gè)很重要的特性:存在棧中的數(shù)據(jù)可以共享。假設(shè)我們同時(shí)定義: int a = 3;  int b = 3; 編譯器先處理int a = 3;首先它會(huì)在棧中創(chuàng)建一個(gè)變量為a的引用,然后查找有沒(méi)有字面值為3的地址,如果沒(méi)找到,就開(kāi)辟一個(gè)存放3這個(gè)字面值的地址,然后將a指向3的地址。接著處理int b = 3;在創(chuàng)建完b的引用變量后,由于在棧中已經(jīng)有3這個(gè)字面值,便將b直接指向3的地址。這樣,就出現(xiàn)了a與b同時(shí)均指向3的情況。
這種字面值的引用與類對(duì)象的引用不同。假定兩個(gè)類對(duì)象的引用同時(shí)指向一個(gè)對(duì)象,如果一個(gè)對(duì)象引用變量修改了這個(gè)對(duì)象的內(nèi)部狀態(tài),那么另一個(gè)對(duì)象引用變量也即刻反映出這個(gè)變化。相反,通過(guò)字面值的引用來(lái)修改其值,不會(huì)導(dǎo)致另一個(gè)指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完a與 b的值后,再令a=4;那么,b不會(huì)等于4,還是等于3。在編譯器內(nèi)部,遇到a=4;時(shí),它就會(huì)重新搜索棧中是否有4的字面值,如果沒(méi)有,重新開(kāi)辟地址存放4的值;如果已經(jīng)有了,則直接將a指向這個(gè)地址。因此a值的改變不會(huì)影響到b的值。
  另一種是包裝類數(shù)據(jù),如Integer, String, Double等將相應(yīng)的基本數(shù)據(jù)類型包裝起來(lái)的類。這些類數(shù)據(jù)全部存在于堆中,Java用new()語(yǔ)句來(lái)顯示地告訴編譯器,在運(yùn)行時(shí)才根據(jù)需要?jiǎng)討B(tài)創(chuàng)建,因此比較靈活,但缺點(diǎn)是要占用更多的時(shí)間。

7.Java 堆的結(jié)構(gòu)是什么樣子的?
JVM的堆是運(yùn)行時(shí)數(shù)據(jù)區(qū),所有類的實(shí)例和數(shù)組都是在堆上分配內(nèi)存。它在JVM啟動(dòng)的時(shí)候被創(chuàng)建。對(duì)象所占的堆內(nèi)存是由自動(dòng)內(nèi)存管理系統(tǒng)也就是垃圾收集器回收。
堆內(nèi)存是由存活和死亡的對(duì)象組成的。存活的對(duì)象是應(yīng)用可以訪問(wèn)的,不會(huì)被垃圾回收。死亡的對(duì)象是應(yīng)用不可訪問(wèn)尚且還沒(méi)有被垃圾收集器回收掉的對(duì)象。一直到垃圾收集器把這些對(duì)象回收掉之前,他們會(huì)一直占據(jù)堆內(nèi)存空間。
永久代是用于存放靜態(tài)文件,如Java類、方法等。持久代對(duì)垃圾回收沒(méi)有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如Hibernate 等,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來(lái)存放這些運(yùn)行過(guò)程中新增的類,永久代中一般包含:
類的方法(字節(jié)碼…)
類名(Sring對(duì)象)
.class文件讀到的常量信息
class對(duì)象相關(guān)的對(duì)象列表和類型列表 (e.g., 方法對(duì)象的array).
JVM創(chuàng)建的內(nèi)部對(duì)象
JIT編譯器優(yōu)化用的信息

虛擬機(jī)中的共劃分為三個(gè)代:
年輕代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對(duì)象關(guān)系
不大。年輕代和年老代的劃分是對(duì)垃 圾收集影響比較大的。
年輕代:
所有新生成的對(duì)象首先都是放在年輕代的。年輕代的目標(biāo)就是盡可能快速的收集掉那些生
命周期短的對(duì)象。年輕代分三個(gè)區(qū)。一個(gè)Eden區(qū),兩個(gè) Survivor區(qū)(一般而言)。大部分對(duì)象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時(shí),還存活的對(duì)象將被復(fù)制到Survivor區(qū)(兩個(gè)中的一個(gè)),當(dāng)這個(gè) Survivor區(qū)滿時(shí),此區(qū)的存活對(duì)象將被復(fù)制到另外一個(gè)Survivor區(qū),當(dāng)這個(gè)Survivor去也滿了的時(shí)候,從第一個(gè)Survivor區(qū)復(fù)制過(guò)來(lái)的并且此時(shí)還存活的對(duì)象,將被復(fù)制“年老區(qū)(Tenured)”。需要注意,Survivor的兩個(gè)區(qū)是對(duì)稱的,沒(méi)先后關(guān)系,所以同一個(gè)區(qū)中可能同時(shí)存在從Eden復(fù)制過(guò)來(lái)對(duì)象,和從前一個(gè)Survivor復(fù)制過(guò)來(lái)的對(duì)象,而復(fù)制到年老區(qū)的只有從第一個(gè)Survivor去過(guò)來(lái)的對(duì)象。而且,Survivor區(qū)總有一個(gè)是空的。同時(shí),根據(jù)程序需要,Survivor區(qū)是可以配置為多個(gè)的(多于兩個(gè)),這樣可以增加對(duì)象在年輕代中的存在時(shí)間,減少被放到年老代的可能。
年老代:
在年輕代中經(jīng)歷了N次垃圾回收后仍然存活的對(duì)象,就會(huì)被放到年老代中。因此,可以認(rèn)
為年老代中存放的都是一些生命周期較長(zhǎng)的對(duì)象。
持久代:
用于存放靜態(tài)文件,如今Java類、方法等。持久代對(duì)垃圾回收沒(méi)有顯著影響,但是有些應(yīng)
用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如Hibernate 等,在這種時(shí)候需要設(shè)置一個(gè)比較大的持
久代空間來(lái)存放這些運(yùn)行過(guò)程中新增的類。持久代大小通過(guò)-XX:MaxPermSize=進(jìn)行設(shè)置。
注意:
JDK1.8中,永久代已經(jīng)從java堆中移除,String直接存放在堆中,類的元數(shù)據(jù)存儲(chǔ)在meta space中,meta space占用外部?jī)?nèi)存,不占用堆內(nèi)存。
可以說(shuō),在java8的新版本中,持久代已經(jīng)更名為了元空間(meta space)。
8. 簡(jiǎn)述各個(gè)版本內(nèi)存區(qū)域的變化?

image

上面都是自己整理好的!我就把資料貢獻(xiàn)出來(lái)給有需要的人(私信我哦)!順便求一波關(guān)注.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容