19年7月份面試題集合【JVM、AQS】

- JVM垃圾回收的算法?
- 如何判斷一個對象要被垃圾回收?GC Roots是怎麼判斷的?
- 垃圾回收器有哪些?有啥區別?

猿們都知道JVM的內存結構包括五大區域:程序計數器、虛擬機棧、本地方法棧、堆區、方法區。其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生、隨線程而滅,因此這幾個區域的內存分配和回收都具備確定性,就不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就跟隨着回收了。而Java堆區和方法區則不一樣、不一樣!(怎麼不一樣說的朗朗上口),這部分內存的分配和回收是動態的,正是垃圾收集器所需關注的部分。
在這裏插入圖片描述

垃圾收集器在對堆區和方法區進行回收前,首先要確定這些區域的對象哪些可以被回收,哪些暫時還不能回收,這就要用到判斷對象是否存活的算法!(面試官肯定沒少問你吧)

【回顧】jvm中的堆棧與數據結構中的堆棧
堆棧這個概念存在於數據機構中,也存在於jvm虛擬機中,但是這兩個概念不是相同的。棧(局部變量),堆(對象)
在數據結構中,堆和棧是數據結構,堆是完全二叉樹,堆中個元素是有序的。建堆的過程就是一個排序的過程,堆的查詢效率也很高。而棧是一種特殊的線性表,具有先進後出,只允許在一端(棧頂)插入、刪除的特點。
在這裏插入圖片描述

2.1 引用計數算法

2.1.1 算法分析

引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數。當一個對象被創建時,就將該對象實例分配給一個變量,該變量計數設置爲1。當任何其它變量被賦值爲這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1。

2.1.2 優缺點

  • 優點:引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。
  • 缺點:無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0

2.2 可達性分析算法

可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關係看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點,無用的節點將會被判定爲是可回收的對象。

G1與CMS對比有一下不同:

1.分代: CMS中,堆被分爲PermGen,YoungGen,OldGen;而YoungGen又分了兩個survivo區域。在G1中,堆被平均分成幾個區域(region),在每個區域中,雖然也保留了新老代的概念,但是收集器是以整個區域爲單位收集的。

2.算法: 相對於CMS的“標記——清理”算法,G1會使用壓縮算法,保證不產生多餘的碎片。收集階段,G1會將某個區域存活的對象拷貝的其他區域,然後將整個區域整個回收。

3.停頓時間可控: 爲了縮短停頓時間,G1建立可預存停頓模型,這樣在用戶設置的停頓時間範圍內,G1會選擇適當的區域進行收集,確保停頓時間不超過用戶指定時間。

垃圾收集器做個小總結:
串行回收,Serial回收器,單線程回收,全程stw;
並行回收,名稱以Parallel開頭的回收器,多線程回收,全程stw;
併發回收,cms與G1,多線程分階段回收,只有某階段會stw;

【回顧】說下AQS的實現原理?如何解決併發獲取鎖的問題?
例題:說下AQS的實現原理?如何解決併發獲取鎖的問題?
3、併發基礎組件AQS與ReetrantLock
AQS工作原理概要

