1. 程式人生 > >深入java虛擬機器面試

深入java虛擬機器面試

請你說一下jvm:

對於虛擬機器我給它分成三大塊內容分別是:記憶體管理、類載入引擎技術、執行緒安全
首先就記憶體管理來說,
虛擬機器給記憶體劃分為:堆、棧、方法區、執行時常量池、本地方法棧、直接記憶體。
堆是執行緒共享的記憶體塊,而堆又細化分為eden區年輕代、survivor區倖存區、Old老年代。
在堆記憶體中建立物件的時候,首先判斷eden區是否能滿足當前物件的儲存空間,如果不滿足,則對eden進行使用複製演算法進行gc,
將滿足年齡物件移到倖存區,如果還不滿足,則啟動倖存區擔保策略,對老年代使用 標記-整理 或 標記清除演算法 進行Full GC,
將滿足年齡的物件移到老年代,如果空間還是不滿足,則報出OOM記憶體溢位異常。否則在eden區中分配物件。
而虛擬機器對物件年齡的判斷可以通過引數來設定,不過目前預設是動態設定,我認為這個方式也比較科學。
其次就是對於大物件,虛擬機器也有做判斷,如果超過引數指定的大小則直接放到老年代。
所以開發的時候應該避免大物件的使用,因為Full GC的是通過 stop world進行的,而且耗時比年輕代要大的多,
10倍以上,因此會造成服務短時間暫停服務的現象。

對於棧來說,是提供給執行緒獨立使用的,每個執行緒都有自己的棧空間,執行緒的方法在棧中會獨佔一塊空間稱為棧幀,
程式的執行通常是通過出棧和壓棧的方式將棧幀放到棧頂去執行的,棧內建立的私有變數和棧幀中的區域性變數只能本執行緒訪問,
如果通過傳參的方式使棧幀中的變數被其他棧幀訪問,則稱為方法逃逸,如果棧中的私有變數被其他執行緒訪問稱為執行緒逃逸。
該技術在1.7以後虛擬機器中給同步鎖提供鎖消除的技術支援。

方法區是當位元組碼資料經過類載入器載入後將類描述資訊等一些指令和初始化的靜態物件和常量存放的空間,1.7以後就和堆區別開
使用記憶體塊了,當虛擬機器進行GC的時候會順便來回收一下方法區的物件。

本地方法棧使用的是系統記憶體,屬於本地方法的c或c++的底層方法自己管理的記憶體

直接記憶體,是nio通過本地函式庫直接分配的堆外記憶體,通過堆中的DirectByteBuffer物件來引用這塊記憶體操作。

說到GC演算法,那麼就可以說到GC的收集器的選擇。
年輕代收集可以使用並行收集器(parallel scavenge),它採用的是複製演算法,並且可以通過引數來控制gc的時間或吞吐量,
當然也可以設定自適應調整。那老年代的收集就可以使用serial old收集器了,他是使用標記-整理演算法。
當然如果你的服務對吞吐量和cpu特別敏感,那麼不妨老年代使用parallel old來執行。
如果是對實時性要求比較高的網路應用來說,可以使用cms收集器,它是可以和使用者執行緒一起執行的收集器,但是在收集垃圾的時候
使用者執行緒響應會變慢,因為對cpu敏感,而且如果浮動垃圾過多無法處理的話,它會重新對老年代開啟serial old收集器收集,停頓時間更長。
不過我比較看好G1收集器,它是將記憶體標記成若干個region,通過維護一個標記region按價值大小排序的佇列,去根據允許的時間來優先收集
價值比較大的region,可以做到低停頓的響應時間和次數的平衡。

當然還可以擴充套件到jvm檢測工具、jvm的實戰優化

其次就是類載入引擎技術:
對執行引擎來說,它不會去管位元組碼的來源是.class檔案還是來源與符合執行引擎規則的十六進位制通過socket流來的資料集(這裡就不把《深入理解java虛擬機器》的列出來了,有興趣的同學可以看書),
這都是可以執行的指令(熱部署技術的核心思想就是基於該技術的實現)。

它會通過雙親委派模型來對這些資料集進行載入、驗證、準備、解析、初始化載入到記憶體中。這裡就不展開講這些過程了。


