面試中必問的jvm與性能優化

1. 描述一下 JVM 加載 Class 文件的原理機制?

在面試java工程師的時候,這道題經常被問到,故需特別注意。
Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬盤讀取到內存中。在寫程序的時候,我們幾乎不需要關心類的加載,因爲這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。
Java類的加載是動態的,它並不會一次性將所有類全部加載後再運行,而是保證程序運行的基礎類(像是基類)完全加載到jvm中,至於其他類,則在需要的時候才加載。這當然就是爲了節省內存開銷。
Java的類加載器有三個,對應Java的三種類:
在這裏插入圖片描述
三個加載器各自完成自己的工作,但它們是如何協調工作呢?哪一個類該由哪個類加載器完成呢?爲了解決這個問題,Java採用了委託模型機制。
委託模型機制的工作原理很簡單:當類加載器需要加載類的時候,先請示其Parent(即上一層加載器)在其搜索路徑載入,如果找不到,纔在自己的搜索路徑搜索該類。這樣的順序其實就是加載器層次上自頂而下的搜索,因爲加載器必須保證基礎類的加載。之所以是這種機制,還有一個安全上的考慮:如果某人將一個惡意的基礎類加載到jvm,委託模型機制會搜索其父類加載器,顯然是不可能找到的,自然就不會將該類加載進來。
我們可以通過這樣的代碼來獲取類加載器:
在這裏插入圖片描述
注意一個很重要的問題,就是Java在邏輯上並不存在BootstrapKLoader的實體!因爲它是用C++編寫的,所以打印其內容將會得到null。
前面是對類加載器的簡單介紹,它的原理機制非常簡單,就是下面幾個步驟:
1.裝載:查找和導入class文件;
2.連接:

在這裏插入圖片描述
3.初始化:初始化靜態變量,靜態代碼塊。
在這裏插入圖片描述

2. 什麼是類加載器?

類加載器是一個用來加載類文件的類。Java源代碼通過javac編譯器編譯成類文件。然後JVM來執行類文件中的字節碼來執行程序。類加載器負責加載文件系統、網絡或其他來源的類文件。

3. 類加載器有哪些?

有三種默認使用的類加載器:Bootstrap類加載器、Extension類加載器和Application類加載器。每種類加載器都有設定好從哪裏加載類。
Bootstrap類加載器負責加載rt.jar中的JDK類文件,它是所有類加載器的父加載器。Bootstrap類加載器沒有任何父類加載器,如果你調用String.class.getClassLoader(),會返回null,任何基於此的代碼會拋出NullPointerException異常。Bootstrap加載器被稱爲初始類加載器。
而Extension將加載類的請求先委託給它的父加載器,也就是Bootstrap,如果沒有成功加載的話,再從jre/lib/ext目錄下或者java.ext.dirs系統屬性定義的目錄下加載類。Extension加載器由sun.misc.LauncherExtClassLoaderApplicationclasspathclasspathclasspathcpJARManifestclasspathApplicationExtensionsun.misc.LauncherExtClassLoader實現。 第三種默認的加載器就是Application類加載器了。它負責從classpath環境變量中加載某些應用相關的類,classpath環境變量通常由-classpath或-cp命令行選項來定義,或者是JAR中的Manifest的classpath屬性。Application類加載器是Extension類加載器的子加載器。通過sun.misc.LauncherAppClassLoader實現。

4. 什麼是tomcat類加載機制?

在tomcat中類的加載稍有不同,如下圖:
在這裏插入圖片描述

當tomcat啓動時,會創建幾種類加載器:
1 Bootstrap 引導類加載器
加載JVM啓動所需的類,以及標準擴展類(位於jre/lib/ext下)
2 System 系統類加載器
加載tomcat啓動的類,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位於CATALINA_HOME/bin下。
在這裏插入圖片描述
3 Common 通用類加載器
加載tomcat使用以及應用通用的一些類,位於CATALINA_HOME/lib下,比如servlet-api.jar
在這裏插入圖片描述

4 webapp 應用類加載器