AbstractQueuedSynchronizer又稱爲隊列同步器(後面簡稱AQS),它是用來構建鎖或其他同步組件的基礎框架,內部通過一個int類型的成員變量state來控制同步狀態,當state=0時,則說明沒有任何線程佔有共享資源的鎖,當state=1時,則說明有線程目前正在使用共享變量,其他線程必須加入同步隊列進行等待,AQS內部通過內部類Node構成FIFO的同步隊列來完成線程獲取鎖的排隊工作,同時利用內部類ConditionObject構建等待隊列,當Condition調用wait()方法後,線程將會加入等待隊列中,而當Condition調用signal()方法後,線程將從等待隊列轉移動同步隊列中進行鎖競爭。注意這裏涉及到兩種隊列,一種的同步隊列,當線程請求鎖而等待的後將加入同步隊列競爭,而另一種則是等待隊列(可有多個),通過Condition調用await()方法釋放鎖後,將加入等待隊列。關於Condition的等待隊列我們後面再分析,這裏我們先來看看AQS中的同步隊列模型,如下
在這裏插入圖片描述
head和tail分別是AQS中的變量,其中head指向同步隊列的頭部,注意head爲空結點,不存儲信息。而tail則是同步隊列的隊尾,同步隊列採用的是雙向鏈表的結構這樣可方便隊列進行結點增刪操作。state變量則是代表同步狀態,執行當線程調用lock方法進行加鎖後,如果此時state的值爲0,則說明當前線程可以獲取到鎖(在本篇文章中,鎖和同步狀態代表同一個意思),同時將state設置爲1,表示獲取成功。如果state已爲1,也就是當前鎖已被其他線程持有,那麼當前執行線程將被封裝爲Node結點加入同步隊列等待。其中Node結點是對每一個訪問同步代碼的線程的封裝,從圖中的Node的數據結構也可看出,其包含了需要同步的線程本身以及線程的狀態,如是否被阻塞,是否等待喚醒,是否已經被取消等。每個Node結點內部關聯其前繼結點prev和後繼結點next,這樣可以方便線程釋放鎖後快速喚醒下一個在等待的線程,Node是AQS的內部類。

例題:說下Synchronized和ReentrantLock的區別和實現原理?
4、基於ReetrantLock分析AQS獨佔模式實現過程
這裏獲取鎖時,首先對同步狀態執行CAS操作,嘗試把state的狀態從0設置爲1,如果返回true則代表獲取同步狀態成功,也就是當前線程獲取鎖成,可操作臨界資源,如果返回false,則表示已有線程持有該同步狀態(其值爲1),獲取鎖失敗,注意這裏存在併發的情景,也就是可能同時存在多個線程設置state變量,因此是CAS操作保證了state變量操作的原子性。返回false後,執行 acquire(1)方法,該方法是AQS中的方法,它對中斷不敏感,即使線程獲取同步狀態失敗,進入同步隊列,後續對該線程執行中斷操作也不會從同步隊列中移出

從代碼執行流程可以看出,這裏做了兩件事,一是嘗試再次獲取同步狀態,如果獲取成功則將當前線程設置爲OwnerThread,否則失敗,二是判斷當前線程current是否爲OwnerThread,如果是則屬於重入鎖,state自增1,並獲取鎖成功,返回true,反之失敗,返回false,也就是tryAcquire(arg)執行失敗,返回false。需要注意的是nonfairTryAcquire(int acquires)內部使用的是CAS原子性操作設置state值,可以保證state的更改是線程安全的,因此只要任意一個線程調用nonfairTryAcquire(int acquires)方法並設置成功即可獲取鎖,不管該線程是新到來的還是已在同步隊列的線程,畢竟這是非公平鎖,並不保證同步隊列中的線程一定比新到來線程請求(可能是head結點剛釋放同步狀態然後新到來的線程恰好獲取到同步狀態)先獲取到鎖,這點跟後面還會講到的公平鎖不同。ok~,接着看之前的方法acquire(int arg)

接口預熱
通常, 一些基於java技術棧的查詢服務系統, 在服務剛剛啓動完時, 由於類加載, hotspot的jit優化機制, lazy加載的資源或數據等問題, 會出現接口響應時間的抖動.

在類加載完畢, jit code cache預熱完畢或資源加載完畢後, 服務的響應時間會恢復正常. 這種抖動在QPS特別高的時候, 影響尤其嚴重, 帶來系統服務質量的下降, 進而影響用戶體驗.

因此我們想到, 在服務上線前, 先自發地調用一下, 等性能穩定後, 再暴露服務, 這就是基本的預熱思路.

消息中間件可靠性+可用性

消息丟失是使用消息中間件時所不得不面對的一個同點,其背後消息可靠性也是衡量消息中間件好壞的一個關鍵因素。尤其是在金融支付領域,消息可靠性尤爲重要。然而說到可靠性必然要說到可用性,注意這兩者之間的區別,消息中間件的可靠性是指對消息不丟失的保障程度;而消息中間件的可用性是指無故障運行的時間百分比。

