Java面試底層原理


面試發現經常有些重復的面試問題,自己也應該學會記錄下來,最好自己能做成筆記,在下一次面的時候說得有條不紊,深入具體,面試官想必也很開心。以下是我個人總結,請參考:

HashSet底層原理:(問了大幾率跟HashMap一起面)

HashMap底層原理:(非常大幾率問到)

Hashtable底層原理:(問的少,問了大幾率問你跟HashMap的區別)

synchronized底層如何實現?鎖優化,怎么優化?

ReentrantLock 底層實現;

ConcurrentHashMap 的工作原理,底層原理(談到多線程高並發大幾率會問它)

JVM調優(JVM層層漸進問時大幾率問)

JVM內存管理,JVM的常見的垃圾收集器,GC調優,Minor GC ,Full GC 觸發條件(像是必考題)

java內存模型

線程池的工作原理(談到多線程高並發大幾率會問它)

ThreadLocal的底層原理(有時問)

voliate底層原理

NIO底層原理

IOC底層實現原理(Spring IOC ,AOP會問的兩個原理,面試官經常會問看過源碼嗎?所以你有所准備吧)

AOP底層實現原理

MyisAM和innodb的有關索引的疑問(容易混淆,可以問的會深入)

HashSet底層原理:(面試過)
http://zhangshixi.iteye.com/blog/673143

https://blog.csdn.net/HD243608836/article/details/80214413

HashSet實現Set接口,由哈希表(實際上是一個HashMap實例)支持。它不保證set 的迭代順序;特別是它不保證該順序恆久不變。此類允許使用null元素。

2.    HashSet的實現:

對於HashSet而言,它是基於HashMap實現的,HashSet底層使用HashMap來保存所有元素,因此HashSet 的實現比較簡單,相關HashSet的操作,基本上都是直接調用底層HashMap的相關方法來完成, (實際底層會初始化一個空的HashMap,並使用默認初始容量為16和加載因子0.75。)

HashSet的源代碼

對於HashSet中保存的對象,請注意正確重寫其equals和hashCode方法,以保證放入的對象的唯一性。

插入
當有新值加入時,底層的HashMap會判斷Key值是否存在(HashMap細節請移步深入理解HashMap),如果不存在,則插入新值,同時這個插入的細節會依照HashMap插入細節;如果存在就不插入

HashMap底層原理:

1.    HashMap概述:

HashMap是基於哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。

2.    HashMap的數據結構:

HashMap實際上是一個“數組+鏈表+紅黑樹”的數據結構

3.    HashMap的存取實現:

(1.8之前的)

當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那么在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

1.8:

put():

  1. 根據key計算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);

  2. 根據key.hash計算得到桶數組的索引index = key.hash & (table.length - 1),這樣就找到該key的存放位置了:

① 如果該位置沒有數據,用該數據新生成一個節點保存新數據,返回null;

② 如果該位置有數據是一個紅黑樹,那么執行相應的插入 / 更新操作

③ 如果該位置有數據是一個鏈表,分兩種情況一是該鏈表沒有這個節點,另一個是該鏈表上有這個節點,注意這里判斷的依據是key.hash是否一樣: 如果該鏈表沒有這個節點,那么采用尾插法新增節點保存新數據,返回null; 如果該鏈表已經有這個節點了,那么找到該節點並更新新數據,返回老數據。 注意: HashMap的put會返回key的上一次保存的數據。

get():

計算需獲取數據的hash值(計算過程跟put一樣),計算存放在數組table中的位置(計算過程跟put一樣),然后依次在數組,紅黑樹,鏈表中查找(通過equals()判斷),最后再判斷獲取的數據是否為空,若為空返回null否則返回該數據

樹化與還原

  • 哈希表的最小樹形化容量

  • 當哈希表中的容量大於這個值時(64),表中的桶才能進行樹形化

  • 否則桶內元素太多時會擴容,而不是樹形化

  • 為了避免進行擴容、樹形化選擇的沖突,這個值不能小於 4 * TREEIFY_THRESHOLD

  • 一個桶的樹化閾值

  • 當桶中元素個數超過這個值時(8),需要使用紅黑樹節點替換鏈表節點

  • 這個值必須為 8,要不然頻繁轉換效率也不高

  • 一個樹的鏈表還原閾值

  • 當擴容時,桶中元素個數小於這個值(6),就會把樹形的桶元素 還原(切分)為鏈表結構

  • 這個值應該比上面那個小,至少為 6,避免頻繁轉換