每個應用在部署後,都會創建一個唯一的類加載器。該類加載器會加載位於 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
當應用需要到某個類時,則會按照下面的順序進行類加載:
1 使用bootstrap引導類加載器加載
2 使用system系統類加載器加載
3 使用應用類加載器在WEB-INF/classes中加載
4 使用應用類加載器在WEB-INF/lib中加載
5 使用common類加載器在CATALINA_HOME/lib中加載

5、類加載器雙親委派模型機制?

什麼是雙親委派模型(Parent-Delegation Model)?爲什麼使用雙親委派模型?
JVM中加載類機制採用的是雙親委派模型,顧名思義,在該模型中,子類加載器收到的加載請求,不會先去處理,而是先把請求委派給父類加載器處理,當父類加載器處理不了時再返回給子類加載器加載;
爲什麼使用雙親委派模型?
因爲安全。使用雙親委派模型來組織類加載器間的關係,能夠使類的加載也具有層次關係,這樣能夠保證核心基礎的Java類會被根加載器加載,而不會去加載用戶自定義的和基礎類庫相同名字的類,從而保證系統的有序、安全。

6.Java 內存分配?

一、 基本概念
每運行一個java程序會產生一個java進程,每個java進程可能包含一個或者多個線程,每一個Java進程對應唯一一個JVM實例,每一個JVM實例唯一對應一個堆,每一個線程有一個自己私有的棧。進程所創建的所有類的實例(也就是對象)或數組(指的是數組的本身,不是引用)都放在堆中,並由該進程所有的線程共享。Java中分配堆內存是自動初始化的,即爲一個對象分配內存的時候,會初始化這個對象中變量。雖然Java中所有對象的存儲空間都是在堆中分配的,但是這個對象的引用卻是在棧中分配,也就是說在建立一個對象時在堆和棧中都分配內存,在堆中分配的內存實際存放這個被創建的對象的本身,而在棧中分配的內存只是存放指向這個堆對象的引用而已。局部變量 new 出來時,在棧空間和堆空間中分配空間,當局部變量生命週期結束後,棧空間立刻被回收,堆空間區域等待GC回收。
具體的概念:JVM的內存可分爲3個區:堆(heap)、棧(stack)和方法區(method,也叫靜態區):

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

棧區:
每個線程包含一個棧區,棧中只保存基礎數據類型本身和自定義對象的引用;
每個棧中的數據(原始類型和對象引用)都是私有的,其他棧不能訪問;
棧分爲3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令);

方法區(靜態區):
被所有的線程共享,方法區包含所有的class(class是指類的原始代碼,要創建一個類的對象,首先要把該類的代碼加載到方法區中,並且初始化)和static變量。
方法區中包含的都是在整個程序中永遠唯一的元素,如class,static變量。

二、實例演示
AppMain.java
在這裏插入圖片描述

運行該程序時,首先啓動一個Java虛擬機進程,這個進程首先從classpath中找到AppMain.class文件,讀取這個文件中的二進制數據,然後把Appmain類的類信息存放到運行時數據區的方法區中,這就是AppMain類的加載過程。
接着,Java虛擬機定位到方法區中AppMain類的Main()方法的字節碼,開始執行它的指令。這個main()方法的第一條語句就是:
在這裏插入圖片描述

該語句的執行過程:
1、Java虛擬機到方法區找到Sample類的類型信息,沒有找到,因爲Sample類還沒有加載到方法區(這裏可以看出,java中的內部類是單獨存在的,而且剛開始的時候不會跟隨包含類一起被加載,等到要用的時候才被加載)。Java虛擬機立馬加載Sample類,把Sample類的類型信息存放在方法區裏。
2、Java虛擬機首先在堆區中爲一個新的Sample實例分配內存, 並在Sample實例的內存中存放一個方法區中存放Sample類的類型信息的內存地址。
3、JVM的進程中,每個線程都會擁有一個方法調用棧,用來跟蹤線程運行中一系列的方法調用過程,棧中的每一個元素就被稱爲棧幀,每當線程調用一個方法的時候就會向方法棧壓入一個新幀。這裏的幀用來存儲方法的參數、局部變量和運算過程中的臨時數據。
4、位於“=”前的Test1是一個在main()方法中定義的一個變量(一個Sample對象的引用),因此,它被會添加到了執行main()方法的主線程的JAVA方法調用棧中。而“=”將把這個test1變量指向堆區中的Sample實例。
5、JVM在堆區裏繼續創建另一個Sample實例,並在main方法的方法調用棧中添加一個Test2變量,該變量指向堆區中剛纔創建的Sample新實例。
6、JVM依次執行它們的printName()方法。當JAVA虛擬機執行test1.printName()方法時,JAVA虛擬機根據局部變量test1持有的引用,定位到堆區中的Sample實例,再根據Sample實例持有的引用,定位到方法去中Sample類的類型信息,從而獲得printName()方法的字節碼,接着執行printName()方法包含的指令,開始執行。