執行緒安全:“當多個執行緒訪問一個物件時,如果不用考慮這些執行緒的排程和交替執行,也無需額外進行同步,或者
呼叫方進行其他協同操作,最後呼叫這個物件的行為都可以獲得正確的結果,這就是執行緒安全”
執行緒安全產生的原因是併發,接下來就可以聊併發的
鎖、鎖優化、volatile 的語義、cas、以及jdk的原子操作的類Atomioc*、
Concurrent包
-----------------------------------------------------------------------------------------------
如何判斷物件可以被GC?
可達性分析,是指當一個物件的根節點指向為空的時候表示這個物件沒有引用了,可以被回收。
------------------------------------------------------------
樂觀鎖悲觀鎖:
悲觀鎖:每次拿資料都上鎖,確保自己使用的過程中不會被其他執行緒訪問,使用完後再解鎖。
樂觀鎖:每次訪問資料都不上鎖,但是更新的時候去判斷該期間是否被別人修改過
(阿里人說版本號控制,AtomicStampedReference類,
深入理解jvm書中jdk7版本[第十三章]說這個比較雞肋,因為大部分情況下ABA問題不會影響併發正確性,
如果需要解決ABA問題傳統的synchronized效率可能比這個要高)
期間資料可以被其他執行緒任意訪問。


---------------------------------------------------------------------
ThreadLocal,主要用於變數安全隔離,實現原理是Thread引用ThreadLocal中的map來儲存這個變數。每次訪問變數都是訪問當前執行緒的ThreadLocal中的map。
不過當不在使用map時一定要顯示的remove掉當前執行緒key=value的變數,因為value是強引用,如果不顯示幹掉,其他的執行緒無無法訪問到可能
會導致OOM的傳送。key是弱引用,噹噹前執行緒結束就可以被回收了。hibernate的session就是通過ThreadLocal來儲存實現安全隔離的。
-------------------------------------------------------------------------------------------
說一下ReentrantLock比synchronized的優勢?
區別:語法上ReentrantLock通過 lock() unlock() 來完成,synchronized是原生語法層面上的互斥鎖
ReentrantLock 優勢:
1.增加了等待可中斷,
2.可實現公平鎖(按申請時間順序獲得鎖,通過構造方法帶boolean值來要求使用公平鎖,預設費公平)、
3.鎖可以繫結多個條件(可多次通過newCondition()方法繫結條件)
---------------------------------------------------------------------
synchronized 實現原理:物件互斥同步通過臨界區、互斥量、訊號量等實現方式實現互斥。
java中通過monitorenter、monitorexit兩個位元組碼指令和reference型別的引數來指明要鎖定和解鎖的物件。
如果synchronized修飾的是例項方法還是類方法,取對應的物件例項或Class物件做為鎖物件。
當monitorenter執行時,開始嘗試獲取物件鎖,如果這個物件沒鎖定或當前執行緒已經有了那個物件的鎖,就將
鎖計數器加1.而執行monitorexit則鎖物件減1,如果獲取鎖物件失敗,則將當前執行緒阻塞等待鎖被其他執行緒釋放。
在jdk1.7的hotspot之後,傳統synchronized做了鎖優化,效率跟ReentrantLock差不多了


鎖優化:自旋鎖:-XX: preBlockspin 改變自選次數、jdk1.6後預設開啟通過虛擬機器線上統計自旋成功概率比較高的次數動態設定
(自旋是,執行緒A持有物件鎖後,執行緒B來嘗試獲取該鎖,但發現已經被其他執行緒持有了,執行緒B就自己執行了一次空操作
jvm執行時,會統計自旋成功率最高的次數動態設定執行緒的自旋次數,超過該次數後執行緒釋放執行權)

鎖消除:檢測到不可能存在資料競爭的鎖進行消除,主要依靠逃逸分析技術支援

輕量鎖:和傳統鎖一樣通過使用作業系統的互斥量來實現的,但比傳統鎖更少的互斥量產生的效能消耗,旨在沒有多執行緒競爭的前提下
執行緒獲取鎖的流程:通過在棧幀中建立LockRecord鎖記錄空間,然後通過cas將物件頭Mark word資訊寫到鎖記錄空間,
如果更新失敗檢查是否已持有該鎖(Mark word指向當前棧幀),否則產生競爭該鎖膨脹為重量鎖10

偏向鎖:當獲取第一個獲取該鎖的執行緒再次獲取該鎖則不用做同步,假如有其他執行緒嘗試獲取該鎖,則結束該狀態轉向未鎖定或輕量鎖狀態

---------------------------------------------------------------
你跟我說一下併發:
併發是指通過壓榨cpu資源達到應用服務效能提高的一種手段。隨著併發的高效會產生共享資料的執行緒安全問題。
傳統的做法是通過將共享資源同步,阻塞式的手段讓共享資料實現執行緒安全。但是這樣就會讓效率的瓶頸卡在共享資料。
為了應對更多的執行緒安全問題,jdk中實現了兩大類的技術手段,阻塞式,非阻塞式。
所謂的非阻塞式思想是基於硬體指令集的進步,語義上多次操作的行為使用一條指令完成。這就是CAS(原子性操作)。
jdk實現的類有AtomicInteger、AtomicStampedReference等類,都是通過volatile關鍵字來控制字面量
內部使用Unsafe.compareAndSwap提供支援,作業系統上(x86)cmpxchg指令來執行