條件1. 如果當前桶數組為null或者桶數組的長度 < MIN_TREEIFY_CAPACITY(64),則進行擴容處理(見代碼片段2:resize());

條件2. 當不滿足條件1的時候則將桶中鏈表內的元素轉換成紅黑樹!!!稍后再詳細討論紅黑樹。

擴容機制的實現

  1. 擴容(resize)就是重新計算容量。當向HashMap對象里不停的添加元素,而HashMap對象內部的桶數組無法裝載更多的元素時,HashMap對象就需要擴大桶數組的長度,以便能裝入更多的元素。

  2. capacity 就是數組的長度/大小,loadFactor 是這個數組填滿程度的最大比比例。

  3. size表示當前HashMap中已經儲存的Node<key,value>的數量,包括桶數組和鏈表 / 紅黑樹中的的Node<key,value>。

  4. threshold表示擴容的臨界值,如果size大於這個值,則必需調用resize()方法進行擴容。

  5. 在jdk1.7及以前,threshold = capacity * loadFactor,其中 capacity 為桶數組的長度。 這里需要說明一點,默認負載因子0.75是是對空間和時間(縱向橫向)效率的一個平衡選擇,建議大家不要修改。 jdk1.8對threshold值進行了改進,通過一系列位移操作算法最后得到一個power of two size的值

什么時候擴容

當向容器添加元素的時候,會判斷當前容器的元素個數,如果大於等於閾值---即當前數組的長度乘以加載因子的值的時候,就要自動擴容啦。

擴容必須滿足兩個條件:

1、 存放新值的時候   當前已有元素的個數  (size) 必須大於等於閾值

2、 存放新值的時候當前存放數據發生hash碰撞(當前key計算的hash值換算出來的數組下標位置已經存在值)

//如果計算的哈希位置有值(及hash沖突),且key值一樣,則覆蓋原值value,並返回原值value

      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

        V oldValue = e.value;

        e.value = value;

        e.recordAccess(this);

        return oldValue;

      }

resize()方法: 該函數有2種使用情況1.初始化哈希表 2.當前數組容量過小,需擴容

過程:

插入鍵值對時發現容量不足,調用resize()方法方法,

1.首先進行異常情況的判斷,如是否需要初始化,二是若當前容量》最大值則不擴容,

2.然后根據新容量(是就容量的2倍)新建數組,將舊數組上的數據(鍵值對)轉移到新的數組中,這里包括:(遍歷舊數組的每個元素,重新計算每個數據在數組中的存放位置(原位置或者原位置+舊容量),將舊數組上的每個數據逐個轉移到新數組中,這里采用的是尾插法。)

3.新數組table引用到HashMap的table屬性上

4.最后重新設置擴容闕值,此時哈希表table=擴容后(2倍)&轉移了舊數據的新table

synchronized底層如何實現?鎖優化,怎么優化?

synchronized 是 Java 內建的同步機制,所以也有人稱其為 Intrinsic Locking,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其他試圖獲取的線程只能等待或者阻塞在那里。

原理:

synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進入到臨界區,同時它還可以保證共享變量的內存可見性

底層實現:

同步代碼塊是使用monitorenter和monitorexit指令實現的, ,當且一個monitor被持有之后,他將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor所有權,即嘗試獲取對象的鎖;

同步方法(在這看不出來需要看JVM底層實現)依靠的是方法修飾符上的ACC_SYNCHRONIZED實現。  synchronized方法則會被翻譯成普通的方法調用和返回指令如:invokevirtual、areturn指令,在VM字節碼層面並沒有任何特別的指令來實現被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標志位置1,表示該方法是同步方法並使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示 Klass 做為鎖對象。

Java對象頭和monitor是實現synchronized的基礎!

synchronized存放的位置:

synchronized用的鎖是存在Java對象頭里的。

其中, Java對象頭包括:

Mark Word(標記字段): 用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳等等。它是實現輕量級鎖和偏向鎖的關鍵

Klass Pointer(類型指針): 是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例

monitor:  可以把它理解為一個同步工具, 它通常被描述為一個對象。 是線程私有的數據結構

鎖優化,怎么優化?

jdk1.6對鎖的實現引入了大量的優化。 鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。 注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。 重量級鎖降級發生於STW階段,降級對象為僅僅能被VMThread訪問而沒有其他JavaThread訪問的對象。( HotSpot JVM/JRockit JVM是支持鎖降級的)