從狹義的角度來說,分佈式系統架構是一致性協議理論的應用實現,對於消息可靠性和可用性而言也可以追溯到消息中間件背後的一致性協議。對於Kafka而言,其採用的是類似PacificA的一致性協議,通過ISR(In-Sync-Replica)來保證多副本之間的同步,並且支持強一致性語義(通過acks實現)。對應的RabbitMQ是通過鏡像環形隊列實現多副本及強一致性語義的。多副本可以保證在master節點宕機異常之後可以提升slave作爲新的master而繼續提供服務來保障可用性。Kafka設計之初是爲日誌處理而生,給人們留下了數據可靠性要求不要的不良印象,但是隨着版本的升級優化,其可靠性得到極大的增強,詳細可以參考KIP101。就目前而言,在金融支付領域使用RabbitMQ居多,而在日誌處理、大數據等方面Kafka使用居多,隨着RabbitMQ性能的不斷提升和Kafka可靠性的進一步增強,相信彼此都能在以前不擅長的領域分得一杯羹。

MySQL分庫分表原理

  • 數據量
    MySQL單庫數據量在5000萬以內性能比較好,超過閾值後性能會隨着數據量的增大而變弱。MySQL單表的數據量是500w-1000w之間性能比較好,超過1000w性能也會下降。
  • 磁盤
    因爲單個服務的磁盤空間是有限制的,如果併發壓力下,所有的請求都訪問同一個節點,肯定會對磁盤IO造成非常大的影響。
  • 數據庫連接
    數據庫連接是非常稀少的資源,如果一個庫裏既有用戶、商品、訂單相關的數據,當海量用戶同時操作時,數據庫連接就很可能成爲瓶頸。

垂直拆分 or 水平拆分?

當我們單個庫太大時,我們先要看一下是因爲表太多還是數據量太大,如果是表太多,則應該將部分表進行遷移(可以按業務區分),這就是所謂的垂直切分。如果是數據量太大,則需要將表拆成更多的小表,來減少單表的數據量,這就是所謂的水平拆分。


ConcurrentHashMap(JDK1.7)

HashMap非線程安全的,HashTable是線程安全的,所有涉及到多線程操作的都加上了synchronized關鍵字來鎖住整個table,這就意味着所有的線程都在競爭一把鎖,在多線程的環境下,它是安全的,但是無疑效率低下的。

在JDK1.7中,ConcurrentHashMap的數據結構是由一個Segment數組和多個HashEntry組成的
在這裏插入圖片描述
Segment數組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是鎖分離技術,而每一個Segment元素存儲的是HashEntry數組+ 鏈表。分段是一開始就確定的,後期不能再進行擴容(即併發度不能改變),但是單個Segment裏面的數組是可以擴容的。


回顧HashMap在這裏插入圖片描述
map的大致容貌是這樣的,當put一個對象的時候會根據對象的hash值計算出它在數組中存放的位置(通過擾動函數計算,後面會講到),然後判斷這個位置上有沒有已經存在的對象,如果沒有就直接放到這個位置,如果有將已存在對象的next指向當前對象形成一個鏈表,當鏈表長度超過一定數量之後,鏈表會轉換成紅黑樹(這是java8之後的修改,爲了提升查詢效率)。所以hashmap本質上是一個二維數組加鏈表加紅黑樹的組合。

對舊的容量判斷是否需要擴容,如果需要擴容,新的數據容量大小爲原來的2倍(newThr = oldThr << 1; 假設oldThr爲16,轉換成2進制之後左移一位結果是32,如果再次擴容左移一位,結果是64 )。算出新的容量大小時候先創建指定大小的空數組,然後將原來的數組數據複製過來,輪詢原數組,利用擾動函數重新計算出位置,如果不是鏈表就直接放入,如果是鏈表以及紅黑樹,則就相應的方法複製數據。

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