● 請簡單描述一下JVM加載class文件的原理是什么?
考察點:JVM
參考回答:
JVM中類的裝載是由ClassLoader和它的子類來實現的,Java ClassLoader 是一個重要的Java運行時系統組件。它負責在運行時查找和裝入類文件的類。
Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬盤讀取到內存中。在寫程序的時候,我們幾乎不需要關心類的加載,因為這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。
類裝載方式,有兩種
(1)隱式裝載,程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中,
(2)顯式裝載,通過class.forname()等方法,顯式加載需要的類 ,隱式加載與顯式加載的區別:兩者本質是一樣的。
Java類的加載是動態的,它並不會一次性將所有類全部加載后再運行,而是保證程序運行的基礎類(像是基類)完全加載到jvm中,至於其他類,則在需要的時候才加載。這當然就是為了節省內存開銷。
● 什么是Java虛擬機?為什么Java被稱作是“平台無關的編程語言”?
考察點:JVM
參考回答:
Java虛擬機是一個可以執行Java字節碼的虛擬機進程。Java源文件被編譯成能被Java虛擬機執行的字節碼文件。
Java被設計成允許應用程序可以運行在任意的平台,而不需要程序員為每一個平台單獨重寫或者是重新編譯。Java虛擬機讓這個變為可能,因為它知道底層硬件平台的指令長度和其他特性。
● jvm最大內存限制多少?
考察點:JVM
參考回答:
(1)堆內存分配
JVM初始分配的內存由-Xms指定,默認是物理內存的1/64;JVM最大分配的內存由-Xmx指定,默認是物理內存的1/4。默認空余堆內存小 於40%時,JVM就會增大堆直到-Xmx的最大限制;空余堆內存大於70%時,JVM會減少堆直到-Xms的最小限制。因此服務器一般設置-Xms、 -Xmx相等以避免在每次GC后調整堆的大小。
(2)非堆內存分配
JVM使用-XX:PermSize設置非堆內存初始值,默認是物理內存的1/64;由XX:MaxPermSize設置最大非堆內存的大小,默認是物理內存的1/4。
(3)VM最大內存
首先JVM內存限制於實際的最大物理內存,假設物理內存無限大的話,JVM內存的最大值跟操作系統有很大的關系。簡單的說就32位處理器雖 然可控內存空間有4GB,但是具體的操作系統會給一個限制,這個限制一般是2GB-3GB(一般來說Windows系統下為1.5G-2G,Linux系 統下為2G-3G),而64bit以上的處理器就不會有限制了。

● jvm是如何實現線程的?
考察點:JVM
參考回答:
線程是比進程更輕量級的調度執行單位。線程可以把一個進程的資源分配和執行調度分開。一個進程里可以啟動多條線程,各個線程可共享該進程的資源(內存地址,文件IO等),又可以獨立調度。線程是CPU調度的基本單位。
主流OS都提供線程實現。Java語言提供對線程操作的同一API,每個已經執行start(),且還未結束的java.lang.Thread類的實例,代表了一個線程。
Thread類的關鍵方法,都聲明為Native。這意味着這個方法無法或沒有使用平台無關的手段來實現,也可能是為了執行效率。
實現線程的方式
A.使用內核線程實現內核線程(Kernel-Level Thread, KLT)就是直接由操作系統內核支持的線程。
內核來完成線程切換
內核通過調度器Scheduler調度線程,並將線程的任務映射到各個CPU上
程序使用內核線程的高級接口,輕量級進程(Light Weight Process,LWP)
用戶態和內核態切換消耗內核資源
使用用戶線程實現
系統內核不能感知線程存在的實現
用戶線程的建立、同步、銷毀和調度完全在用戶態中完成
所有線程操作需要用戶程序自己處理,復雜度高
用戶線程加輕量級進程混合實現
輕量級進程作為用戶線程和內核線程之間的橋梁
● 請問什么是JVM內存模型?
考察點:JVM內存模型
參考回答:
Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。