偏斜鎖:

當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操作(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,所以並不涉及真正的互斥鎖。

自旋鎖:

自旋鎖 for(;;)結合cas確保線程獲取取鎖

就是讓該線程等待一段時間,不會被立即掛起,看持有鎖的線程是否會很快釋放鎖。怎么等待呢?執行一段無意義的循環即可(自旋)。

輕量級鎖:

引入偏向鎖主要目的是:為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑。 當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖

重量級鎖:

重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。

ReentrantLock 底層實現

https://blog.csdn.net/u011202334/article/details/73188404

AQS原理:

AQS和Condition各自維護了不同的隊列,在使用lock和condition的時候,其實就是兩個隊列的互相移動。如果我們想自定義一個同步器,可以實現AQS。它提供了獲取共享鎖和互斥鎖的方式,都是基於對state操作而言的。

概念+實現:

ReentrantLock實現了Lock接口,是AQS( 一個用來構建鎖和同步工具的框架, AQS沒有 鎖之 類的概念)的一種。加鎖和解鎖都需要顯式寫出,注意一定要在適當時候unlock。ReentranLock這個是可重入的。其實要弄明白它為啥可重入的呢,咋實現的呢。其實它內部自定義了同步器Sync,這個又實現了AQS,同時又實現了AOS,而后者就提供了一種互斥鎖持有的方式。其實就是每次獲取鎖的時候,看下當前維護的那個線程和當前請求的線程是否一樣,一樣就可重入了。

和synhronized相比:

synchronized相比,ReentrantLock用起來會復雜一些。在基本的加鎖和解鎖上,兩者是一樣的,所以無特殊情況下,推薦使用synchronized。ReentrantLock的優勢在於它更靈活、更強大,增加了輪訓、超時、中斷等高級功能。

可重入鎖。可重入鎖是指同一個線程可以多次獲取同一把鎖。ReentrantLock和synchronized都是可重入鎖。

可中斷鎖。可中斷鎖是指線程嘗試獲取鎖的過程中,是否可以響應中斷。synchronized是不可中斷鎖,而ReentrantLock則z,dz提供了中斷功能。

公平鎖與非公平鎖。公平鎖是指多個線程同時嘗試獲取同一把鎖時,獲取鎖的順序按照線程達到的順序,而非公平鎖則允許線程“插隊”。synchronized是非公平鎖,而ReentrantLock的默認實現是非公平鎖,但是也可以設置為公平鎖。

lock()和unlock()是怎么實現的呢?

由lock()和unlock的源碼可以看到,它們只是分別調用了sync對象的lock()和release(1)方法。而  Sync是ReentrantLock的內部類, 其擴展了AbstractQueuedSynchronizer。

lock():

final void lock() {

if (compareAndSetState(0, 1))

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1);

}

首先用一個CAS操作,判斷state是否是0(表示當前鎖未被占用),如果是0則把它置為1,並且設置當前線程為該鎖的獨占線程,表示獲取鎖成功。當多個線程同時嘗試占用同一個鎖時,CAS操作只能保證一個線程操作成功,剩下的只能乖乖的去排隊啦。( “非公平”即體現在這里)。

設置state失敗,走到了else里面。我們往下看acquire。

  1. 第一步。嘗試去獲取鎖。如果嘗試獲取鎖成功,方法直接返回。

2. 第二步,入隊。( 自旋+CAS組合來實現非阻塞的原子操作)

3. 第三步,掛起。 讓已經入隊的線程嘗試獲取鎖,若失敗則會被掛起

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

unlock():
流程大致為先嘗試釋放鎖,若釋放成功,那么查看頭結點的狀態是否為SIGNAL,如果是則喚醒頭結點的下個節點關聯的線程,

如果釋放失敗那么返回false表示解鎖失敗。這里我們也發現了,每次都只喚起頭結點的下一個節點關聯的線程。

public void unlock() {

sync.release(1);

}

