公衆號(五分鐘學大數據)已推出大數據面試系列文章—五分鐘小面試,此係列文章將會深刻研究各大廠筆面試真題,並根據筆面試題擴展相關的知識點,助力你們都可以成功入職大廠!java
大數據筆面試系列文章分爲兩種類型:混合型(即一篇文章中會有多個框架的知識點—融會貫通);專項型(一篇文章針對某個框架進行深刻解析—專項演練)。程序員
此篇文章爲系列文章的第二篇(JVM專項)面試
答:算法
由於這塊內容太多了,許多小夥伴可能記不住這麼多,因此下面的答案分爲簡答和精答。編程
JVM 運行時內存共分爲程序計數器,Java虛擬機棧,本地方法棧,堆,方法區五個部分:數組
注:JVM調優主要就是優化 Heap 堆 和 Method Area 方法區緩存
簡答: 每一個線程都有一個程序計算器,就是一個指針,指向方法區中的方法字節碼(下一個將要執行的指令代碼),由執行引擎讀取下一條指令,是一個很是小的內存空間,幾乎能夠忽略不記。安全
精答:佔據一塊較小的內存空間,能夠看作當前線程所執行的字節碼的行號指示器。在虛擬機概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能都須要依賴這個計數器來完成。服務器
因爲jvm的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器都只會執行一條線程中的指令。所以將來線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。網絡
若是線程正在執行的是一個Java方法,這個計數器記錄的則是正在執行的虛擬機字節碼指令的地址;
若是正在執行的是Native方法,這個計數器則爲空(undefined)。
此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。
簡答:主管Java程序的運行,在線程建立時建立,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來講不存在垃圾回收問題,只要線程一結束該棧就Over,生命週期和線程一致,是線程私有的。基本類型的變量和對象的引用變量都是在函數的棧內存中分配。
精答:線程私有,生命週期和線程相同,虛擬機棧描述的是Java方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀用於存儲局部變量表,操做數棧,動態連接,方法出口等信息。每個方法從調用直至完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表存放了編譯期可知的各類基本類型數據(boolean、byte、char、short、int、float、long、double)、對象引用、returnAddress類型(指向了一條字節碼指令的地址)。
其中64位長度的long和double類型的數據會佔用2個局部變量表空間(slot),其他的數據類型只佔用1個。局部變量表所需的內存空間在編譯期完成分配,當進入一個方法時,這個方法所須要在棧幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。
在Java虛擬機規範中,對此區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將會拋出Stack OverflowError異常;若是虛擬機棧能夠動態擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。
簡答:本地方法棧爲虛擬機中使用到的native方法服務,native方法做用是融合不一樣的編程語言爲Java所用,它的初衷是融合C/C++程序,Java誕生的時候C/C++橫行的時候,要想立足,必須有調用C/C++程序,因而就在內存中專門開闢了一塊區域處理標記爲native的代碼。
精答:本地方法棧與虛擬機棧所發揮的做用很是類似,他們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(字節碼)服務,而本地方法棧則爲虛擬機中使用到的native方法服務。在虛擬機規範中對本地方法棧中方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機直接把本地方法棧和虛擬機棧合二爲一,與虛擬機棧同樣也會拋出Stack OverflowError異常和OutOfMemoryError異常。
簡答:堆這塊區域是JVM中最大的,應用的對象和數據都是存在這個區域,這塊區域也是線程共享的,也是 gc 主要的回收區,一個 JVM 實例只存在一個堆類存。堆內存的大小是能夠調節的。
精答:對於大多數應用來講,堆空間是jvm內存中最大的一塊。Java堆是被全部線程共享,虛擬機啓動時建立,此內存區域惟一的目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。這一點在Java虛擬機規範中的描述是:全部的對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配,標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也就變得不那麼絕對了。
Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱爲「GC堆」。從內存回收角度看,因爲如今收集器基本都採用分代收集算法,因此Java堆還能夠細分爲:新生代和老年代;再細緻一點的有Eden空間,From Survivor空間,To Survivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區。不過不管如何劃分,都與存放內容無關,不管哪一個區域,存儲的都仍然是對象實例,進一步劃分的目的是爲了更好的回收內存,或者更快的分配內存。(若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。)
簡答:和堆同樣全部線程共享,主要用於存儲已被jvm加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
精答:方法區是被全部線程共享,全部字段和方法字節碼,以及一些特殊方法如構造函數,接口代碼也在此定義。簡單說,全部定義的方法的信息都保存在該區域,此區域屬於共享區間。
靜態變量,常量,類信息(構造方法/接口定義),運行時常量池存在方法區中;可是實例變量存在堆內存中,和方法區無關。
在JDK1.7發佈的HotSpot中,已經把字符串常量池移除方法區了。
簡答:運行時常量池是方法區的一部分。用於存放編譯期生成的各類字面量和符號引用,它的重要特性是動態性,即Java語言並不要求常量必定只能在編譯期產生,運行期間也可能產生新的常量,這些常量被放在運行時常量池中。
精答:運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
Java虛擬機對class文件每一部分的格式都有嚴格規定,每個字節用於存儲哪一種數據都必須符合規範纔會被jvm承認。但對於運行時常量池,Java虛擬機規範沒作任何細節要求。
運行時常量池有個重要特性是動態性,Java語言不要求常量必定只在編譯期才能產生,也就是並不是預置入class文件中常量池的內容才能進入方法區的運行時常量池,運行期間也有可能將新的常量放入池中,這種特性使用最多的是String類的intern()方法。
既然運行時常量池是方法區的一部分,天然受到方法區內存的限制。當常量池沒法再申請到內存時會拋出outOfMemeryError異常。
jdk 1.8 同 jdk 1.7 比,最大的差異就是:元數據區取代了永久代。元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元數據空間並不在虛擬機中,而是使用本地內存。
答:
簡答:類加載過程便是指JVM虛擬機把.class文件中類信息加載進內存,並進行解析生成對應的class對象的過程。分爲五個步驟:加載 -> 驗證 -> 準備 -> 解析 -> 初始化。加載:將外部的 .class 文件加載到Java虛擬機中;驗證:確保加載進來的 calss 文件包含的額信息符合 Java 虛擬機的要求;準備:爲類變量分配內存,設置類變量的初始值;解析:將常量池內的符號引用 轉爲 直接引用;初始化:初始化類變量和靜態代碼塊。
精答:前方預警,內容較長,作好準備!
一個Java文件從編碼完成到最終執行,通常主要包括兩個過程:編譯、運行
編譯:即把咱們寫好的java文件,經過javac命令編譯成字節碼,也就是咱們常說的.class文件。
運行:則是把編譯生成的.class文件交給Java虛擬機(JVM)執行。
而咱們所說的類加載過程便是指JVM虛擬機把.class文件中類信息加載進內存,並進行解析生成對應的class對象的過程。
舉個簡單的例子來講,JVM在執行某段代碼時,遇到了class A, 然而此時內存中並無class A的相關信息,因而JVM就會到相應的class文件中去尋找class A的類信息,並加載進內存中,這就是咱們所說的類加載過程。
因而可知,JVM不是一開始就把全部的類都加載進內存中,而是隻有第一次遇到某個須要運行的類時纔會加載,且只加載一次。
類加載的過程主要分爲三個部分:加載、連接、初始化。
而連接又能夠細分爲三個小部分:驗證、準備、解析。
簡單來講,加載指的是把class字節碼文件從各個來源經過類加載器裝載入內存中。
這裏有兩個重點:
字節碼來源:通常的加載來源包括從本地路徑下編譯生成的.class文件,從jar包中的.class文件,從遠程網絡,以及動態代理實時編譯
類加載器:通常包括啓動類加載器,擴展類加載器,應用類加載器,以及用戶的自定義類加載器。
注:爲何會有自定義類加載器?
一方面是因爲java代碼很容易被反編譯,若是須要對本身的代碼加密的話,能夠對編譯後的代碼進行加密,而後再經過實現本身的自定義類加載器進行解密,最後再加載。
另外一方面也有可能從非標準的來源加載代碼,好比從網絡來源,那就須要本身實現一個類加載器,從指定源進行加載。
主要是爲了保證加載進來的字節流符合虛擬機規範,不會形成安全錯誤。
包括對於文件格式的驗證,好比常量中是否有不被支持的常量?文件中是否有不規範的或者附加的其餘信息?
對於元數據的驗證,好比該類是否繼承了被final修飾的類?類中的字段,方法是否與父類衝突?是否出現了不合理的重載?
對於字節碼的驗證,保證程序語義的合理性,好比要保證類型轉換的合理性。
對於符號引用的驗證,好比校驗符號引用中經過全限定名是否可以找到對應的類?校驗符號引用中的訪問性(private,public等)是否可被當前類訪問?
主要是爲類變量(注意,不是實例變量)分配內存,而且賦予初值。
特別須要注意,初值,不是代碼中具體寫的初始化的值,而是Java虛擬機根據不一樣變量類型的默認初始值。
好比8種基本類型的初值,默認爲0;引用類型的初值則爲null;常量的初值即爲代碼中設置的值,final
static tmp = 456, 那麼該階段tmp的初值就是456。
將常量池內的符號引用替換爲直接引用的過程。
兩個重點:
符號引用:即一個字符串,可是這個字符串給出了一些可以惟一性識別一個方法,一個變量,一個類的相關信息。
直接引用:能夠理解爲一個內存地址,或者一個偏移量。好比類方法,類變量的直接引用是指向方法區的指針;而實例方法,實例變量的直接引用則是從實例的頭指針開始算起到這個實例變量位置的偏移量。
舉個例子來講,如今調用方法hello(),這個方法的地址是1234567,那麼hello就是符號引用,1234567就是直接引用。
在解析階段,虛擬機會把全部的類名,方法名,字段名這些符號引用替換爲具體的內存地址或偏移量,也就是直接引用。
這個階段主要是對類變量初始化,是執行類構造器的過程。
換句話說,只對static修飾的變量或語句進行初始化。
若是初始化一個類的時候,其父類還沒有初始化,則優先初始化其父類。
若是同時包含多個靜態變量和靜態代碼塊,則按照自上而下的順序依次執行。
類加載過程只是一個類生命週期的一部分,在其前,有編譯的過程,只有對源代碼編譯以後,才能得到可以被虛擬機加載的字節碼文件;在其後還有具體的類使用過程,當使用完成以後,還會在方法區垃圾回收的過程當中進行卸載。若是想要了解Java類整個生命週期的話,能夠自行上網查閱相關資料,這裏再也不多作贅述。
答:
理論上Java由於有垃圾回收機制(GC)不會存在內存泄露問題(這也是Java被普遍使用於服務器端編程的一個重要緣由);然而在實際開發中,可能會存在無用但可達的對象,這些對象不能被GC回收也會發生內存泄露。
一個例子就是Hibernate的Session(一級緩存)中的對象屬於持久態,垃圾回收器是不會回收這些對象的,然而這些對象中可能存在無用的垃圾對象。
下面的例子也展現了Java中發生內存泄露的狀況:
package com.yuan_more;
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack(){
elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem){
ensureCapacity();
}
public T pop(){
if(size == 0){
throw new EmptyStackException();
}
return elements[-- size];
}
private void ensureCapacity() {
if(elements.length == size){
elements = Arrays.copyOf(elements,2 * size +1);
}
}
}
上面的代碼實現了一個棧(先進後出(FILO))結構,乍看之下彷佛沒有什麼明顯的問題,它甚至能夠經過你編寫的各類單元測試。
然而其中的pop方法卻存在內存泄露的問題,當咱們用pop方法彈出棧中的對象時,該對象不會被看成垃圾回收,即便使用棧的程序再也不引用這些對象,由於棧內部維護着對這些對象的過時引用(obsolete reference)。
在支持垃圾回收的語言中,內存泄露是很隱蔽的,這種內存泄露其實就是無心識的對象保持。
若是一個對象引用被無心識的保留起來了,那麼垃圾回收器不會處理這個對象,也不會處理該對象引用的其餘對象,即便這樣的對象只有少數幾個,也可能會致使不少的對象被排除在垃圾回收以外,從而對性能形成重大影響,極端狀況下會引起Disk Paging(物理內存與硬盤的虛擬內存交換數據),甚至形成OutOfMemoryError。
答:
GC是垃圾收集的意思,內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會致使程序或系統的不穩定甚至崩潰。
Java提供的 GC 功能能夠自動監測對象是否超過做用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操做方法。Java程序員不用擔憂內存管理,由於垃圾收集器會自動進行管理。
要請求垃圾收集,能夠調用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,注意,只是請求,JVM什麼時候進行垃圾回收具備不可預知性。
垃圾回收能夠有效的防止內存泄露,有效的使用可使用的內存。垃圾回收器一般是做爲一個單獨的低優先級的線程運行,不可預知的狀況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收,程序員不能實時的調用垃圾回收器對某個對象或全部對象進行垃圾回收。
在Java誕生初期,垃圾回收是Java最大的亮點之一,由於服務器端的編程須要有效的防止內存泄露問題,然而時過境遷,現在Java的垃圾回收機制已經成爲被詬病的東西。移動智能終端用戶一般以爲iOS的系統比Android系統有更好的用戶體驗,其中一個深層次的緣由就在於Android系統中垃圾回收的不可預知性。
答:
由於有的對象壽命長,有的對象壽命短。應該將壽命長的對象放在一個區,壽命短的對象放在一個區。不一樣的區採用不一樣的垃圾收集算法。壽命短的區清理頻次高一點,壽命長的區清理頻次低一點,提升效率。
所謂的新生代和老年代是針對於分代收集算法來定義的,新生代又分爲Eden和Survivor兩個區。加上老年代就這三個區。
數據會首先分配到Eden區當中,固然也有特殊狀況,若是是大對象那麼會直接放入到老年代(大對象是指須要大量連續內存空間的java對象)。當Eden沒有足夠空間的時候就會觸發jvm發起一次Minor GC。新生代垃圾回收採用的是複製算法。
若是對象通過一次Minor GC還存活,而且又能被Survivor空間接受,那麼將被移動到Survivor空間當中。並將其年齡設爲1,對象在Survivor每熬過一次Minor GC,年齡就加1,當年齡達到必定的程度(默認爲15)時,就會被晉升到老年代中了,固然晉升老年代的年齡是能夠設置的。若是老年代滿了就執行:Full GC, 由於不常常執行,所以老年代垃圾回收採用了標記-整理(Mark-Compact)算法。