● 請列舉一下,在JAVA虛擬機中,哪些對象可作為ROOT對象?
考察點:JAVA虛擬機
參考回答:
虛擬機棧中的引用對象
方法區中類靜態屬性引用的對象
方法區中常量引用對象
本地方法棧中JNI引用對象
● GC中如何判斷對象是否需要被回收?
考察點:JAVA虛擬機
參考回答:
即使在可達性分析算法中不可達的對象,也並非是“非回收不可”的,這時候它們暫時處於“等待”階段,要真正宣告一個對象回收,至少要經歷兩次標記過程:如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。(即意味着直接回收)
如果這個對象被判定為有必要執行finalize()方法,那么這個對象將會放置在一個叫做F-Queue的隊列之中,並在稍后由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這里所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。
finalize()方法是對象逃脫回收的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中跳出回收——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。
● 請說明一下JAVA虛擬機的作用是什么?
考察點:java虛擬機
參考回答:
解釋運行字節碼程序消除平台相關性。
jvm將java字節碼解釋為具體平台的具體指令。一般的高級語言如要在不同的平台上運行,至少需要編譯成不同的目標代碼。而引入JVM后,Java語言在不同平台上運行時不需要重新編譯。Java語言使用模式Java虛擬機屏蔽了與具體平台相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平台上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平台上的機器指令執行。
假設一個場景,要求stop the world時間非常短,你會怎么設計垃圾回收機制?
絕大多數新創建的對象分配在Eden區。
在Eden區發生一次GC后,存活的對象移到其中一個Survivor區。
在Eden區發生一次GC后,對象是存放到Survivor區,這個Survivor區已經存在其他存活的對象。
一旦一個Survivor區已滿,存活的對象移動到另外一個Survivor區。然后之前那個空間已滿Survivor區將置為空,沒有任何數據。
經過重復多次這樣的步驟后依舊存活的對象將被移到老年代。
● 請說明一下eden區和survial區的含義以及工作原理?
考察點:JVM
參考回答:
目前主流的虛擬機實現都采用了分代收集的思想,把整個堆區划分為新生代和老年代;新生代又被划分成Eden 空間、 From Survivor 和 To Survivor 三塊區域。
我們把Eden : From Survivor : To Survivor 空間大小設成 8 : 1 : 1 ,對象總是在 Eden 區出生, From Survivor 保存當前的幸存對象, To Survivor 為空。一次 gc 發生后: 1)Eden 區活着的對象 + From Survivor 存儲的對象被復制到 To Survivor ;
2) 清空 Eden 和 From Survivor ; 3) 顛倒 From Survivor 和 To Survivor 的邏輯關系: From 變 To , To 變 From 。可以看出,只有在 Eden 空間快滿的時候才會觸發 Minor GC 。而 Eden 空間占新生代的絕大部分,所以 Minor GC 的頻率得以降低。當然,使用兩個 Survivor 這種方式我們也付出了一定的代價,如 10% 的空間浪費、復制對象的開銷等。
● 請簡單描述一下JVM分區都有哪些?
考察點:JVM
參考回答:
java內存通常被划分為5個區域:程序計數器(Program Count Register)、本地方法棧(Native Stack)、方法區(Methon Area)、棧(Stack)、堆(Heap)。
● 請簡單描述一下類的加載過程
考察點:JVM
參考回答:
如下圖所示,JVM類加載機制分為五個部分:加載,驗證,准備,解析,初始化,下面我們就分別來看一下這五個過程。
加載
加載是類加載過程中的一個階段,這個階段會在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的入口。注意這里不一定非得要從一個Class文件獲取,這里既可以從ZIP包中讀取(比如從jar包和war包中讀取),也可以在運行時計算生成(動態代理),也可以由其它文件生成(比如將JSP文件轉換成對應的Class類)。
驗證
這一階段的主要目的是為了確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
准備
准備階段是正式為類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這里所說的初始值概念,比如一個類變量定義為:
public static int v = 8080;
實際上變量v在准備階段過后的初始值為0而不是8080,將v賦值為8080的putstatic指令是程序被編譯后,存放於類構造器<client>方法之中,這里我們后面會解釋。
public static final int v = 8080;
在編譯階段會為v生成ConstantValue屬性,在准備階段虛擬機會根據ConstantValue屬性將v賦值為8080。
解析
解析階段是指虛擬機將常量池中的符號引用替換為直接引用的過程。符號引用就是class文件中的:
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等類型的常量。
下面我們解釋一下符號引用和直接引用的概念:
符號引用與虛擬機實現的布局無關,引用的目標並不一定要已經加載到內存中。各種虛擬機實現的內存布局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規范的Class文件格式中。
直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。
初始化
初始化階段是類加載最后一個階段,前面的類加載階段之后,除了在加載階段可以自定義類加載器以外,其它操作都由JVM主導。到了初始階段,才開始真正執行類中定義的Java程序代碼。
初始化階段是執行類構造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變量的賦值操作和靜態語句塊中的語句合並而成的。虛擬機會保證<client>方法執行之前,父類的<client>方法已經執行完畢。p.s: 如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那么編譯器可以不為這個類生成<client>()方法。
注意以下幾種情況不會執行類初始化:
通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
定義對象數組,不會觸發該類的初始化。
常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。
通過類名獲取Class對象,不會觸發類的初始化。
通過Class.forName加載指定類時,如果指定參數initialize為false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
通過ClassLoader默認的loadClass方法,也不會觸發初始化動作。
類加載器
虛擬機設計團隊把加載動作放到JVM外部實現,以便讓應用程序決定如何獲取所需的類,JVM提供了3種類加載器:
啟動類加載器(Bootstrap ClassLoader):負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
擴展類加載器(Extension ClassLoader):負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。
應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。
JVM通過雙親委派模型進行類的加載,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類加載器。
當一個類加載器收到類加載任務,會先交給其父類加載器去完成,因此最終加載任務都會傳遞到頂層的啟動類加載器,只有當父類加載器無法完成加載任務時,才會嘗試執行加載任務。采用雙親委派的一個好處是比如加載位於rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object對象。
● 請簡單說明一下JVM的回收算法以及它的回收器是什么?還有CMS采用哪種回收算法?使用CMS怎樣解決內存碎片的問題呢?
考察點:JVM
參考回答:
垃圾回收算法
標記清除
標記-清除算法將垃圾回收分為兩個階段:標記階段和清除階段。在標記階段首先通過根節點,標記所有從根節點開始的對象,未被標記的對象就是未被引用的垃圾對象。然后,在清除階段,清除所有未被標記的對象。標記清除算法帶來的一個問題是會存在大量的空間碎片,因為回收后的空間是不連續的,這樣給大對象分配內存的時候可能會提前觸發full gc。
復制算法
將現有的內存空間分為兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中,之后,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收。
現在的商業虛擬機都采用這種收集算法來回收新生代,IBM研究表明新生代中的對象98%是朝夕生死的,所以並不需要按照1:1的比例划分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地拷貝到另外一個Survivor空間上,最后清理掉Eden和剛才用過的Survivor的空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1(可以通過-SurvivorRattio來配置),也就是每次新生代中可用內存空間為整個新生代容量的90%,只有10%的內存會被“浪費”。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保。
標記整理
復制算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在新生代經常發生,但是在老年代更常見的情況是大部分對象都是存活對象。如果依然使用復制算法,由於存活的對象較多,復制的成本也將很高。
標記-壓縮算法是一種老年代的回收算法,它在標記-清除算法的基礎上做了一些優化。首先也需要從根節點開始對所有可達對象做一次標記,但之后,它並不簡單地清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之后,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此,其性價比比較高。
增量算法
增量算法的基本思想是,如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反復,直到垃圾收集完成。使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
垃圾回收器
Serial收集器
Serial收集器是最古老的收集器,它的缺點是當Serial收集器想進行垃圾回收的時候,必須暫停用戶的所有進程,即stop the world。到現在為止,它依然是虛擬機運行在client模式下的默認新生代收集器,與其他收集器相比,對於限定在單個CPU的運行環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾回收自然可以獲得最高的單線程收集效率。
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用”標記-整理“算法。這個收集器的主要意義也是被Client模式下的虛擬機使用。在Server模式下,它主要還有兩大用途:一個是在JDK1.5及以前的版本中與Parallel Scanvenge收集器搭配使用,另外一個就是作為CMS收集器的后備預案,在並發收集發生Concurrent Mode Failure的時候使用。
通過指定-UseSerialGC參數,使用Serial + Serial Old的串行收集器組合進行內存回收。
ParNew收集器
ParNew收集器是Serial收集器新生代的多線程實現,注意在進行垃圾回收的時候依然會stop the world,只是相比較Serial收集器而言它會運行多條進程進行垃圾回收。
ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百的保證能超越Serial收集器。當然,隨着可以使用的CPU的數量增加,它對於GC時系統資源的利用還是很有好處的。它默認開啟的收集線程數與CPU的數量相同,在CPU非常多(譬如32個,現在CPU動輒4核加超線程,服務器超過32個邏輯CPU的情況越來越多了)的環境下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
-UseParNewGC: 打開此開關后,使用ParNew + Serial Old的收集器組合進行內存回收,這樣新生代使用並行收集器,老年代使用串行收集器。
Parallel Scavenge收集器
Parallel是采用復制算法的多線程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一個特點是它所關注的目標是吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能夠提升用戶的體驗;而高吞吐量則可以最高效率地利用CPU時間,盡快地完成程序的運算任務,主要適合在后台運算而不需要太多交互的任務。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多線程和”標記-整理”算法。這個收集器是在jdk1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。原因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外別無選擇。由於單線程的老年代Serial Old收集器在服務端應用性能上的”拖累“,即使使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,又因為老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合”給力“。直到Parallel Old收集器出現后,”吞吐量優先“收集器終於有了比較名副其實的應用祝賀,在注重吞吐量及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。
-UseParallelGC: 虛擬機運行在Server模式下的默認值,打開此開關后,使用Parallel Scavenge + Serial Old的收集器組合進行內存回收。-UseParallelOldGC: 打開此開關后,使用Parallel Scavenge + Parallel Old的收集器組合進行垃圾回收
CMS收集器
CMS(Concurrent Mark Swep)收集器是一個比較重要的回收器,現在應用非常廣泛,我們重點來看一下,CMS一種獲取最短回收停頓時間為目標的收集器,這使得它很適合用於和用戶交互的業務。從名字(Mark Swep)就可以看出,CMS收集器是基於標記清除算法實現的。它的收集過程分為四個步驟:
初始標記(initial mark)
並發標記(concurrent mark)
重新標記(remark)
並發清除(concurrent sweep)
注意初始標記和重新標記還是會stop the world,但是在耗費時間更長的並發標記和並發清除兩個階段都可以和用戶進程同時工作。
G1收集器
G1收集器是一款面向服務端應用的垃圾收集器。HotSpot團隊賦予它的使命是在未來替換掉JDK1.5中發布的CMS收集器。與其他GC收集器相比,G1具備如下特點:
並行與並發:G1能更充分的利用CPU,多核環境下的硬件優勢來縮短stop the world的停頓時間。
分代收集:和其他收集器一樣,分代的概念在G1中依然存在,不過G1不需要其他的垃圾回收器的配合就可以獨自管理整個GC堆。
空間整合:G1收集器有利於程序長時間運行,分配大對象時不會無法得到連續的空間而提前觸發一次GC。
可預測的非停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
CMS:采用標記清除算法
解決這個問題的辦法就是可以讓CMS在進行一定次數的Full GC(標記清除)的時候進行一次標記整理算法,CMS提供了以下參數來控制:
-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
也就是CMS在進行5次Full GC(標記清除)之后進行一次標記整理算法,從而可以控制老年帶的碎片在一定的數量以內,甚至可以配置CMS在每次Full GC的時候都進行內存的整理。