public final boolean release(int arg) {

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

ConcurrentHashMap 的工作原理

概念:

ConcurrentHashMap的目標是實現支持高並發、高吞吐量的線程安全的HashMap。

1.8之前:

數據結構:

ConcurrentHashMap是由Segment數組結構和 多個HashEntry數組結構組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap里扮演鎖的角色,HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap里包含一個Segment數組,Segment的結構和HashMap相似,是一種數組和鏈表結構, 一個Segment里包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素, 每個Segment守護者一個HashEntry數組里的元素,當對HashEntry數組的數據進行修改時,必須首先獲得它對應的Segment鎖。

put和get的時候,都是現根據key.hashCode()算出放到哪個Segment中: ConcurrentHashMap中默認是把segments初始化為長度為16的數組

https://www.cnblogs.com/wuzhitong/p/8492228.html

1.8后:

變化:

ConcurrentHashMap的JDK8與JDK7版本的並發實現相比,最大的區別在於JDK8的鎖粒度更細,理想情況下talbe數組元素的大小就是其支持並發的最大個數

實現:

改進一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存數據,采用table數組元素作為鎖,從而實現了對每一行數據進行加鎖,進一步減少並發沖突的概率。

數據結構:

改進二:將原先table數組+單向鏈表的數據結構,變更為table數組+單向鏈表+紅黑樹的結構。對於hash表來說,最核心的能力在於將key hash之后能均勻的分布在數組中。

概念:

JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,並發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,但是已經簡化了屬性,只是為了兼容舊版本。

樹化和還原:

與HashMap一樣 。

一些成員:

Node是ConcurrentHashMap存儲結構的基本單元,繼承於HashMap中的Entry,用於存儲數據。 ,就是一個鏈表,但是只允許對數據進行查找,不允許進行修改

通過TreeNode作為存儲結構代替Node來轉換成黑紅樹。

TreeBin
TreeBin就是封裝TreeNode的容器,它提供轉換黑紅樹的一些條件和鎖的控制

// 讀寫鎖狀態

static final int WRITER = 1; // 獲取寫鎖的狀態

static final int WAITER = 2; // 等待寫鎖的狀態

static final int READER = 4; // 增加數據時讀鎖的狀態

構造器

public ConcurrentHashMap() {

} 初始化其實是一個空實現, 初始化操作並不是在構造函數實現的,而是在put操作中實現。 還提供了其他的構造函數,有指定容量大小或者指定負載因子,跟HashMap一樣。

存取實現:

put(): 對當前的table進行無條件自循環直到put成功

  1. 如果沒有初始化就先調用initTable()方法來進行初始化過程

  2. 如果沒有hash沖突就直接CAS插入

  3. 如果還在進行擴容操作就先進行擴容

  4. 如果存在hash沖突,就加鎖來保證線程安全,這里有兩種情況,一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入,

  5. 最后一個如果該鏈表的數量大於閾值8,就要先轉換成黑紅樹的結構,break再一次進入循環

  6. 如果添加成功就調用addCount()方法統計size,並且檢查是否需要擴容。

get()

  1. 計算hash值,定位到該table索引位置,如果是首節點符合就返回

  2. 如果遇到擴容的時候,會調用標志正在擴容節點ForwardingNode的find方法,查找該節點,匹配就返回

  3. 以上都不符合的話,就往下遍歷節點,匹配就返回,否則最后就返回null

概括版:

(1)對於get讀操作,如果當前節點有數據,還沒遷移完成,此時不影響讀,能夠正常進行。

如果當前鏈表已經遷移完成,那么頭節點會被設置成fwd節點,此時get線程會幫助擴容。

(2)對於put/remove寫操作,如果當前鏈表已經遷移完成,那么頭節點會被設置成fwd節點,此時寫線程會幫助擴容,如果擴容沒有完成,當前鏈表的頭節點會被鎖住,所以寫線程會被阻塞,直到擴容完成。

擴容機制:https://www.e-learn.cn/content/java/1154828

引入了一個ForwardingNode類,在一個線程發起擴容的時候,就會改變sizeCtl這個值,

  1. sizeCtl :默認為0,用來控制table的初始化和擴容操作,具體應用在后續會體現出來。

  2. -1 代表table正在初始化

  3. -N 表示有N-1個線程正在進行擴容操作  。

擴容時候會判斷這個值,

如果超過閾值就要擴容,首先根據運算得到需要遍歷的次數i,然后利用tabAt方法獲得i位置的元素f,初始化一個forwardNode實例fwd,如果f == null,則在table中的i位置放入fwd,

否則采用頭插法的方式把當前舊table數組的指定任務范圍的數據給遷移到新的數組中,

然后

給舊table原位置賦值fwd。直到遍歷過所有的節點以后就完成了復制工作,把table指向nextTable,並更新sizeCtl為新數組大小的0.75倍 ,擴容完成。在此期間如果其他線程的有讀寫操作都會判斷head節點是否為forwardNode節點,如果是就幫助擴容。

Hashtable底層原理:

概念:

HashTable類繼承自Dictionary類, 實現了Map接口。 大部分的操作都是通過synchronized鎖保護的,是線程安全的, key、value都不可以為null, 每次put方法不允許null值,如果發現是null,則直接拋出異常。

官方文檔也說了:如果在非線程安全的情況下使用,建議使用HashMap替換,如果在線程安全的情況下使用,建議使用ConcurrentHashMap替換。

數據結構:

數組+鏈表。

存取實現:

put():

限制了value不能為null。

由於直接使用key.hashcode(),而沒有向hashmap一樣先判斷key是否為null,所以key為null時,調用key.hashcode()會出錯,所以hashtable中key也不能為null。

Hashtable是在鏈表的頭部添加元素的。

int index = (hash & 0x7FFFFFFF) %tab.length;獲取index的方式與HashMap不同

擴容機制:

Hashtable默認capacity是11,默認負載因子是0.75.。當前表中的Entry數量,如果超過了閾值,就會擴容,即調用rehash方法,重新計算每個鍵值對的hashCode;

判斷新的容量是否超過了上限,沒超過就新建一個新數組,大小為原數組的2倍+1,將舊數的鍵值對重新hash添加到新數組中。

JVM調優

查看堆空間大小分配(年輕代、年老代、持久代分配)

垃圾回收監控(長時間監控回收情況)

線程信息監控:系統線程數量

線程狀態監控:各個線程都處在什么樣的狀態下

線程詳細信息:查看線程內部運行情況,死鎖檢查

CPU熱點:檢查系統哪些方法占用了大量CPU時間

內存熱點:檢查哪些對象在系統中數量最大

jvm問題排查和調優:

jps主要用來輸出JVM中運行的進程狀態信息。

jstat命令可以用於持續觀察虛擬機內存中各個分區的使用率以及GC的統計數據

jmap可以用來查看堆內存的使用詳情。

jstack可以用來查看Java進程內的線程堆棧信息。 jstack是個非常好用的工具,結合應用日志可以迅速定位到問題線程。

Java性能分析工具
jdk會自帶JMC(JavaMissionControl)工具。可以分析本地應用以及連接遠程ip使用。提供了實時分析線程、內存,CPU、GC等信息的可視化界面。

JVM內存調優

對JVM內存的系統級的調優主要的目的是減少GC的頻率和Full GC的次數。 過多的GC和Full GC是會占用很多的系統資源(主要是CPU),影響系統的吞吐量。

使用JDK提供的內存查看工具,比如JConsole和Java VisualVM。

導致Full GC一般由於以下幾種情況:

舊生代空間不足

調優時盡量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象

新生代設置過小

一是新生代GC次數非常頻繁,增大系統消耗;二是導致大對象直接進入舊生代,占據了舊生代剩余空間,誘發Full GC

2). 新生代設置過大