--------------------------以下部分來源於importnew------------------------------------------------------------------------------------------------------

阻塞式的類:

ArrayBlockingQueue,有界阻塞佇列的陣列,按先進先出原則存取,定義定長後不可變,
如果試圖在佇列已滿中插入元素會導致操作受阻塞,試圖從空那個佇列取元素將導致類似阻塞
適合生產消費者模型的公平策略。使用ReentrantLock鎖住整個佇列


LinkeBlockingQueue,基於已連結節點的,範圍任意的佇列,先進先出存取,吞吐量高於陣列佇列,但是其可預知的效能要低
要指定佇列擴充套件範圍,預設Integer.MAX_VALUE。佇列沒有超出容量的情況下,每次插入都是動態建立連結節點。
若佇列沒有指定容量,生產者的速度一旦大於消費者速度,會把系統記憶體全部吃掉。
使用讀鎖 和 寫鎖來控制同步
陣列佇列和連結串列佇列的區別在於,連結串列動態擴充套件佇列,每次在next建立節點,用讀鎖和寫鎖分別控制生產消費者的同步,
陣列佇列,建立後不能擴充套件,使用單個鎖控制讀寫同步


ConcurrentLinkedQueue,使用非阻塞式執行緒安全策略,使用cas原理,在原始碼中使用hops>1(預設)來控制減少cas的寫操作,
達到優化效率的目的
取節點:首先取head節點,判斷是否為null,是則表示其他執行緒取走了,不為空則使用cas方式設定頭節點引用為null,
如果設定失敗表示已經被取走,需要重新獲取head節點


ConcurrentHashMap,使用陣列+連結串列的資料結構採用分段式segmens鎖表技術來實現執行緒安全的map,
在get資料的時候segmens是不加鎖的(volatile 的“先行原則”先寫在讀),put操作是先判斷是否需要擴容,
第二定位存放的HashEntry,擴容只針對segment

CopyOnWriteArrayList,寫時複製容器,當新增元素是,把舊的容器cp到新的容器中並新增元素,讀的時候也能讀到舊值,
因為舊的容器沒有被鎖。


執行緒管理:執行緒池:通過工程類Executors 去建立一個執行緒池,來管理執行緒的生命週期。
1.初始化ThreadPoolExecutor物件。當提交任務時,建立一個執行緒執行任務,直到指定執行緒數量等於corePoolSize
2.如果繼續提交任務,則儲存阻塞佇列中等待被執行,如果阻塞佇列被塞滿了,且繼續提交任務,則建立新的執行緒
處理任務且當前執行緒數量小於maximumPoolSize,如果佇列飽和執行緒無空閒,則預設丟擲異常AbortPolicy,
可選(丟棄任務,DiscardPolicy|CallerRunsPolicy用呼叫者來執行|DiscardOldestPolicy丟掉靠前的,執行當前任務)
如果跑完所以任務後執行緒會存活一定時間keepAliveTime
3.實現方法:
3.1 newFixedThreadPool 初始化指定數量的執行緒池,corePoolSize==maximumPoolSize,使用
LinkedBlockingQueue作為阻塞佇列,沒有任務也不釋放執行緒
3.2 newCachedThreadPool 初始可以快取執行緒的池子,預設快取60s,maximumPoolSize=Integer.MAX_VALUE
使用 SynchronusQueue 作為阻塞佇列,當執行緒空閒時間超過keepAliveTime,使用該方式注意控制執行緒數量
3.3 newSingleThreadExecutor。初始化的執行緒池中只有一個執行緒,如果執行緒異常結束,重啟新的執行緒繼續執行任務
內部使用LinkedBlockingQueue做阻塞佇列
3.4 newSchedulerThreadPool 初始化執行緒池可以在指定的時間內週期性的執行所提交的任務,實際業務場景中
可以是hi用該執行緒池定期的同步資料。
execute()方法提交的任務必須實現Runnable介面,無法判斷任務是否執行成功
ExecutorService.submit() 提交任務可以獲取任務執行完的返回值

新建執行緒的流程:
1.通過workerCountOf方法根據ctl的低29為得到執行緒池的當前執行緒數,如果當前執行緒小於corePoolSize
則建立新執行緒執行任務,否則
2.判斷執行緒是否是RUNNING狀態,且提交的任務放到阻塞佇列,否則再次檢查執行緒池的狀態,不是RUNNING
在執行reject按預設策略處理這個任務;
3.否則執行addWorker方法建立新執行緒執行任務,失敗。執行reject處理任務。