三、辨析
在Java語言裏堆(heap)和棧(stack)裏的區別 :
棧(stack)與堆(heap)都是Java用來在Ram中存放數據的地方。與C++不同,Java自動管理棧和堆,程序員不能直接地設置棧或堆。
棧的優勢是,存取速度比堆要快,僅次於直接位於CPU中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。另外,棧數據可以共享(詳見下面的介紹)。堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由於要在運行時動態分配內存,存取速度較慢。

Java中的2種數據類型:
一種是基本類型(primitive types), 共有8類,即int, short, long, byte, float, double, boolean, char(注意,並沒有string的基本類型)。這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱爲自動變量。自動變量存的是字面值,不是類的實例,即不是類的引用,這裏並沒有類的存在。如int a = 3; 這裏的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由於大小可知,生存期可知(這些字面值固定定義在某個程序塊裏面,程序塊退出後,字段值就消失了),出於追求速度的原因,就存在於棧中。
棧有一個很重要的特性:存在棧中的數據可以共享。假設我們同時定義: int a = 3;  int b = 3; 編譯器先處理int a = 3;首先它會在棧中創建一個變量爲a的引用,然後查找有沒有字面值爲3的地址,如果沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接着處理int b = 3;在創建完b的引用變量後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。
這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那麼另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟着改變的情況。如上例,我們定義完a與 b的值後,再令a=4;那麼,b不會等於4,還是等於3。在編譯器內部,遇到a=4;時,它就會重新搜索棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。
  另一種是包裝類數據,如Integer, String, Double等將相應的基本數據類型包裝起來的類。這些類數據全部存在於堆中,Java用new()語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要佔用更多的時間。

7.Java 堆的結構是什麼樣子的?
JVM的堆是運行時數據區,所有類的實例和數組都是在堆上分配內存。它在JVM啓動的時候被創建。對象所佔的堆內存是由自動內存管理系統也就是垃圾收集器回收。
堆內存是由存活和死亡的對象組成的。存活的對象是應用可以訪問的,不會被垃圾回收。死亡的對象是應用不可訪問尚且還沒有被垃圾收集器回收掉的對象。一直到垃圾收集器把這些對象回收掉之前,他們會一直佔據堆內存空間。
永久代是用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類,永久代中一般包含:
類的方法(字節碼…)
類名(Sring對象)
.class文件讀到的常量信息
class對象相關的對象列表和類型列表 (e.g., 方法對象的array).
JVM創建的內部對象
JIT編譯器優化用的信息

虛擬機中的共劃分爲三個代:
年輕代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係
不大。年輕代和年老代的劃分是對垃 圾收集影響比較大的

年輕代:
所有新生成的對象首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生
命週期短的對象。年輕代分三個區。一個Eden區,兩個 Survivor區(一般而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個 Survivor區滿時,此區的存活對象將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的對象,將被複制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。同時,根據程序需要,Survivor區是可以配置爲多個的(多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。

年老代:
在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認
爲年老代中存放的都是一些生命週期較長的對象。
持久代:
用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應
用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持
久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設置。
注意:
JDK1.8中,永久代已經從java堆中移除,String直接存放在堆中,類的元數據存儲在meta space中,meta space佔用外部內存,不佔用堆內存。
可以說,在java8的新版本中,持久代已經更名爲了元空間(meta space)。
在這裏插入圖片描述

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章