一是新生代設置過大會導致舊生代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加

3). Survivor設置過小

導致對象從eden直接到達舊生代

4). Survivor設置過大

導致eden過小,增加了GC頻率

一般說來新生代占整個堆1/3比較合適

GC策略的設置方式

1). 吞吐量優先 可由-XX:GCTimeRatio=n來設置

2). 暫停時間優先 可由-XX:MaxGCPauseRatio=n來設置

JVM內存管理:

1.先講內存5大模塊以及他們各種的作用。

2.將垃圾收集器,垃圾收集算法

3.適當講講GC優化,JVM優化

JVM的常見的垃圾收集器:

(注:此回答源於楊曉峰的Java核心技術36講之一)

GC調優:

  • GC日志分析

  • 調優命令

  • 調優工具

調優命令

Sun JDK監控和故障處理命令有jps jstat jmap jhat jstack jinfo

  • jps,JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機進程。

  • jstat,JVM statistics Monitoring是用於監視虛擬機運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據。

  • jmap,JVM Memory Map命令用於生成heap dump文件

  • jhat,JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內置了一個微型的HTTP/HTML服務器,生成dump的分析結果后,可以在瀏覽器中查看

  • jstack,用於生成java虛擬機當前時刻的線程快照。

  • jinfo,JVM Configuration info 這個命令作用是實時查看和調整虛擬機運行參數。

調優工具

常用調優工具分為兩類,jdk自帶監控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

  • jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制台,用於對JVM中內存,線程和類等的監控

GC觸發的條件有兩種。(1)程序調用System.gc時可以觸發;(2)系統自身來決定GC觸發的時機。

要完全回收一個對象,至少需要經過兩次標記的過程。

第一次標記:對於一個沒有其他引用的對象,篩選該對象是否有必要執行finalize()方法,如果沒有執行必要,則意味可直接回收。(篩選依據:是否復寫或執行過finalize()方法;因為finalize方法只能被執行一次)。

第二次標記:如果被篩選判定位有必要執行,則會放入FQueue隊列,並自動創建一個低優先級的finalize線程來執行釋放操作。如果在一個對象釋放前被其他對象引用,則該對象會被移除FQueue隊列。

Minor GC ,Full GC 觸發條件

Minor GC觸發條件:當Eden區滿時,觸發Minor GC。

Full GC觸發條件:

(1)調用System.gc時,系統建議執行Full GC,但是不必然執行

(2)老年代空間不足

(3)方法區空間不足

(4)通過Minor GC后進入老年代的平均大小大於老年代的可用內存

(5)由Eden區、From Space區向To Space區復制時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

java內存模型

與JVM 內存模型不同。

Java內存模型即Java Memory Model,簡稱JMM。JMM定義了Java 虛擬機(JVM)在計算機內存(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬於JVM的。

Java內存模型定義了多線程之間共享變量的可見性以及如何在需要的時候對共享變量進行同步。

Java線程之間的通信采用的是過共享內存模型,這里提到的共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。

線程池的工作原理

1.先講下作用

減少資源的開銷    可以減少每次創建銷毀線程的開銷

提高響應速度    由於線程已經創建成功

提高線程的可管理性

2.講實現

線程池主要有兩部分組成,多個工作線程和一個阻塞隊列。

其中 工作線程是一組已經處在運行中的線程,它們不斷地向阻塞隊列中領取任務執行。而 阻塞隊列用於存儲工作線程來不及處理的任務。

3.細分講下線程的組成

創建一個線程池需要要的一些核心參數。

corePoolSize:基本線程數量 它表示你希望線程池達到的一個值。線程池會盡量把實際線程數量保持在這個值上下。

maximumPoolSize:最大線程數量 這是線程數量的上界。 如果實際線程數量達到這個值: 阻塞隊列未滿:任務存入阻塞隊列等待執行 阻塞隊列已滿:調用飽和策略 。

keepAliveTime:空閑線程的存活時間 當實際線程數量超過corePoolSize時,若線程空閑的時間超過該值,就會被停止。 PS:當任務很多,且任務執行時間很短的情況下,可以將該值調大,提高線程利用率。

timeUnit:keepAliveTime的單位

runnableTaskQueue:任務隊列

這是一個存放任務的阻塞隊列,可以有如下幾種選擇:

ArrayBlockingQueue 它是一個由數組實現的阻塞隊列,FIFO。

LinkedBlockingQueue 它是一個由鏈表實現的阻塞隊列,FIFO。 吞吐量通常要高於ArrayBlockingQueue。fixedThreadPool使用的阻塞隊列就是它。 它是一個無界隊列。

SynchronousQueue 它是一個沒有存儲空間的阻塞隊列,任務提交給它之后必須要交給一條工作線程處理;如果當前沒有空閑的工作線程,則立即創建一條新的工作線程。 cachedThreadPool用的阻塞隊列就是它。 它是一個無界隊列。 PriorityBlockingQueue 它是一個優先權阻塞隊列。

handler:飽和策略 當實際線程數達到maximumPoolSize,並且阻塞隊列已滿時,就會調用飽和策略。

AbortPolicy 默認。直接拋異常。 CallerRunsPolicy 只用調用者所在的線程執行任務。 DiscardOldestPolicy 丟棄任務隊列中最久的任務。 DiscardPolicy 丟棄當前任務。

4.運行機制

當有請求到來時:

1.若當前實際線程數量 少於 corePoolSize,即使有空閑線程,也會創建一個新的工作線程;

2 若當前實際線程數量處於corePoolSize和maximumPoolSize之間,並且阻塞隊列沒滿,則任務將被放入阻塞隊列中等待執行;

3.若當前實際線程數量 小於 maximumPoolSize,但阻塞隊列已滿,則直接創建新線程處理任務;

4.若當前實際線程數量已經達到maximumPoolSize,並且阻塞隊列已滿,則使用飽和策略。

ThreadLocal的底層原理

概括:

該類提供了線程局部 (thread-local) 變量。這些變量不同於它們的普通對應物,因為訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量

使用:

set(obj):向當前線程中存儲數據 get():獲取當前線程中的數據 remove():刪除當前線程中的數據

實現原理:

ThreadLocal並不維護ThreadLocalMap(ThreadLocalMap是Thread的)並不是一個存儲數據的容器,它只是相當於一個工具包,提供了操作該容器的方法,如get、set、remove等。而ThreadLocal內部類ThreadLocalMap才是存儲數據的容器,並且該容器由Thread維護。 每一個Thread對象均含有一個ThreadLocalMap類型的成員變量threadLocals,它存儲本線程中所有ThreadLocal對象及其對應的值( ThreadLocalMap 是個弱引用類,內部 一個Entry由ThreadLocal對象和Object構成,

為什么要用弱引用呢?

如果是直接new一個對象的話,使用完之后設置為null后才能被垃圾收集器清理,如果為弱引用,使用完后垃圾收集器自動清理key,程序員不用再關注指針。

操作細節

進行set,get等操作都是首先會獲取當前線程對象,然后獲取當前線程的ThreadLocalMap對象。再以當前ThreadLocal對象為key ,再做相應的處理。

內存泄露問題

在ThreadLocalMap中,只有key是弱引用,value仍然是一個強引用。

每次操作set、get、remove操作時,ThreadLocal都會將key為null的Entry刪除,從而避免內存泄漏。

當然,當 如果一個線程運行周期較長,而且將一個大對象放入LocalThreadMap后便不再調用set、get、remove方法,此時該仍然可能會導致內存泄漏。 這個問題確實存在,沒辦法通過ThreadLocal解決,而是需要程序員在完成ThreadLocal的使用后要養成手動調用remove的習慣,從而避免內存泄漏。

使用場景;

Web系統Session的存儲

當請求到來時,可以將當前Session信息存儲在ThreadLocal中,在請求處理過程中可以隨時使用Session信息,每個請求之間的Session信息互不影響。當請求處理完成后通過remove方法將當前Session信息清除即可。

voliate 的實現原理

為什么volatile能保證共享變量的內存可見性?

volatile變量寫

當被volatile修飾的變量進行寫操作時,這個變量將會被直接寫入共享內存,而非線程的專屬存儲空間。

volatile變量讀

當讀取一個被volatile修飾的變量時,會直接從共享內存中讀,而非線程專屬的存儲空間中讀。

禁止指令重排序

volatile讀

若volatile讀操作的前一行為volatile讀/寫,則這兩行不會發生重排序 volatile讀操作和它后一行代碼都不會發生重排序

volatile寫

volatile寫操作和它前一行代碼都不會發生重排序; 若volatile寫操作的后一行代碼為volatile讀/寫,則這兩行不會發生重排序。

當volatile變量寫后,線程中本地內存中共享變量就會置為失效的狀態,因此線程B再需要讀取從主內存中去讀取該變量的最新值。

NIO底層原理

1概念:

NIO 指新IO,核心是 同步非阻塞,解決傳統IO的阻塞問題。操作對象是Buffer。 其實NIO的核心是IO線程池,(一定要記住這個關鍵點)。 NIO中的IO多路復用調用系統級別的select和poll模型,由系統進行監控IO狀態,避免用戶線程通過反復嘗試的方式查詢狀態。

  • Java NIO : 同步非阻塞,服務器實現模式為一個請求一個線程,即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。

2.工作原理:

  1. 由一個專門的線程來處理所有的 IO 事件,並負責分發。

  2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。

  3. 線程通訊:線程之間通過 wait,notify 等方式通訊。保證每次上下文切換都是有意義的。減少無謂的線程切換。

3.通信模型是怎么實現的呢?

java NIO采用了雙向通道(channel)進行數據傳輸,而不是單向的流(stream),在通道上可以注冊我們感興趣的事件。

四種事件

服務端接收客戶端連接事件SelectionKey.OP_ACCEPT(16)

客戶端連接服務端事件SelectionKey.OP_CONNECT(8)

讀事件SelectionKey.OP_READ(1)

寫事件SelectionKey.OP_WRITE(4)

服務端和客戶端各自維護一個管理通道的對象,我們稱之為selector,該對象能檢測一個或多個通道 (channel) 上的事件。我們以服務端為例,如果服務端的selector上注冊了讀事件,某時刻客戶端給服務端發送了一些數據,阻塞I/O這時會調用read()方法阻塞地讀取數據,而NIO的服務端會在selector中添加一個讀事件。服務端的處理線程會輪詢地訪問selector,如果訪問selector時發現有感興趣的事件到達,則處理這些事件,如果沒有感興趣的事件到達,則處理線程會一直阻塞直到感興趣的事件到達為止。

IOC底層實現原理

概念:

IOC 是面向對象編程中的一種設計原則,IOC理論提出的觀點大體是這樣的:借助於“第三方”實現具有依賴關系的對象之間的解耦。 。所謂IoC,對於spring框架來說,就是由spring來負責控制對象的生命周期和對象間的關系。 是說創建對象的控制權進行轉移,以前創建對象的主動權和創建時機是由自己把控的,而現在這種權力轉移到第三方。

實現原理:

它是通過反射機制+工廠模式實現的,在實例化一個類時,它通過反射調用類中set方法將事先保存在HashMap中的類屬性注入到類中。

控制反轉就是:獲得依賴對象的方式反轉了。

1、依賴注入發生的時間

(1).用戶第一次通過getBean方法向IoC容索要Bean時,IoC容器觸發依賴注入。

(2).當用戶在Bean定義資源中為 元素配置了lazy-init屬性,即讓容器在解析注冊Bean定義時進行預實例化,觸發依賴注入。

2.依賴注入實現在以下兩個方法中:

(1).createBeanInstance:生成Bean所包含的java對象實例。

(2).populateBean :對Bean屬性的依賴注入進行處理。

AOP底層實現原理

概念

AOP(Aspect-OrientedProgramming,面向方面編程),可以說是OOP(Object-Oriented Programing,面向對象編程)的補充和完善。OOP引入封裝、繼承和多態性等概念來建立一種對象層次結構,用以模擬公共行為的一個集合。  而AOP技術則恰恰相反,它利用一種稱為“橫切”的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行為封裝到一個可重用模塊,並將其名為“Aspect”,即方面。 簡單地說,就是將那些與業務無關,卻為業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重復代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。

AOP的核心思想就是“將應用程序中的商業邏輯同對其提供支持的通用服務進行分離。

AOP的實現

實現AOP的技術,主要分為兩大類:一是采用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行為的執行;二是采用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼。

如何使用Spring AOP

可以通過配置文件或者編程的方式來使用Spring AOP。   配置可以通過xml文件來進行,大概有四種方式:

  1. 配置ProxyFactoryBean,顯式地設置advisors, advice, target等

2.        配置AutoProxyCreator,這種方式下,還是如以前一樣使用定義的bean,但是從容器中獲得的其實已經是代理對象 3.        通過aop:config來配置

4.        通aop:aspectj-autoproxy過來配置,使用AspectJ的注解來標識通知及切入點

Spring AOP的實現

如何生成代理類:

Spring提供了兩種方式來生成代理對象: JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport對象的配置來決定。默認的策略是如果目標類是接口,則使用JDK動態代理技術,否則使用Cglib來生成代理

切面是如何織入的?

InvocationHandler是JDK動態代理的核心,生成的代理對象的方法調用都會委托到InvocationHandler.invoke()方法。

MyisAM和innodb的有關索引的疑問

兩者都是什么索引?聚集還是非聚集https://www.cnblogs.com/olinux/p/5217186.html

MyISAM( 非聚集)

使用B+Tree作為索引結構,葉節點的data域存放的是數據記錄的地址。

MyISAM中索引檢索的算法為首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,則取出其data域的值,然后以data域的值為地址,讀取相應數據記錄。

InnoDB( 聚集索引)

第一個重大區別是InnoDB的數據文件本身就是索引文件, 這棵樹的葉節點data域保存了完整的數據記錄。

但是輔助索引搜索需要檢索兩遍索引:首先檢索輔助索引獲得主鍵,然后用主鍵到主索引中檢索獲得記錄。

因為InnoDB的數據文件本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會自動選擇一個可以唯一標識數據記錄的列作為主鍵,如果不存在這種列,則MySQL自動為InnoDB表生成一個隱含字段作為主鍵,這個字段長度為6個字節,類型為長整形。

簡單說:

如果我們定義了主鍵(PRIMARY KEY),那么InnoDB會選擇其作為聚集索引;如果沒有顯式定義主鍵,則InnoDB會選擇第一個不包含有NULL值的唯一索引作為主鍵索引;

作者:睶先生
來源:CSDN
原文:https://blog.csdn.net/Butterfly_resting/article/details/82894172
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM